Files
versitygw/iamapi/controller.go
T
niksis02 90119359ee fix: infer UserName from the calling access key in the access-key APIs
`CreateAccessKey`, `UpdateAccessKey`, `DeleteAccessKey` and `ListAccessKeys` required an explicit `UserName`. Real IAM treats it as optional and resolves it from the access key signing the request, so an IAM user managing its own keys had to name itself. They now infer it, matching AWS: only an entirely absent parameter is inferred, while a present-but-empty one stays a `ValidationError`, and the inferred scope is strictly the caller's own user — another user's key id returns `NoSuchEntity` rather than being touched.

A caller with no IAM user of its own gets IAM's own `Must specify userName when calling with non-User credentials` `ValidationError`, shared with `GetUser` as `iamerr.MustSpecifyUserName`. That covers assumed-role sessions and also the gateway's root credential, which is configured rather than stored as an IAM user and so owns no access keys the API could manage — real IAM manages the root account's own keys here, which has no equivalent in this gateway.

The policy middleware resolves the same four actions through `callerOrNamedUserResource`, so the resource-level check targets the caller's own user ARN when UserName is omitted instead of falling back to no resource at all, which would have denied every request authorized by an own-ARN-scoped grant.
2026-09-02 16:15:53 +04:00

1551 lines
49 KiB
Go

