mirror of
https://github.com/versity/versitygw.git
synced 2026-09-23 08:24:17 +00:00
`CreateAccessKey`, `UpdateAccessKey`, `DeleteAccessKey` and `ListAccessKeys` required an explicit `UserName`. Real IAM treats it as optional and resolves it from the access key signing the request, so an IAM user managing its own keys had to name itself. They now infer it, matching AWS: only an entirely absent parameter is inferred, while a present-but-empty one stays a `ValidationError`, and the inferred scope is strictly the caller's own user — another user's key id returns `NoSuchEntity` rather than being touched. A caller with no IAM user of its own gets IAM's own `Must specify userName when calling with non-User credentials` `ValidationError`, shared with `GetUser` as `iamerr.MustSpecifyUserName`. That covers assumed-role sessions and also the gateway's root credential, which is configured rather than stored as an IAM user and so owns no access keys the API could manage — real IAM manages the root account's own keys here, which has no equivalent in this gateway. The policy middleware resolves the same four actions through `callerOrNamedUserResource`, so the resource-level check targets the caller's own user ARN when UserName is omitted instead of falling back to no resource at all, which would have denied every request authorized by an own-ARN-scoped grant.
339 lines
11 KiB
Go
339 lines
11 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 integration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
|
"github.com/aws/aws-sdk-go-v2/service/iam"
|
|
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
|
"github.com/versity/versitygw/iamapi/iamerr"
|
|
)
|
|
|
|
// IAMUpdateAccessKey_missing_user_name calls as root, which is a configured
|
|
// credential rather than a stored IAM user and so has no user name to infer
|
|
// — unlike a caller signing with an IAM user's own access key, covered by
|
|
// IAMUpdateAccessKey_infers_caller_user_name.
|
|
func IAMUpdateAccessKey_missing_user_name(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_missing_user_name"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
AccessKeyId: aws.String(genRandString(20)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.MustSpecifyUserName())
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_infers_caller_user_name(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_infers_caller_user_name"
|
|
return iamActionHandler(s, testName, func(root *iam.Client) error {
|
|
caller, cleanup, err := newAccessKeyCaller(root, s, "iam:UpdateAccessKey")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
created, err := createIAMAccessKey(root, &iam.CreateAccessKeyInput{UserName: &caller.userName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accessKeyID := aws.ToString(created.AccessKey.AccessKeyId)
|
|
|
|
if _, err := updateIAMAccessKey(caller.client, &iam.UpdateAccessKeyInput{
|
|
AccessKeyId: &accessKeyID,
|
|
Status: iamtypes.StatusTypeInactive,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
listOut, err := listIAMAccessKeys(root, &iam.ListAccessKeysInput{UserName: &caller.userName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, key := range listOut.AccessKeyMetadata {
|
|
want := iamtypes.StatusTypeActive
|
|
if aws.ToString(key.AccessKeyId) == accessKeyID {
|
|
want = iamtypes.StatusTypeInactive
|
|
}
|
|
if key.Status != want {
|
|
return fmt.Errorf("expected access key %q status %q, instead got %q", aws.ToString(key.AccessKeyId), want, key.Status)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// IAMUpdateAccessKey_inferred_user_name_scoped_to_caller confirms the
|
|
// inferred user name is the caller's own and nothing else: another user's
|
|
// access key is simply not found, rather than being updated.
|
|
func IAMUpdateAccessKey_inferred_user_name_scoped_to_caller(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_inferred_user_name_scoped_to_caller"
|
|
return iamActionHandler(s, testName, func(root *iam.Client) error {
|
|
caller, cleanup, err := newAccessKeyCaller(root, s, "iam:UpdateAccessKey")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
otherName := newIAMUserName()
|
|
if _, err := createIAMUser(root, &iam.CreateUserInput{UserName: &otherName}); err != nil {
|
|
return err
|
|
}
|
|
defer deleteIAMUserAndAccessKeys(root, otherName)
|
|
|
|
otherKey, err := createIAMAccessKey(root, &iam.CreateAccessKeyInput{UserName: &otherName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
otherKeyID := aws.ToString(otherKey.AccessKey.AccessKeyId)
|
|
|
|
_, err = updateIAMAccessKey(caller.client, &iam.UpdateAccessKeyInput{
|
|
AccessKeyId: &otherKeyID,
|
|
Status: iamtypes.StatusTypeInactive,
|
|
})
|
|
if err := checkIAMApiErr(err, iamerr.NoSuchEntityAccessKey(otherKeyID)); err != nil {
|
|
return err
|
|
}
|
|
|
|
listOut, err := listIAMAccessKeys(root, &iam.ListAccessKeysInput{UserName: &otherName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return checkIAMListAccessKeys(listOut.AccessKeyMetadata, otherName,
|
|
map[string]iamtypes.StatusType{otherKeyID: iamtypes.StatusTypeActive})
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_invalid_user_name(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_invalid_user_name"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("invalid/user"),
|
|
AccessKeyId: aws.String(genRandString(20)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.InvalidUserName("userName"))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_long_user_name(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_long_user_name"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String(strings.Repeat("a", 129)),
|
|
AccessKeyId: aws.String(genRandString(20)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.UserNameTooLong("userName", 128))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_missing_access_key_id(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_missing_access_key_id"
|
|
body := []byte(url.Values{
|
|
"Action": {"UpdateAccessKey"},
|
|
"Version": {"2010-05-08"},
|
|
"UserName": {"validusername"},
|
|
"Status": {"Active"},
|
|
}.Encode())
|
|
return authHandler(s, &authConfig{
|
|
testName: testName,
|
|
method: http.MethodPost,
|
|
service: "iam",
|
|
region: iamAuthRegion,
|
|
body: body,
|
|
date: time.Now().UTC(),
|
|
headers: map[string]string{
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
}, func(req *http.Request) error {
|
|
return checkIAMAuthRequest(s, req, iamerr.MissingParameter("AccessKeyId"))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_access_key_id_too_short(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_access_key_id_too_short"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("validusername"),
|
|
AccessKeyId: aws.String(genRandString(15)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.AccessKeyIDTooShort(16))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_access_key_id_too_long(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_access_key_id_too_long"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("validusername"),
|
|
AccessKeyId: aws.String(genRandString(129)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.AccessKeyIDTooLong(128))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_invalid_access_key_id_chars(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_invalid_access_key_id_chars"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("validusername"),
|
|
AccessKeyId: aws.String("invalid-key-id-1234"),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrInvalidAccessKeyIDChars))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_missing_status(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_missing_status"
|
|
body := []byte(url.Values{
|
|
"Action": {"UpdateAccessKey"},
|
|
"Version": {"2010-05-08"},
|
|
"UserName": {"validusername"},
|
|
"AccessKeyId": {genRandString(20)},
|
|
}.Encode())
|
|
return authHandler(s, &authConfig{
|
|
testName: testName,
|
|
method: http.MethodPost,
|
|
service: "iam",
|
|
region: iamAuthRegion,
|
|
body: body,
|
|
date: time.Now().UTC(),
|
|
headers: map[string]string{
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
}, func(req *http.Request) error {
|
|
return checkIAMAuthRequest(s, req, iamerr.MissingParameter("Status"))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_invalid_status(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_invalid_status"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("validusername"),
|
|
AccessKeyId: aws.String(genRandString(20)),
|
|
Status: iamtypes.StatusType("Bogus"),
|
|
})
|
|
return checkIAMApiErr(err, iamerr.InvalidAccessKeyStatus("Bogus"))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_non_existing_user(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_non_existing_user"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
userName := "non-existing-" + genRandString(16)
|
|
_, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: &userName,
|
|
AccessKeyId: aws.String(genRandString(20)),
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
return checkIAMApiErr(err, iamerr.NoSuchEntityUser(userName))
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_non_existing_access_key(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_non_existing_access_key"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
userName := newIAMUserName()
|
|
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
|
return err
|
|
}
|
|
|
|
accessKeyID := genRandString(20)
|
|
_, updateErr := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: &userName,
|
|
AccessKeyId: &accessKeyID,
|
|
Status: iamtypes.StatusTypeActive,
|
|
})
|
|
checkErr := checkIAMApiErr(updateErr, iamerr.NoSuchEntityAccessKey(accessKeyID))
|
|
|
|
deleteErr := deleteIAMUser(client, userName)
|
|
if checkErr != nil {
|
|
return checkErr
|
|
}
|
|
return deleteErr
|
|
})
|
|
}
|
|
|
|
func IAMUpdateAccessKey_success(s *S3Conf) error {
|
|
testName := "IAMUpdateAccessKey_success"
|
|
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
|
userName := newIAMUserName()
|
|
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
|
return err
|
|
}
|
|
|
|
checkErr := func() error {
|
|
created, err := createIAMAccessKey(client, &iam.CreateAccessKeyInput{UserName: &userName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accessKeyID := aws.ToString(created.AccessKey.AccessKeyId)
|
|
|
|
out, err := updateIAMAccessKey(client, &iam.UpdateAccessKeyInput{
|
|
UserName: &userName,
|
|
AccessKeyId: &accessKeyID,
|
|
Status: iamtypes.StatusTypeInactive,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out == nil {
|
|
return fmt.Errorf("expected UpdateAccessKey output")
|
|
}
|
|
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
|
return fmt.Errorf("expected UpdateAccessKey response request id")
|
|
}
|
|
|
|
listOut, err := listIAMAccessKeys(client, &iam.ListAccessKeysInput{UserName: &userName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(listOut.AccessKeyMetadata) != 1 {
|
|
return fmt.Errorf("expected 1 access key, instead got %d", len(listOut.AccessKeyMetadata))
|
|
}
|
|
if listOut.AccessKeyMetadata[0].Status != iamtypes.StatusTypeInactive {
|
|
return fmt.Errorf("expected access key status to be %q, instead got %q", iamtypes.StatusTypeInactive, listOut.AccessKeyMetadata[0].Status)
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
|
|
deleteErr := deleteIAMUserAndAccessKeys(client, userName)
|
|
if checkErr != nil {
|
|
return checkErr
|
|
}
|
|
return deleteErr
|
|
})
|
|
}
|
|
|
|
func updateIAMAccessKey(client *iam.Client, input *iam.UpdateAccessKeyInput) (*iam.UpdateAccessKeyOutput, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
defer cancel()
|
|
return client.UpdateAccessKey(ctx, input)
|
|
}
|