mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 16:04:15 +00:00
`AssumeRoleWithWebIdentity` always fetched a provider's discovery document from `<provider url>/.well-known/openid-configuration`, so an identity provider that issues tokens naming a public issuer while serving its metadata and keys on a cluster-internal path could not be used: reaching it meant relaxing the endpoint checks for every registered provider. `--oidc-discovery-url` moves that one fetch to an operator-named endpoint, which is how keys can be looked up over an optimized private path while the tokens themselves stay verifiable from the public internet against the issuer alone, as the JWT spec requires. The flag takes `<provider url>=<discovery url>` pairs, can be repeated once per provider, and is also read from `VGW_IAM_OIDC_DISCOVERY_URLS` as a comma-separated list; the Helm chart exposes the same list as `iamServer.oidc.discoveryUrls`. The discovery URL is fetched exactly as written, so it must carry the `/.well-known/openid-configuration` path when the provider serves it there. A malformed pair is rejected at startup rather than at the first assume-role call. Only the fetch moves. The provider URL is still what a token's `iss` claim is matched against, the fetched document's own `issuer` field must still equal it, and the key set still comes from the `jwks_uri` that document publishes. A configured discovery endpoint is named by the operator at startup rather than by a request, so it and the `jwks_uri` it publishes waive the private-address check for that provider's fetch chain only, without `--oidc-allow-private-endpoints` and its far broader effect on every other provider. Transport rules are unchanged: a plaintext discovery URL still requires `--oidc-allow-insecure-transport`. Thumbprint auto-fetch follows the override and pins the discovery endpoint's certificate chain, since that is the host every later fetch is verified against.
1563 lines
50 KiB
Go
1563 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"
|
|
"strings"
|
|
"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
|
|
// oidc holds the OIDC provider settings; see OIDCConfig. Its zero value
|
|
// is the default AWS-matching posture: auto-fetch enabled, and only
|
|
// verified https endpoints at publicly routable addresses.
|
|
oidc OIDCConfig
|
|
// oidcPolicy is oidc's endpoint relaxations in the form iamutil's URL
|
|
// validation and fetch helpers take, projected once at construction.
|
|
oidcPolicy iamutil.OIDCEndpointPolicy
|
|
}
|
|
|
|
func NewController(store storage.Storer, oidc OIDCConfig) IAMApiController {
|
|
return IAMApiController{
|
|
store: store,
|
|
oidc: oidc,
|
|
oidcPolicy: oidc.endpointPolicy(),
|
|
}
|
|
}
|
|
|
|
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, c.oidcPolicy)
|
|
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 {
|
|
endpoint, _ := c.oidcPolicy.ResolveDiscovery(url)
|
|
switch {
|
|
case !strings.HasPrefix(endpoint, "https://"):
|
|
// A plaintext http endpoint never presents a certificate, so
|
|
// there is nothing to auto-fetch and nothing for a later JWKS
|
|
// fetch to pin against: an empty ThumbprintList is the accurate
|
|
// record of that, not a missing one.
|
|
debuglogger.Logf("CreateOpenIDConnectProvider: %q is reached over plaintext http; storing an empty ThumbprintList", endpoint)
|
|
case c.oidc.ThumbprintAutoFetchDisabled:
|
|
debuglogger.Logf("CreateOpenIDConnectProvider: ThumbprintList omitted and auto-fetch is disabled")
|
|
return nil, iamerr.MissingValue("thumbprintList")
|
|
default:
|
|
fetched, err := iamutil.FetchThumbprint(ctx.Context(), url, c.oidcPolicy)
|
|
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, c.oidcPolicy)
|
|
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
|
|
}
|
|
}
|