// 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 iamapi
import (
"errors"
"fmt"
"slices"
"time"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/iamapi/iamerr"
"github.com/versity/versitygw/iamapi/internal/iamutil"
"github.com/versity/versitygw/iamapi/policy"
"github.com/versity/versitygw/iamapi/storage"
"github.com/versity/versitygw/iamapi/types"
"github.com/versity/versitygw/internal/httpctx"
)
type IAMApiController struct {
store storage.Storer
// oidcThumbprintAutoFetchDisabled disables CreateOpenIDConnectProvider's
// TLS auto-fetch fallback when ThumbprintList is omitted (operational
// safety valve for restricted/air-gapped deployments); set via
// iamapi.WithOIDCThumbprintAutoFetchDisabled(). Defaults to false
// (auto-fetch enabled), matching real AWS behavior.
oidcThumbprintAutoFetchDisabled bool
}
func NewController(store storage.Storer, oidcThumbprintAutoFetchDisabled bool) IAMApiController {
return IAMApiController{
store: store,
oidcThumbprintAutoFetchDisabled: oidcThumbprintAutoFetchDisabled,
}
}
func (c IAMApiController) CreateUser(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserName(ctx, "CreateUser", iamutil.MaxUserNameLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
path, ok := iamutil.RequestParam(ctx, "Path")
if !ok || path == "" {
path = iamutil.DefaultUserPath
}
if err := iamutil.ValidatePath("path", path); err != nil {
return nil, err
}
tags, err := iamutil.ParseTags(ctx, iamutil.TagKeysFolded)
if err != nil {
return nil, err
}
for range 3 {
userID, err := iamutil.GenerateUserID()
if err != nil {
return nil, err
}
user := types.User{
Path: path,
UserName: userName,
UserID: userID,
Arn: iamutil.BuildUserArn(iamutil.DefaultAccountID, path, userName),
CreateDate: time.Now().UTC().Truncate(time.Second),
Tags: tags,
}
stored, err := c.store.CreateUser(ctx.Context(), user)
if errors.Is(err, storage.ErrUserIDAlreadyExists) {
debuglogger.Logf("IAM user ID collision while creating user %q: %v", userName, err)
continue
}
if err != nil {
debuglogger.Logf("failed to create IAM user %q: %v", userName, err)
return nil, err
}
return &Response{Data: &types.CreateUserResponse{
Result: types.CreateUserResult{User: *stored},
}}, nil
}
err = fmt.Errorf("generate IAM user id: exhausted collision retries")
debuglogger.Logf("failed to create IAM user %q: %v", userName, err)
return nil, err
}
func (c IAMApiController) DeleteUser(ctx fiber.Ctx) (*Response, error) {
username, err := iamutil.GetUserName(ctx, "DeleteUser", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
if err != nil {
return nil, err
}
if err := c.store.DeleteUser(ctx.Context(), username); err != nil {
debuglogger.Logf("failed to delete IAM user %q: %v", username, err)
return nil, err
}
return &Response{Data: &types.DeleteUserResponse{}}, nil
}
func (c IAMApiController) GetUser(ctx fiber.Ctx) (*Response, error) {
username, ok := iamutil.RequestParam(ctx, "UserName")
if !ok || username == "" {
// Real IAM treats an omitted UserName as "look up the caller's own identity
identity, _ := httpctx.ContextKeyCallerIdentity.Get(ctx).(types.Identity)
switch {
case identity.IsRoot:
return &Response{Data: &types.GetUserResponse{
Result: types.GetUserResult{User: types.User{
UserID: iamutil.DefaultAccountID,
Arn: fmt.Sprintf("arn:aws:iam::%s:root", iamutil.DefaultAccountID),
}},
}}, nil
case identity.User != nil:
return &Response{Data: &types.GetUserResponse{
Result: types.GetUserResult{User: *identity.User},
}}, nil
default:
return nil, iamerr.MustSpecifyUserName()
}
}
if err := iamutil.ValidateName("userName", username, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
user, err := c.store.GetUser(ctx.Context(), username)
if err != nil {
debuglogger.Logf("failed to get IAM user %q: %v", username, err)
return nil, err
}
return &Response{Data: &types.GetUserResponse{
Result: types.GetUserResult{User: *user},
}}, nil
}
func (c IAMApiController) ListUsers(ctx fiber.Ctx) (*Response, error) {
pathPrefix, ok := iamutil.RequestParam(ctx, "PathPrefix")
if !ok || pathPrefix == "" {
pathPrefix = iamutil.DefaultUserPath
}
if err := iamutil.ValidatePathPrefix(pathPrefix); err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListUsers")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListUsers(ctx.Context(), storage.ListUsersInput{
PathPrefix: pathPrefix,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM users: %v", err)
return nil, err
}
return &Response{Data: &types.ListUsersResponse{
Result: types.ListUsersResult{
Users: types.Users{Members: out.Users},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) UpdateUser(ctx fiber.Ctx) (*Response, error) {
username, err := iamutil.GetUserName(ctx, "UpdateUser", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
if err != nil {
return nil, err
}
newPath, _ := iamutil.RequestParam(ctx, "NewPath")
if newPath != "" {
if err := iamutil.ValidatePath("newPath", newPath); err != nil {
return nil, err
}
}
newUserName, _ := iamutil.RequestParam(ctx, "NewUserName")
if newUserName != "" {
if err := iamutil.ValidateName("newUserName", newUserName, iamutil.MaxUserNameLen); err != nil {
return nil, err
}
}
user, err := c.store.GetUser(ctx.Context(), username)
if err != nil {
debuglogger.Logf("failed to get IAM user %q for update: %v", username, err)
return nil, err
}
finalPath := user.Path
if newPath != "" {
finalPath = newPath
}
finalUserName := user.UserName
if newUserName != "" {
finalUserName = newUserName
}
updated, err := c.store.UpdateUser(ctx.Context(), storage.UpdateUserInput{
UserName: username,
NewPath: newPath,
NewUserName: newUserName,
NewArn: iamutil.BuildUserArn(iamutil.DefaultAccountID, finalPath, finalUserName),
})
if err != nil {
debuglogger.Logf("failed to update IAM user %q: %v", finalUserName, err)
return nil, err
}
return &Response{Data: &types.UpdateUserResponse{
Result: types.UpdateUserResult{User: updated},
}}, nil
}
// TagUser adds or replaces tags on an existing user. AWS validates the
// request in full before it ever looks the user up, so a malformed tag on a
// non-existent user reports the tag error, not NoSuchEntity.
func (c IAMApiController) TagUser(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserName(ctx, "TagUser", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
tags, err := iamutil.ParseTags(ctx, iamutil.TagKeysFolded)
if err != nil {
return nil, err
}
if len(tags) == 0 {
debuglogger.Logf("missing required TagUser parameter: Tags")
return nil, iamerr.MissingValue("tags")
}
if err := c.store.TagUser(ctx.Context(), userName, tags); err != nil {
debuglogger.Logf("failed to tag IAM user %q: %v", userName, err)
return nil, err
}
return &Response{Data: &types.TagUserResponse{}}, nil
}
// UntagUser removes the named tags from an existing user. Removal is
// idempotent: a key naming no current tag is not an error.
func (c IAMApiController) UntagUser(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserName(ctx, "UntagUser", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
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 UntagUser parameter: TagKeys")
return nil, iamerr.MissingValue("tagKeys")
}
if err := c.store.UntagUser(ctx.Context(), userName, tagKeys); err != nil {
debuglogger.Logf("failed to untag IAM user %q: %v", userName, err)
return nil, err
}
return &Response{Data: &types.UntagUserResponse{}}, nil
}
func (c IAMApiController) ListUserTags(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserName(ctx, "ListUserTags", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListUserTags")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListUserTags(ctx.Context(), storage.ListUserTagsInput{
UserName: userName,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM user %q tags: %v", userName, err)
return nil, err
}
return &Response{Data: &types.ListUserTagsResponse{
Result: types.ListUserTagsResult{
Tags: types.Tags{Members: out.Tags},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) CreateAccessKey(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserNameOrCaller(ctx, "CreateAccessKey", iamutil.MaxUserLookupLen)
if err != nil {
return nil, err
}
for range 3 {
accessKeyID, err := iamutil.GenerateAccessKeyID()
if err != nil {
return nil, err
}
secretAccessKey, err := iamutil.GenerateSecretAccessKey()
if err != nil {
return nil, err
}
stored, err := c.store.CreateAccessKey(ctx.Context(), storage.CreateAccessKeyInput{
UserName: userName,
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
Status: iamutil.AccessKeyStatusActive,
CreateDate: time.Now().UTC().Truncate(time.Second),
})
if errors.Is(err, storage.ErrAccessKeyIDAlreadyExists) {
debuglogger.Logf("IAM access key id collision for user %q: %v", userName, err)
continue
}
if err != nil {
debuglogger.Logf("failed to create IAM access key for user %q: %v", userName, err)
return nil, err
}
return &Response{
Data: &types.CreateAccessKeyResponse{
Result: types.CreateAccessKeyResult{AccessKey: *stored},
},
}, nil
}
err = fmt.Errorf("generate IAM access key id: exhausted collision retries")
debuglogger.Logf("failed to create IAM access key for user %q: %v", userName, err)
return nil, err
}
func (c IAMApiController) UpdateAccessKey(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserNameOrCaller(ctx, "UpdateAccessKey", iamutil.MaxUserLookupLen)
if err != nil {
return nil, err
}
accessKeyID, ok := iamutil.RequestParam(ctx, "AccessKeyId")
if !ok || accessKeyID == "" {
debuglogger.Logf("missing required UpdateAccessKey parameter: AccessKeyId")
return nil, iamerr.MissingParameter("AccessKeyId")
}
if err := iamutil.ValidateAccessKeyID(accessKeyID); err != nil {
return nil, err
}
status, ok := iamutil.RequestParam(ctx, "Status")
if !ok || status == "" {
debuglogger.Logf("missing required UpdateAccessKey parameter: Status")
return nil, iamerr.MissingParameter("Status")
}
if err := iamutil.ValidateAccessKeyStatus(status); err != nil {
return nil, err
}
if err := c.store.UpdateAccessKey(ctx.Context(), storage.UpdateAccessKeyInput{
UserName: userName,
AccessKeyID: accessKeyID,
Status: status,
}); err != nil {
debuglogger.Logf("failed to update IAM access key %q for user %q: %v", accessKeyID, userName, err)
return nil, err
}
return &Response{Data: &types.UpdateAccessKeyResponse{}}, nil
}
func (c IAMApiController) DeleteAccessKey(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserNameOrCaller(ctx, "DeleteAccessKey", iamutil.MaxUserLookupLen)
if err != nil {
return nil, err
}
accessKeyID, ok := iamutil.RequestParam(ctx, "AccessKeyId")
if !ok || accessKeyID == "" {
debuglogger.Logf("missing required DeleteAccessKey parameter: AccessKeyId")
return nil, iamerr.MissingParameter("AccessKeyId")
}
if err := iamutil.ValidateAccessKeyID(accessKeyID); err != nil {
return nil, err
}
if err := c.store.DeleteAccessKey(ctx.Context(), userName, accessKeyID); err != nil {
debuglogger.Logf("failed to delete IAM access key %q for user %q: %v", accessKeyID, userName, err)
return nil, err
}
return &Response{Data: &types.DeleteAccessKeyResponse{}}, nil
}
func (c IAMApiController) GetAccessKeyLastUsed(ctx fiber.Ctx) (*Response, error) {
accessKeyID, ok := iamutil.RequestParam(ctx, "AccessKeyId")
if !ok || accessKeyID == "" {
debuglogger.Logf("missing required GetAccessKeyLastUsed parameter: AccessKeyId")
return nil, iamerr.MissingParameter("AccessKeyId")
}
if err := iamutil.ValidateAccessKeyID(accessKeyID); err != nil {
return nil, err
}
out, err := c.store.GetAccessKeyLastUsed(ctx.Context(), accessKeyID)
if err != nil {
debuglogger.Logf("failed to get IAM access key last used %q: %v", accessKeyID, err)
return nil, err
}
serviceName := out.ServiceName
if serviceName == "" {
serviceName = "N/A"
}
region := out.Region
if region == "" {
region = "N/A"
}
var lastUsedDate *time.Time
if !out.LastUsedDate.IsZero() {
lastUsedDate = &out.LastUsedDate
}
return &Response{Data: &types.GetAccessKeyLastUsedResponse{
Result: types.GetAccessKeyLastUsedResult{
UserName: out.UserName,
AccessKeyLastUsed: types.AccessKeyLastUsed{
LastUsedDate: lastUsedDate,
ServiceName: serviceName,
Region: region,
},
},
}}, nil
}
func (c IAMApiController) ListAccessKeys(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserNameOrCaller(ctx, "ListAccessKeys", iamutil.MaxUserLookupLen)
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListAccessKeys")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListAccessKeys(ctx.Context(), storage.ListAccessKeysInput{
UserName: userName,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM access keys for user %q: %v", userName, err)
return nil, err
}
return &Response{Data: &types.ListAccessKeysResponse{
Result: types.ListAccessKeysResult{
AccessKeyMetadata: types.AccessKeyMetadataList{Members: out.AccessKeys},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) PutUserPolicy(ctx fiber.Ctx) (*Response, error) {
policyDocument, ok := iamutil.RequestParam(ctx, "PolicyDocument")
if !ok {
debuglogger.Logf("missing required PutUserPolicy parameter: PolicyDocument")
return nil, iamerr.MissingValue("policyDocument")
}
if err := policy.Validate("policyDocument", policyDocument); err != nil {
return nil, err
}
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required PutUserPolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
userName, err := iamutil.GetUserName(ctx, "PutUserPolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
// Confirm the user exists before inspecting policy document content
if _, err := c.store.GetUser(ctx.Context(), userName); err != nil {
debuglogger.Logf("failed to get IAM user %q for PutUserPolicy: %v", userName, err)
return nil, err
}
if err := policy.Parse(policyDocument); err != nil {
return nil, err
}
if err := c.store.PutUserPolicy(ctx.Context(), storage.PutUserPolicyInput{
UserName: userName,
PolicyName: policyName,
PolicyDocument: policyDocument,
}); err != nil {
debuglogger.Logf("failed to put IAM user policy %q for user %q: %v", policyName, userName, err)
return nil, err
}
return &Response{Data: &types.PutUserPolicyResponse{}}, nil
}
func (c IAMApiController) GetUserPolicy(ctx fiber.Ctx) (*Response, error) {
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required GetUserPolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
userName, err := iamutil.GetUserName(ctx, "GetUserPolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
entry, err := c.store.GetUserPolicy(ctx.Context(), userName, policyName)
if err != nil {
debuglogger.Logf("failed to get IAM user policy %q for user %q: %v", policyName, userName, err)
return nil, err
}
return &Response{Data: &types.GetUserPolicyResponse{
Result: types.GetUserPolicyResult{
UserName: userName,
PolicyName: entry.PolicyName,
PolicyDocument: iamutil.EncodePolicyDocument(entry.PolicyDocument),
},
}}, nil
}
func (c IAMApiController) DeleteUserPolicy(ctx fiber.Ctx) (*Response, error) {
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required DeleteUserPolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
userName, err := iamutil.GetUserName(ctx, "DeleteUserPolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
if err := c.store.DeleteUserPolicy(ctx.Context(), userName, policyName); err != nil {
debuglogger.Logf("failed to delete IAM user policy %q for user %q: %v", policyName, userName, err)
return nil, err
}
return &Response{Data: &types.DeleteUserPolicyResponse{}}, nil
}
func (c IAMApiController) ListUserPolicies(ctx fiber.Ctx) (*Response, error) {
userName, err := iamutil.GetUserName(ctx, "ListUserPolicies", iamutil.MaxUserLookupLen, iamerr.MissingValue("userName"))
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListUserPolicies")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListUserPolicies(ctx.Context(), storage.ListUserPoliciesInput{
UserName: userName,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM user policies for user %q: %v", userName, err)
return nil, err
}
return &Response{Data: &types.ListUserPoliciesResponse{
Result: types.ListUserPoliciesResult{
PolicyNames: types.PolicyNameList{Members: out.PolicyNames},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) CreateRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "CreateRole", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
path, ok := iamutil.RequestParam(ctx, "Path")
if !ok || path == "" {
path = iamutil.DefaultUserPath
}
if err := iamutil.ValidatePath("path", path); err != nil {
return nil, err
}
assumeRolePolicyDocument, ok := iamutil.RequestParam(ctx, "AssumeRolePolicyDocument")
if !ok || assumeRolePolicyDocument == "" {
debuglogger.Logf("missing required CreateRole parameter: AssumeRolePolicyDocument")
return nil, iamerr.MissingValue("assumeRolePolicyDocument")
}
if err := policy.Validate("assumeRolePolicyDocument", assumeRolePolicyDocument); err != nil {
return nil, err
}
if err := policy.ParseTrust(assumeRolePolicyDocument); err != nil {
return nil, err
}
if len(assumeRolePolicyDocument) > policy.MaxTrustPolicyBytes {
return nil, iamerr.TrustPolicySizeLimitExceeded(policy.MaxTrustPolicyBytes)
}
description, _ := iamutil.RequestParam(ctx, "Description")
if err := iamutil.ValidateDescription("description", description); err != nil {
return nil, err
}
maxSessionDuration, err := iamutil.ParseMaxSessionDuration(ctx)
if err != nil {
return nil, err
}
tags, err := iamutil.ParseTags(ctx, iamutil.TagKeysFolded)
if err != nil {
return nil, err
}
for range 3 {
roleID, err := iamutil.GenerateRoleID()
if err != nil {
return nil, err
}
role := types.Role{
Path: path,
RoleName: roleName,
RoleID: roleID,
Arn: iamutil.BuildRoleArn(iamutil.DefaultAccountID, path, roleName),
CreateDate: time.Now().UTC().Truncate(time.Second),
AssumeRolePolicyDocument: assumeRolePolicyDocument,
Description: description,
MaxSessionDuration: maxSessionDuration,
Tags: tags,
}
stored, err := c.store.CreateRole(ctx.Context(), role)
if errors.Is(err, storage.ErrRoleIDAlreadyExists) {
debuglogger.Logf("IAM role ID collision while creating role %q: %v", roleName, err)
continue
}
if err != nil {
debuglogger.Logf("failed to create IAM role %q: %v", roleName, err)
return nil, err
}
stored.AssumeRolePolicyDocument = iamutil.EncodePolicyDocument(stored.AssumeRolePolicyDocument)
return &Response{Data: &types.CreateRoleResponse{
Result: types.CreateRoleResult{Role: stored},
}}, nil
}
err = fmt.Errorf("generate IAM role id: exhausted collision retries")
debuglogger.Logf("failed to create IAM role %q: %v", roleName, err)
return nil, err
}
func (c IAMApiController) GetRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "GetRole", iamutil.MaxRoleNameLen, iamerr.MissingParameter("RoleName"))
if err != nil {
return nil, err
}
role, err := c.store.GetRole(ctx.Context(), roleName)
if err != nil {
debuglogger.Logf("failed to get IAM role %q: %v", roleName, err)
return nil, err
}
role.AssumeRolePolicyDocument = iamutil.EncodePolicyDocument(role.AssumeRolePolicyDocument)
return &Response{Data: &types.GetRoleResponse{
Result: types.GetRoleResult{Role: role},
}}, nil
}
func (c IAMApiController) ListRoles(ctx fiber.Ctx) (*Response, error) {
pathPrefix, ok := iamutil.RequestParam(ctx, "PathPrefix")
if !ok || pathPrefix == "" {
pathPrefix = iamutil.DefaultUserPath
}
if err := iamutil.ValidatePathPrefix(pathPrefix); err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListRoles")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListRoles(ctx.Context(), storage.ListRolesInput{
PathPrefix: pathPrefix,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM roles: %v", err)
return nil, err
}
roles := make([]types.Role, len(out.Roles))
for i, role := range out.Roles {
role.AssumeRolePolicyDocument = iamutil.EncodePolicyDocument(role.AssumeRolePolicyDocument)
roles[i] = role
}
return &Response{Data: &types.ListRolesResponse{
Result: types.ListRolesResult{
Roles: types.Roles{Members: roles},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) DeleteRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "DeleteRole", iamutil.MaxRoleNameLen, iamerr.MissingParameter("RoleName"))
if err != nil {
return nil, err
}
if err := c.store.DeleteRole(ctx.Context(), roleName); err != nil {
debuglogger.Logf("failed to delete IAM role %q: %v", roleName, err)
return nil, err
}
return &Response{Data: &types.DeleteRoleResponse{}}, nil
}
func (c IAMApiController) UpdateAssumeRolePolicy(ctx fiber.Ctx) (*Response, error) {
policyDocument, ok := iamutil.RequestParam(ctx, "PolicyDocument")
if !ok {
debuglogger.Logf("missing required UpdateAssumeRolePolicy parameter: PolicyDocument")
return nil, iamerr.MissingValue("policyDocument")
}
if err := policy.Validate("policyDocument", policyDocument); err != nil {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "UpdateAssumeRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
// Confirm the role exists before inspecting policy document content
if _, err := c.store.GetRole(ctx.Context(), roleName); err != nil {
debuglogger.Logf("failed to get IAM role %q for UpdateAssumeRolePolicy: %v", roleName, err)
return nil, err
}
if err := policy.ParseTrust(policyDocument); err != nil {
return nil, err
}
if len(policyDocument) > policy.MaxTrustPolicyBytes {
return nil, iamerr.TrustPolicySizeLimitExceeded(policy.MaxTrustPolicyBytes)
}
if _, err := c.store.UpdateAssumeRolePolicy(ctx.Context(), storage.UpdateAssumeRolePolicyInput{
RoleName: roleName,
PolicyDocument: policyDocument,
}); err != nil {
debuglogger.Logf("failed to update IAM assume role policy for role %q: %v", roleName, err)
return nil, err
}
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, iamutil.TagKeysFolded)
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 {
debuglogger.Logf("missing required PutRolePolicy parameter: PolicyDocument")
return nil, iamerr.MissingValue("policyDocument")
}
if err := policy.Validate("policyDocument", policyDocument); err != nil {
return nil, err
}
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required PutRolePolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "PutRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
// Confirm the role exists before inspecting policy document content
if _, err := c.store.GetRole(ctx.Context(), roleName); err != nil {
debuglogger.Logf("failed to get IAM role %q for PutRolePolicy: %v", roleName, err)
return nil, err
}
if err := policy.Parse(policyDocument); err != nil {
return nil, err
}
if err := c.store.PutRolePolicy(ctx.Context(), storage.PutRolePolicyInput{
RoleName: roleName,
PolicyName: policyName,
PolicyDocument: policyDocument,
}); err != nil {
debuglogger.Logf("failed to put IAM role policy %q for role %q: %v", policyName, roleName, err)
return nil, err
}
return &Response{Data: &types.PutRolePolicyResponse{}}, nil
}
func (c IAMApiController) GetRolePolicy(ctx fiber.Ctx) (*Response, error) {
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required GetRolePolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "GetRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
entry, err := c.store.GetRolePolicy(ctx.Context(), roleName, policyName)
if err != nil {
debuglogger.Logf("failed to get IAM role policy %q for role %q: %v", policyName, roleName, err)
return nil, err
}
return &Response{Data: &types.GetRolePolicyResponse{
Result: types.GetRolePolicyResult{
RoleName: roleName,
PolicyName: entry.PolicyName,
PolicyDocument: iamutil.EncodePolicyDocument(entry.PolicyDocument),
},
}}, nil
}
func (c IAMApiController) DeleteRolePolicy(ctx fiber.Ctx) (*Response, error) {
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
if !ok {
debuglogger.Logf("missing required DeleteRolePolicy parameter: PolicyName")
return nil, iamerr.MissingValue("policyName")
}
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "DeleteRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
if err := c.store.DeleteRolePolicy(ctx.Context(), roleName, policyName); err != nil {
debuglogger.Logf("failed to delete IAM role policy %q for role %q: %v", policyName, roleName, err)
return nil, err
}
return &Response{Data: &types.DeleteRolePolicyResponse{}}, nil
}
func (c IAMApiController) ListRolePolicies(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "ListRolePolicies", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListRolePolicies")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListRolePolicies(ctx.Context(), storage.ListRolePoliciesInput{
RoleName: roleName,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM role policies for role %q: %v", roleName, err)
return nil, err
}
return &Response{Data: &types.ListRolePoliciesResponse{
Result: types.ListRolePoliciesResult{
PolicyNames: types.PolicyNameList{Members: out.PolicyNames},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) CreateOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
rawURL, ok := iamutil.RequestParam(ctx, "Url")
if !ok || rawURL == "" {
debuglogger.Logf("missing required CreateOpenIDConnectProvider parameter: Url")
return nil, iamerr.MissingValue("url")
}
url, err := iamutil.ValidateOIDCProviderURL(rawURL)
if err != nil {
return nil, err
}
clientIDs := iamutil.ParseStringList(ctx, "ClientIDList")
if len(clientIDs) > storage.MaxClientIDsPerOIDCProvider {
return nil, iamerr.ClientIdsPerOpenIdConnectProviderLimitExceeded(storage.MaxClientIDsPerOIDCProvider)
}
for _, id := range clientIDs {
if len(id) > iamutil.MaxOIDCClientIDLen {
return nil, iamerr.ValueTooLong("clientID", iamutil.MaxOIDCClientIDLen)
}
}
thumbprints := iamutil.ParseStringList(ctx, "ThumbprintList")
if len(thumbprints) == 0 {
if c.oidcThumbprintAutoFetchDisabled {
debuglogger.Logf("CreateOpenIDConnectProvider: ThumbprintList omitted and auto-fetch is disabled")
return nil, iamerr.MissingValue("thumbprintList")
}
fetched, err := iamutil.FetchThumbprint(ctx.Context(), url)
if err != nil {
debuglogger.Logf("failed to auto-fetch OIDC thumbprint for url %q: %v", url, err)
return nil, err
}
thumbprints = []string{fetched}
} else {
if err := iamutil.ValidateThumbprintList(thumbprints, false); err != nil {
return nil, err
}
thumbprints = iamutil.NormalizeThumbprintList(thumbprints)
}
tags, err := iamutil.ParseTags(ctx, iamutil.TagKeysExact)
if err != nil {
return nil, err
}
provider := types.OIDCProvider{
Arn: iamutil.BuildOIDCProviderArn(iamutil.DefaultAccountID, url),
Url: url,
ClientIDList: clientIDs,
ThumbprintList: thumbprints,
CreateDate: time.Now().UTC().Truncate(time.Second),
Tags: tags,
}
stored, err := c.store.CreateOIDCProvider(ctx.Context(), provider)
if err != nil {
debuglogger.Logf("failed to create IAM OIDC provider for url %q: %v", url, err)
return nil, err
}
return &Response{Data: &types.CreateOpenIDConnectProviderResponse{
Result: types.CreateOpenIDConnectProviderResult{
OpenIDConnectProviderArn: stored.Arn,
Tags: stored.Tags,
},
}}, nil
}
func (c IAMApiController) GetOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "GetOpenIDConnectProvider")
if err != nil {
return nil, err
}
provider, err := c.store.GetOIDCProvider(ctx.Context(), arn)
if err != nil {
debuglogger.Logf("failed to get IAM OIDC provider %q: %v", arn, err)
return nil, err
}
return &Response{Data: &types.GetOpenIDConnectProviderResponse{
Result: types.GetOpenIDConnectProviderResult{
Url: provider.Url,
ClientIDList: provider.ClientIDList,
ThumbprintList: provider.ThumbprintList,
CreateDate: provider.CreateDate,
Tags: provider.Tags,
},
}}, nil
}
func (c IAMApiController) ListOpenIDConnectProviders(ctx fiber.Ctx) (*Response, error) {
out, err := c.store.ListOIDCProviders(ctx.Context())
if err != nil {
debuglogger.Logf("failed to list IAM OIDC providers: %v", err)
return nil, err
}
return &Response{Data: &types.ListOpenIDConnectProvidersResponse{
Result: types.ListOpenIDConnectProvidersResult{
OpenIDConnectProviderList: types.OpenIDConnectProviderList{Members: out.Providers},
},
}}, nil
}
func (c IAMApiController) DeleteOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "DeleteOpenIDConnectProvider")
if err != nil {
return nil, err
}
if err := c.store.DeleteOIDCProvider(ctx.Context(), arn); err != nil {
debuglogger.Logf("failed to delete IAM OIDC provider %q: %v", arn, err)
return nil, err
}
return &Response{Data: &types.DeleteOpenIDConnectProviderResponse{}}, nil
}
func (c IAMApiController) AddClientIDToOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "AddClientIDToOpenIDConnectProvider")
if err != nil {
return nil, err
}
clientID, ok := iamutil.RequestParam(ctx, "ClientID")
if !ok || clientID == "" {
debuglogger.Logf("missing required AddClientIDToOpenIDConnectProvider parameter: ClientID")
return nil, iamerr.MissingValue("clientID")
}
if len(clientID) > iamutil.MaxOIDCClientIDLen {
return nil, iamerr.ValueTooLong("clientID", iamutil.MaxOIDCClientIDLen)
}
if err := c.store.AddClientIDToOIDCProvider(ctx.Context(), arn, clientID); err != nil {
debuglogger.Logf("failed to add client id %q to IAM OIDC provider %q: %v", clientID, arn, err)
return nil, err
}
return &Response{Data: &types.AddClientIDToOpenIDConnectProviderResponse{}}, nil
}
func (c IAMApiController) RemoveClientIDFromOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "RemoveClientIDFromOpenIDConnectProvider")
if err != nil {
return nil, err
}
clientID, ok := iamutil.RequestParam(ctx, "ClientID")
if !ok || clientID == "" {
debuglogger.Logf("missing required RemoveClientIDFromOpenIDConnectProvider parameter: ClientID")
return nil, iamerr.MissingValue("clientID")
}
if len(clientID) > iamutil.MaxOIDCClientIDLen {
return nil, iamerr.ValueTooLong("clientID", iamutil.MaxOIDCClientIDLen)
}
if err := c.store.RemoveClientIDFromOIDCProvider(ctx.Context(), arn, clientID); err != nil {
debuglogger.Logf("failed to remove client id %q from IAM OIDC provider %q: %v", clientID, arn, err)
return nil, err
}
return &Response{Data: &types.RemoveClientIDFromOpenIDConnectProviderResponse{}}, nil
}
func (c IAMApiController) UpdateOpenIDConnectProviderThumbprint(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "UpdateOpenIDConnectProviderThumbprint")
if err != nil {
return nil, err
}
thumbprints := iamutil.ParseStringList(ctx, "ThumbprintList")
if err := iamutil.ValidateThumbprintList(thumbprints, true); err != nil {
return nil, err
}
thumbprints = iamutil.NormalizeThumbprintList(thumbprints)
if err := c.store.UpdateOIDCProviderThumbprint(ctx.Context(), arn, thumbprints); err != nil {
debuglogger.Logf("failed to update IAM OIDC provider thumbprint for %q: %v", arn, err)
return nil, err
}
return &Response{Data: &types.UpdateOpenIDConnectProviderThumbprintResponse{}}, nil
}
// TagOpenIDConnectProvider adds or replaces tags on an existing OIDC
// provider. Unlike the user and role tag actions, provider tag keys are
// compared exactly, so "env" and "ENV" are two distinct tags.
func (c IAMApiController) TagOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "TagOpenIDConnectProvider")
if err != nil {
return nil, err
}
tags, err := iamutil.ParseTags(ctx, iamutil.TagKeysExact)
if err != nil {
return nil, err
}
if len(tags) == 0 {
debuglogger.Logf("missing required TagOpenIDConnectProvider parameter: Tags")
return nil, iamerr.MissingValue("tags")
}
if err := c.store.TagOIDCProvider(ctx.Context(), arn, tags); err != nil {
debuglogger.Logf("failed to tag IAM OIDC provider %q: %v", arn, err)
return nil, err
}
return &Response{Data: &types.TagOpenIDConnectProviderResponse{}}, nil
}
// UntagOpenIDConnectProvider removes the named tags from an existing OIDC
// provider. Removal is idempotent: a key naming no current tag is not an
// error.
func (c IAMApiController) UntagOpenIDConnectProvider(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "UntagOpenIDConnectProvider")
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 UntagOpenIDConnectProvider parameter: TagKeys")
return nil, iamerr.MissingValue("tagKeys")
}
if err := c.store.UntagOIDCProvider(ctx.Context(), arn, tagKeys); err != nil {
debuglogger.Logf("failed to untag IAM OIDC provider %q: %v", arn, err)
return nil, err
}
return &Response{Data: &types.UntagOpenIDConnectProviderResponse{}}, nil
}
func (c IAMApiController) ListOpenIDConnectProviderTags(ctx fiber.Ctx) (*Response, error) {
arn, err := iamutil.GetOIDCProviderArn(ctx, "ListOpenIDConnectProviderTags")
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListOpenIDConnectProviderTags")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListOIDCProviderTags(ctx.Context(), storage.ListOIDCProviderTagsInput{
Arn: arn,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM OIDC provider %q tags: %v", arn, err)
return nil, err
}
return &Response{Data: &types.ListOpenIDConnectProviderTagsResponse{
Result: types.ListOpenIDConnectProviderTagsResult{
Tags: types.Tags{Members: out.Tags},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) AssumeRoleWithWebIdentity(ctx fiber.Ctx) (*Response, error) {
rawRoleArn, ok := iamutil.RequestParam(ctx, "RoleArn")
if !ok || rawRoleArn == "" {
debuglogger.Logf("missing required AssumeRoleWithWebIdentity parameter: RoleArn")
return nil, iamerr.MissingValue("roleArn")
}
if err := iamutil.ValidateRoleArnLength(rawRoleArn); err != nil {
return nil, err
}
roleSessionName, ok := iamutil.RequestParam(ctx, "RoleSessionName")
if !ok || roleSessionName == "" {
debuglogger.Logf("missing required AssumeRoleWithWebIdentity parameter: RoleSessionName")
return nil, iamerr.MissingValue("roleSessionName")
}
if err := iamutil.ValidateRoleSessionName(roleSessionName); err != nil {
return nil, err
}
webIdentityToken, ok := iamutil.RequestParam(ctx, "WebIdentityToken")
if !ok || webIdentityToken == "" {
debuglogger.Logf("missing required AssumeRoleWithWebIdentity parameter: WebIdentityToken")
return nil, iamerr.MissingValue("webIdentityToken")
}
if err := iamutil.ValidateWebIdentityTokenLength(webIdentityToken); err != nil {
return nil, err
}
// PolicyArns (managed session policies) and ProviderId (legacy Login
// with Amazon support) are valid AssumeRoleWithWebIdentity parameters
// this implementation doesn't enforce. Rejecting them outright, rather
// than silently accepting and ignoring them
if iamutil.HasRequestParamPrefix(ctx, "PolicyArns.member.") {
debuglogger.Logf("AssumeRoleWithWebIdentity: PolicyArns is not supported")
return nil, iamerr.UnsupportedParameter("PolicyArns")
}
if providerID, ok := iamutil.RequestParam(ctx, "ProviderId"); ok && providerID != "" {
debuglogger.Logf("AssumeRoleWithWebIdentity: ProviderId is not supported")
return nil, iamerr.UnsupportedParameter("ProviderId")
}
durationSeconds, err := iamutil.ParseDurationSeconds(ctx)
if err != nil {
return nil, err
}
// sessionPolicy is an optional additional permissions filter on top of
// the assumed role's own policies (Effective permissions = Role
// identity-based permissions ∩ Session policy permissions, enforced by
// iammiddleware.VerifyIAMPolicy); it uses identity-policy grammar, not
// trust-policy grammar, same as PutUserPolicy/PutRolePolicy.
sessionPolicy, ok := iamutil.RequestParam(ctx, "Policy")
if ok && sessionPolicy != "" {
if len(sessionPolicy) > policy.MaxSessionPolicyBytes {
return nil, iamerr.ValueTooLong("policy", policy.MaxSessionPolicyBytes)
}
if err := policy.Validate("policy", sessionPolicy); err != nil {
return nil, err
}
if err := policy.Parse(sessionPolicy); err != nil {
return nil, err
}
}
// Structural JWT parsing happens before the role is even looked up —
// a malformed token is rejected the same way regardless of whether
// RoleArn names a real role.
claims, err := iamutil.ParseWebIdentityClaims(webIdentityToken)
if err != nil {
return nil, err
}
roleName, ok := iamutil.RoleNameFromAssumeArn(rawRoleArn, iamutil.DefaultAccountID)
if !ok {
debuglogger.Logf("AssumeRoleWithWebIdentity: RoleArn is not a role in this account: %q", rawRoleArn)
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
}
role, err := c.store.GetRole(ctx.Context(), roleName)
if err != nil {
debuglogger.Logf("AssumeRoleWithWebIdentity: role %q not found: %v", roleName, err)
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
}
// RoleNameFromAssumeArn only extracted the final path segment; confirm
// the full ARN the caller supplied — path included — actually matches
// this role's own Arn. Without this, an ARN naming the right role name
// but a different (or missing) path would still resolve to, and assume,
// this role.
if rawRoleArn != role.Arn {
debuglogger.Logf("AssumeRoleWithWebIdentity: RoleArn %q does not match role %q's actual arn %q", rawRoleArn, roleName, role.Arn)
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
}
if role.MaxSessionDuration > 0 && durationSeconds > role.MaxSessionDuration {
debuglogger.Logf("AssumeRoleWithWebIdentity: requested duration %ds exceeds role %q max session duration %ds", durationSeconds, roleName, role.MaxSessionDuration)
return nil, iamerr.DurationExceedsMaxSessionDuration()
}
issuer, ok := iamutil.WebIdentityIssuer(claims)
if !ok {
debuglogger.Logf("AssumeRoleWithWebIdentity: token has no iss claim")
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
}
audience, originalAudience, err := iamutil.WebIdentityAudience(claims)
if err != nil {
return nil, err
}
subject, _ := claims["sub"].(string)
rawIssuer, _ := claims["iss"].(string)
now := time.Now().UTC().Truncate(time.Second)
wctx := policy.WebIdentityContext{
ProviderURL: issuer,
Audience: audience,
OriginalAudience: originalAudience,
Subject: subject,
Claims: iamutil.ExtractClaimContext(claims),
SourceIP: ctx.IP(),
Secure: ctx.Secure(),
Now: now,
RoleSessionName: roleSessionName,
}
lookup := func(federatedArn string) (string, bool) {
provider, err := c.store.GetOIDCProvider(ctx.Context(), federatedArn)
if err != nil {
return "", false
}
return provider.Url, true
}
result, providerArn := policy.EvaluateWebIdentityTrust(role.AssumeRolePolicyDocument, lookup, wctx)
switch result {
case policy.NoPrincipal, policy.ExplicitlyDenied:
debuglogger.Logf("AssumeRoleWithWebIdentity: role %q trust policy does not authorize this request", roleName)
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
case policy.NoIssuerMatch, policy.ConditionFailed:
debuglogger.Logf("AssumeRoleWithWebIdentity: role %q trust policy rejected the token's claims", roleName)
return nil, iamerr.InvalidIdentityTokenClaims()
}
provider, err := c.store.GetOIDCProvider(ctx.Context(), providerArn)
if err != nil {
debuglogger.Logf("AssumeRoleWithWebIdentity: matched provider %q vanished before use: %v", providerArn, err)
return nil, iamerr.AccessDeniedAssumeRoleWithWebIdentity()
}
if len(provider.ClientIDList) == 0 || !slices.Contains(provider.ClientIDList, audience) {
debuglogger.Logf("AssumeRoleWithWebIdentity: audience %q not in provider %q ClientIDList", audience, providerArn)
return nil, iamerr.InvalidIdentityTokenClaims()
}
verifiedClaims, err := iamutil.VerifyWebIdentitySignature(ctx.Context(), webIdentityToken, provider.Url, provider.ThumbprintList)
if err != nil {
return nil, err
}
if err := iamutil.VerifyWebIdentityExpiration(verifiedClaims, now); err != nil {
return nil, err
}
if err := iamutil.VerifyWebIdentityRequiredClaims(verifiedClaims, now); err != nil {
return nil, err
}
accessKeyID, err := iamutil.GenerateTempAccessKeyID()
if err != nil {
return nil, err
}
secretAccessKey, err := iamutil.GenerateSecretAccessKey()
if err != nil {
return nil, err
}
sessionToken, err := iamutil.GenerateSessionToken()
if err != nil {
return nil, err
}
expiration := now.Add(time.Duration(durationSeconds) * time.Second)
session := types.Session{
AccessKeyId: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
RoleArn: role.Arn,
RoleName: role.RoleName,
RoleID: role.RoleID,
RoleSessionName: roleSessionName,
Provider: providerArn,
Audience: audience,
Subject: subject,
CreateDate: now,
Expiration: expiration,
Policy: sessionPolicy,
}
if _, err := c.store.CreateSession(ctx.Context(), session); err != nil {
debuglogger.Logf("failed to store AssumeRoleWithWebIdentity session for access key %q: %v", accessKeyID, err)
return nil, err
}
return &Response{Data: &types.AssumeRoleWithWebIdentityResponse{
Result: types.AssumeRoleWithWebIdentityResult{
Audience: audience,
AssumedRoleUser: types.AssumedRoleUser{
AssumedRoleId: role.RoleID + ":" + roleSessionName,
Arn: iamutil.BuildAssumedRoleArn(iamutil.DefaultAccountID, role.RoleName, roleSessionName),
},
Provider: rawIssuer,
Credentials: types.Credentials{
AccessKeyId: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Expiration: expiration,
},
SubjectFromWebIdentityToken: subject,
PackedPolicySize: iamutil.PackedPolicySize(sessionPolicy),
},
}}, nil
}
func (c IAMApiController) GetCallerIdentity(ctx fiber.Ctx) (*Response, error) {
identity, _ := httpctx.ContextKeyCallerIdentity.Get(ctx).(types.Identity)
switch {
case identity.Session != nil:
session := identity.Session
return &Response{Data: &types.GetCallerIdentityResponse{
Result: types.GetCallerIdentityResult{
Arn: iamutil.BuildAssumedRoleArn(iamutil.DefaultAccountID, session.RoleName, session.RoleSessionName),
UserId: session.RoleID + ":" + session.RoleSessionName,
Account: iamutil.DefaultAccountID,
},
}}, nil
case identity.User != nil:
user := identity.User
return &Response{Data: &types.GetCallerIdentityResponse{
Result: types.GetCallerIdentityResult{
Arn: user.Arn,
UserId: user.UserID,
Account: iamutil.DefaultAccountID,
},
}}, nil
default:
return &Response{Data: &types.GetCallerIdentityResponse{
Result: types.GetCallerIdentityResult{
Arn: fmt.Sprintf("arn:aws:iam::%s:root", iamutil.DefaultAccountID),
UserId: iamutil.DefaultAccountID,
Account: iamutil.DefaultAccountID,
},
}}, nil
}
}