// 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) }