mirror of
https://github.com/versity/versitygw.git
synced 2026-08-29 04:06:56 +00:00
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.
1551 lines
50 KiB
Go
1551 lines
50 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.ValidationError("Must specify userName when calling with non-User credentials")
|
|
}
|
|
}
|
|
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.GetUserName(ctx, "CreateAccessKey", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
|
|
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.GetUserName(ctx, "UpdateAccessKey", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
|
|
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.GetUserName(ctx, "DeleteAccessKey", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
|
|
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.GetUserName(ctx, "ListAccessKeys", iamutil.MaxUserLookupLen, iamerr.MissingParameter("UserName"))
|
|
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
|
|
}
|
|
}
|