mirror of
https://github.com/versity/versitygw.git
synced 2026-09-23 16:34:18 +00:00
feat: add IAM Role CRUD
Adds `CreateRole`, `GetRole`, `ListRoles`, `DeleteRole`, and `UpdateAssumeRolePolicy` to the standalone IAM service, following the same controller/storage patterns established for users. Both the internal filesystem/S3-backed store and the Vault-backed store implement the new `Storer` methods, with role-specific indexing and lookup helpers mirroring the existing user ones. Role creation requires a trust policy, passed as `AssumeRolePolicyDocument`. A trust policy is a distinct kind of IAM policy document that governs who (or what) is allowed to assume a role, rather than what actions the role itself is permitted to perform. Its grammar is effectively the inverse of an identity policy: `Principal` is required, `Action`/`NotAction` values must carry the `sts:` prefix, and `Resource`/`NotResource` are forbidden. This is implemented in `iamapi/policy/trust.go` as a new validation path alongside the existing identity-policy validation, and is reused by `UpdateAssumeRolePolicy` when replacing a role's trust policy. Also fixes user name uniqueness enforcement to be case-insensitive, matching AWS IAM behavior, and applies the same case-insensitive handling to role names. The internal store now maintains lowercase name indexes for both users and roles, and the Vault store resolves the canonical stored key via a case-insensitive list-and-compare fallback since Vault's KV paths are case-sensitive.
This commit is contained in:
@@ -1128,6 +1128,7 @@ func TestIAMQueryAuth(ts *TestState) {
|
||||
|
||||
func TestIAMCreateUser(ts *TestState) {
|
||||
ts.Run(IAMCreateUser_user_already_exists)
|
||||
ts.Run(IAMCreateUser_already_exists_case_insensitive)
|
||||
ts.Run(IAMCreateUser_invalid_user_name)
|
||||
ts.Run(IAMCreateUser_long_user_name)
|
||||
ts.Run(IAMCreateUser_missing_user_name)
|
||||
@@ -1281,6 +1282,68 @@ func TestIAMListUserPolicies(ts *TestState) {
|
||||
ts.Run(IAMListUserPolicies_pagination)
|
||||
}
|
||||
|
||||
func TestIAMCreateRole(ts *TestState) {
|
||||
ts.Run(IAMCreateRole_missing_role_name)
|
||||
ts.Run(IAMCreateRole_invalid_role_name)
|
||||
ts.Run(IAMCreateRole_long_role_name)
|
||||
ts.Run(IAMCreateRole_already_exists)
|
||||
ts.Run(IAMCreateRole_already_exists_case_insensitive)
|
||||
ts.Run(IAMCreateRole_invalid_path)
|
||||
ts.Run(IAMCreateRole_long_path)
|
||||
ts.Run(IAMCreateRole_missing_assume_role_policy_document)
|
||||
ts.Run(IAMCreateRole_non_ascii_assume_role_policy_document)
|
||||
ts.Run(IAMCreateRole_trust_policy_size_limit_exceeded)
|
||||
ts.Run(IAMCreateRole_description_invalid_charset)
|
||||
ts.Run(IAMCreateRole_description_too_long)
|
||||
ts.Run(IAMCreateRole_max_session_duration_invalid_format)
|
||||
ts.Run(IAMCreateRole_max_session_duration_too_low)
|
||||
ts.Run(IAMCreateRole_max_session_duration_too_high)
|
||||
ts.Run(IAMCreateRole_duplicate_tag_keys)
|
||||
ts.Run(IAMCreateRole_success)
|
||||
ts.Run(IAMCreateRole_defaults)
|
||||
ts.Run(IAMCreateRole_trust_policy_document_grammar)
|
||||
}
|
||||
|
||||
func TestIAMGetRole(ts *TestState) {
|
||||
ts.Run(IAMGetRole_missing_role_name)
|
||||
ts.Run(IAMGetRole_invalid_role_name)
|
||||
ts.Run(IAMGetRole_long_role_name)
|
||||
ts.Run(IAMGetRole_non_existing_role)
|
||||
ts.Run(IAMGetRole_success)
|
||||
}
|
||||
|
||||
func TestIAMListRoles(ts *TestState) {
|
||||
ts.Run(IAMListRoles_invalid_path_prefix)
|
||||
ts.Run(IAMListRoles_long_path_prefix)
|
||||
ts.Run(IAMListRoles_invalid_max_items)
|
||||
ts.Run(IAMListRoles_invalid_max_items_format)
|
||||
ts.Run(IAMListRoles_empty_result)
|
||||
ts.Run(IAMListRoles_success)
|
||||
ts.Run(IAMListRoles_path_prefix)
|
||||
ts.Run(IAMListRoles_pagination)
|
||||
ts.Run(IAMListRoles_path_prefix_pagination)
|
||||
}
|
||||
|
||||
func TestIAMDeleteRole(ts *TestState) {
|
||||
ts.Run(IAMDeleteRole_missing_role_name)
|
||||
ts.Run(IAMDeleteRole_invalid_role_name)
|
||||
ts.Run(IAMDeleteRole_long_role_name)
|
||||
ts.Run(IAMDeleteRole_non_existing_role)
|
||||
ts.Run(IAMDeleteRole_success)
|
||||
}
|
||||
|
||||
func TestIAMUpdateAssumeRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_missing_role_name)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_missing_policy_document)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_invalid_role_name)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_long_role_name)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_non_existing_role)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_non_ascii_policy_document)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_success)
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_trust_policy_document_grammar)
|
||||
}
|
||||
|
||||
func TestIAM(ts *TestState) {
|
||||
TestIAMAuth(ts)
|
||||
TestIAMQueryAuth(ts)
|
||||
@@ -1298,6 +1361,11 @@ func TestIAM(ts *TestState) {
|
||||
TestIAMGetUserPolicy(ts)
|
||||
TestIAMDeleteUserPolicy(ts)
|
||||
TestIAMListUserPolicies(ts)
|
||||
TestIAMCreateRole(ts)
|
||||
TestIAMGetRole(ts)
|
||||
TestIAMListRoles(ts)
|
||||
TestIAMDeleteRole(ts)
|
||||
TestIAMUpdateAssumeRolePolicy(ts)
|
||||
}
|
||||
|
||||
func TestAccessControl(ts *TestState) {
|
||||
@@ -1653,6 +1721,7 @@ func GetIntTests() IntTests {
|
||||
"IAMQueryAuth_invalid_sha256_payload_hash_ignored": IAMQueryAuth_invalid_sha256_payload_hash_ignored,
|
||||
"IAMQueryAuth_with_expect_header": IAMQueryAuth_with_expect_header,
|
||||
"IAMCreateUser_user_already_exists": IAMCreateUser_user_already_exists,
|
||||
"IAMCreateUser_already_exists_case_insensitive": IAMCreateUser_already_exists_case_insensitive,
|
||||
"IAMCreateUser_invalid_user_name": IAMCreateUser_invalid_user_name,
|
||||
"IAMCreateUser_long_user_name": IAMCreateUser_long_user_name,
|
||||
"IAMCreateUser_missing_user_name": IAMCreateUser_missing_user_name,
|
||||
@@ -1765,6 +1834,53 @@ func GetIntTests() IntTests {
|
||||
"IAMListUserPolicies_empty_result": IAMListUserPolicies_empty_result,
|
||||
"IAMListUserPolicies_success": IAMListUserPolicies_success,
|
||||
"IAMListUserPolicies_pagination": IAMListUserPolicies_pagination,
|
||||
"IAMCreateRole_missing_role_name": IAMCreateRole_missing_role_name,
|
||||
"IAMCreateRole_invalid_role_name": IAMCreateRole_invalid_role_name,
|
||||
"IAMCreateRole_long_role_name": IAMCreateRole_long_role_name,
|
||||
"IAMCreateRole_already_exists": IAMCreateRole_already_exists,
|
||||
"IAMCreateRole_already_exists_case_insensitive": IAMCreateRole_already_exists_case_insensitive,
|
||||
"IAMCreateRole_invalid_path": IAMCreateRole_invalid_path,
|
||||
"IAMCreateRole_long_path": IAMCreateRole_long_path,
|
||||
"IAMCreateRole_missing_assume_role_policy_document": IAMCreateRole_missing_assume_role_policy_document,
|
||||
"IAMCreateRole_non_ascii_assume_role_policy_document": IAMCreateRole_non_ascii_assume_role_policy_document,
|
||||
"IAMCreateRole_trust_policy_size_limit_exceeded": IAMCreateRole_trust_policy_size_limit_exceeded,
|
||||
"IAMCreateRole_description_invalid_charset": IAMCreateRole_description_invalid_charset,
|
||||
"IAMCreateRole_description_too_long": IAMCreateRole_description_too_long,
|
||||
"IAMCreateRole_max_session_duration_invalid_format": IAMCreateRole_max_session_duration_invalid_format,
|
||||
"IAMCreateRole_max_session_duration_too_low": IAMCreateRole_max_session_duration_too_low,
|
||||
"IAMCreateRole_max_session_duration_too_high": IAMCreateRole_max_session_duration_too_high,
|
||||
"IAMCreateRole_duplicate_tag_keys": IAMCreateRole_duplicate_tag_keys,
|
||||
"IAMCreateRole_success": IAMCreateRole_success,
|
||||
"IAMCreateRole_defaults": IAMCreateRole_defaults,
|
||||
"IAMCreateRole_trust_policy_document_grammar": IAMCreateRole_trust_policy_document_grammar,
|
||||
"IAMGetRole_missing_role_name": IAMGetRole_missing_role_name,
|
||||
"IAMGetRole_invalid_role_name": IAMGetRole_invalid_role_name,
|
||||
"IAMGetRole_long_role_name": IAMGetRole_long_role_name,
|
||||
"IAMGetRole_non_existing_role": IAMGetRole_non_existing_role,
|
||||
"IAMGetRole_success": IAMGetRole_success,
|
||||
"IAMListRoles_invalid_path_prefix": IAMListRoles_invalid_path_prefix,
|
||||
"IAMListRoles_long_path_prefix": IAMListRoles_long_path_prefix,
|
||||
"IAMListRoles_invalid_max_items": IAMListRoles_invalid_max_items,
|
||||
"IAMListRoles_invalid_max_items_format": IAMListRoles_invalid_max_items_format,
|
||||
"IAMListRoles_empty_result": IAMListRoles_empty_result,
|
||||
"IAMListRoles_success": IAMListRoles_success,
|
||||
"IAMListRoles_path_prefix": IAMListRoles_path_prefix,
|
||||
"IAMListRoles_pagination": IAMListRoles_pagination,
|
||||
"IAMListRoles_path_prefix_pagination": IAMListRoles_path_prefix_pagination,
|
||||
"IAMDeleteRole_missing_role_name": IAMDeleteRole_missing_role_name,
|
||||
"IAMDeleteRole_invalid_role_name": IAMDeleteRole_invalid_role_name,
|
||||
"IAMDeleteRole_long_role_name": IAMDeleteRole_long_role_name,
|
||||
"IAMDeleteRole_non_existing_role": IAMDeleteRole_non_existing_role,
|
||||
"IAMDeleteRole_success": IAMDeleteRole_success,
|
||||
"IAMUpdateAssumeRolePolicy_missing_role_name": IAMUpdateAssumeRolePolicy_missing_role_name,
|
||||
"IAMUpdateAssumeRolePolicy_missing_policy_document": IAMUpdateAssumeRolePolicy_missing_policy_document,
|
||||
"IAMUpdateAssumeRolePolicy_invalid_role_name": IAMUpdateAssumeRolePolicy_invalid_role_name,
|
||||
"IAMUpdateAssumeRolePolicy_long_role_name": IAMUpdateAssumeRolePolicy_long_role_name,
|
||||
"IAMUpdateAssumeRolePolicy_non_existing_role": IAMUpdateAssumeRolePolicy_non_existing_role,
|
||||
"IAMUpdateAssumeRolePolicy_non_ascii_policy_document": IAMUpdateAssumeRolePolicy_non_ascii_policy_document,
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded": IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded,
|
||||
"IAMUpdateAssumeRolePolicy_success": IAMUpdateAssumeRolePolicy_success,
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_document_grammar": IAMUpdateAssumeRolePolicy_trust_policy_document_grammar,
|
||||
"PresignedAuth_security_token_not_supported": PresignedAuth_security_token_not_supported,
|
||||
"PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm,
|
||||
"PresignedAuth_ECDSA_not_supported": PresignedAuth_ECDSA_not_supported,
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
// 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"
|
||||
"regexp"
|
||||
"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"
|
||||
"github.com/versity/versitygw/iamapi/policy"
|
||||
)
|
||||
|
||||
// validTrustPolicyDocument is a minimal role trust policy accepted by
|
||||
// ParseTrust: any principal may assume the role via sts:AssumeRole.
|
||||
const validTrustPolicyDocument = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`
|
||||
|
||||
var integrationIAMRoleIDPattern = regexp.MustCompile(`^AROA[A-Z2-7]{17}$`)
|
||||
|
||||
func IAMCreateRole_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicyDocument},
|
||||
}.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.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String("invalid/role"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 65)),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_already_exists(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_already_exists"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
checkErr := checkIAMApiErr(err, iamerr.EntityAlreadyExistsRole(roleName))
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_already_exists_case_insensitive(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_already_exists_case_insensitive"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upperName := strings.ToUpper(roleName)
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &upperName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
checkErr := checkIAMApiErr(err, iamerr.EntityAlreadyExistsRole(upperName))
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_invalid_path(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_invalid_path"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Path: aws.String("invalid"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidPath("path"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_long_path(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_long_path"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Path: aws.String("/" + strings.Repeat("a", 511) + "/"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.PathTooLong("path", 512))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_missing_assume_role_policy_document(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_missing_assume_role_policy_document"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
}.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.MissingValue("assumeRolePolicyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_non_ascii_assume_role_policy_document(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_non_ascii_assume_role_policy_document"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String("emoji\U0001F600test"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidCharset("assumeRolePolicyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_trust_policy_size_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_trust_policy_size_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
oversized := `{"Version":"2012-10-17","Statement":[{"Sid":"` + strings.Repeat("x", 2000) + `","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(oversized),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TrustPolicySizeLimitExceeded(policy.MaxTrustPolicyBytes))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_description_invalid_charset(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_description_invalid_charset"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Description: aws.String("emoji\U0001F600test"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidDescriptionCharset("description"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_description_too_long(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_description_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Description: aws.String(strings.Repeat("a", 1001)),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.ValueTooLong("description", 1000))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_max_session_duration_invalid_format(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_max_session_duration_invalid_format"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicyDocument},
|
||||
"MaxSessionDuration": {"not-a-number"},
|
||||
}.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.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_max_session_duration_too_low(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_max_session_duration_too_low"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
MaxSessionDuration: aws.Int32(3599),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.MaxSessionDurationTooLow())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_max_session_duration_too_high(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_max_session_duration_too_high"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
MaxSessionDuration: aws.Int32(43201),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.MaxSessionDurationTooHigh())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_duplicate_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_duplicate_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("key"), Value: aws.String("one")},
|
||||
{Key: aws.String("KEY"), Value: aws.String("two")},
|
||||
},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidInput("Duplicate tag keys found. Please note that Tag keys are case insensitive."))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_success(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
out, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
Path: aws.String("/engineering/"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Description: aws.String("a test role"),
|
||||
MaxSessionDuration: aws.Int32(7200),
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("test")},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkCreateRoleOutput(out, roleName, "/engineering/", "a test role", 7200, validTrustPolicyDocument, true)
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_defaults(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_defaults"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
out, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkCreateRoleOutput(out, roleName, "/", "", 3600, validTrustPolicyDocument, false)
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateRole_trust_policy_document_grammar(s *S3Conf) error {
|
||||
testName := "IAMCreateRole_trust_policy_document_grammar"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, tt := range trustPolicyGrammarCases {
|
||||
if err := checkCreateRoleTrustPolicyCase(client, tt.doc, tt.wantErr); err != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// checkCreateRoleTrustPolicyCase verifies doc is accepted/rejected as
|
||||
// expected when used as a fresh role's AssumeRolePolicyDocument.
|
||||
func checkCreateRoleTrustPolicyCase(client *iam.Client, doc string, wantErr iamerr.APIError) error {
|
||||
roleName := newIAMRoleName()
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(doc),
|
||||
})
|
||||
if wantErr == nil {
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateRole: %w", err)
|
||||
}
|
||||
return deleteIAMRole(client, roleName)
|
||||
}
|
||||
return checkIAMApiErr(err, wantErr)
|
||||
}
|
||||
|
||||
func createIAMRole(client *iam.Client, input *iam.CreateRoleInput) (*iam.CreateRoleOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.CreateRole(ctx, input)
|
||||
}
|
||||
|
||||
func newIAMRoleName() string {
|
||||
return "create-role-" + genRandString(16)
|
||||
}
|
||||
|
||||
// checkCreateRoleOutput verifies the fields of a CreateRoleOutput-shaped role.
|
||||
func checkCreateRoleOutput(out *iam.CreateRoleOutput, roleName, path, description string, maxSessionDuration int32, wantDocument string, expectTags bool) error {
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected CreateRole output role")
|
||||
}
|
||||
requestID, hasRequestID := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata)
|
||||
return checkRoleFields("CreateRole", out.Role, roleName, path, description, maxSessionDuration, wantDocument, expectTags, requestID, hasRequestID)
|
||||
}
|
||||
|
||||
func checkRoleFields(operation string, role *iamtypes.Role, roleName, path, description string, maxSessionDuration int32, wantDocument string, expectTags bool, requestID string, hasRequestID bool) error {
|
||||
if role == nil {
|
||||
return fmt.Errorf("expected %s output role", operation)
|
||||
}
|
||||
if aws.ToString(role.Path) != path {
|
||||
return fmt.Errorf("expected role path to be %q, instead got %q", path, aws.ToString(role.Path))
|
||||
}
|
||||
if aws.ToString(role.RoleName) != roleName {
|
||||
return fmt.Errorf("expected role name to be %q, instead got %q", roleName, aws.ToString(role.RoleName))
|
||||
}
|
||||
expectedARN := "arn:aws:iam::000000000000:role" + path + roleName
|
||||
if aws.ToString(role.Arn) != expectedARN {
|
||||
return fmt.Errorf("expected role ARN to be %q, instead got %q", expectedARN, aws.ToString(role.Arn))
|
||||
}
|
||||
if !integrationIAMRoleIDPattern.MatchString(aws.ToString(role.RoleId)) {
|
||||
return fmt.Errorf("expected AWS IAM role id, instead got %q", aws.ToString(role.RoleId))
|
||||
}
|
||||
if role.CreateDate == nil || role.CreateDate.IsZero() {
|
||||
return fmt.Errorf("expected role create date")
|
||||
}
|
||||
if aws.ToString(role.Description) != description {
|
||||
return fmt.Errorf("expected role description to be %q, instead got %q", description, aws.ToString(role.Description))
|
||||
}
|
||||
if aws.ToInt32(role.MaxSessionDuration) != maxSessionDuration {
|
||||
return fmt.Errorf("expected role max session duration to be %d, instead got %d", maxSessionDuration, aws.ToInt32(role.MaxSessionDuration))
|
||||
}
|
||||
gotDocument, err := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to url-decode assume role policy document %q: %w", aws.ToString(role.AssumeRolePolicyDocument), err)
|
||||
}
|
||||
if gotDocument != wantDocument {
|
||||
return fmt.Errorf("expected assume role policy document %q, instead got %q", wantDocument, gotDocument)
|
||||
}
|
||||
if role.RoleLastUsed == nil {
|
||||
return fmt.Errorf("expected role RoleLastUsed to be non-nil (empty element)")
|
||||
}
|
||||
if expectTags {
|
||||
if len(role.Tags) != 1 || aws.ToString(role.Tags[0].Key) != "env" || aws.ToString(role.Tags[0].Value) != "test" {
|
||||
return fmt.Errorf("expected role tag env=test, instead got %#v", role.Tags)
|
||||
}
|
||||
} else if len(role.Tags) != 0 {
|
||||
return fmt.Errorf("expected no role tags, instead got %#v", role.Tags)
|
||||
}
|
||||
if !hasRequestID || requestID == "" {
|
||||
return fmt.Errorf("expected %s response request id", operation)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -47,6 +47,27 @@ func IAMCreateUser_user_already_exists(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateUser_already_exists_case_insensitive(s *S3Conf) error {
|
||||
testName := "IAMCreateUser_already_exists_case_insensitive"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{
|
||||
UserName: &userName,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upperName := strings.ToUpper(userName)
|
||||
_, err := createIAMUser(client, &iam.CreateUserInput{UserName: &upperName})
|
||||
checkErr := checkIAMApiErr(err, iamerr.EntityAlreadyExistsUser(upperName))
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMCreateUser_invalid_user_name(s *S3Conf) error {
|
||||
testName := "IAMCreateUser_invalid_user_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMDeleteRole_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_missing_role_name"
|
||||
body := []byte("Action=DeleteRole&Version=2010-05-08")
|
||||
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("RoleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
err := deleteIAMRole(client, "invalid/role")
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
err := deleteIAMRole(client, strings.Repeat("a", 129))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
const roleName = "asdfadsf"
|
||||
err := deleteIAMRole(client, roleName)
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_success(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteIAMRole(client, roleName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := getIAMRole(client, roleName)
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func deleteIAMRole(client *iam.Client, roleName string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
_, err := client.DeleteRole(ctx, &iam.DeleteRoleInput{RoleName: &roleName})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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"
|
||||
"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"
|
||||
)
|
||||
|
||||
func IAMGetRole_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMGetRole_missing_role_name"
|
||||
body := []byte("Action=GetRole&Version=2010-05-08")
|
||||
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("RoleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRole_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMGetRole_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := getIAMRole(client, "invalid/role")
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRole_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMGetRole_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := getIAMRole(client, strings.Repeat("a", 129))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRole_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMGetRole_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
const roleName = "asdfadsf"
|
||||
_, err := getIAMRole(client, roleName)
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRole_success(s *S3Conf) error {
|
||||
testName := "IAMGetRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
Path: aws.String("/engineering/"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Description: aws.String("a test role"),
|
||||
MaxSessionDuration: aws.Int32(7200),
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("test")},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := getIAMRole(client, roleName)
|
||||
if err != nil {
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if deleteErr != nil {
|
||||
return fmt.Errorf("get role: %v; delete role: %w", err, deleteErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkGetRoleOutput(out, roleName, "/engineering/", "a test role", 7200, validTrustPolicyDocument, true)
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func getIAMRole(client *iam.Client, roleName string) (*iam.GetRoleOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.GetRole(ctx, &iam.GetRoleInput{RoleName: &roleName})
|
||||
}
|
||||
|
||||
// checkGetRoleOutput verifies the fields of a GetRoleOutput-shaped role.
|
||||
func checkGetRoleOutput(out *iam.GetRoleOutput, roleName, path, description string, maxSessionDuration int32, wantDocument string, expectTags bool) error {
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected GetRole output role")
|
||||
}
|
||||
requestID, hasRequestID := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata)
|
||||
return checkRoleFields("GetRole", out.Role, roleName, path, description, maxSessionDuration, wantDocument, expectTags, requestID, hasRequestID)
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"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"
|
||||
)
|
||||
|
||||
func IAMListRoles_invalid_path_prefix(s *S3Conf) error {
|
||||
testName := "IAMListRoles_invalid_path_prefix"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
expected := iamerr.ValidationError("The specified value for pathPrefix is invalid. It must begin with the / character and contain only alphanumeric characters and/or / characters.")
|
||||
for _, pathPrefix := range []string{"invalid", "/invalid\n"} {
|
||||
_, err := listIAMRoles(client, &iam.ListRolesInput{PathPrefix: aws.String(pathPrefix)})
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("PathPrefix %q: %w", pathPrefix, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_long_path_prefix(s *S3Conf) error {
|
||||
testName := "IAMListRoles_long_path_prefix"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
pathPrefix := "/" + strings.Repeat("a", 512)
|
||||
_, err := listIAMRoles(client, &iam.ListRolesInput{PathPrefix: &pathPrefix})
|
||||
return checkIAMApiErr(err, iamerr.ValidationError("The specified value for pathPrefix is invalid. It must begin with the / character and contain only alphanumeric characters and/or / characters."))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListRoles_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, maxItems := range []int32{-1, 0, 1001} {
|
||||
_, err := listIAMRoles(client, &iam.ListRolesInput{MaxItems: aws.Int32(maxItems)})
|
||||
expected := iamerr.ValidationError(fmt.Sprintf("1 validation error detected: Value '%d' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000", maxItems))
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_invalid_max_items_format(s *S3Conf) error {
|
||||
testName := "IAMListRoles_invalid_max_items_format"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListRoles"},
|
||||
"Version": {"2010-05-08"},
|
||||
"MaxItems": {"not-a-number"},
|
||||
}.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 {
|
||||
expected := iamerr.ValidationError("1 validation error detected: Value 'not-a-number' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000")
|
||||
return checkIAMAuthRequest(s, req, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_empty_result(s *S3Conf) error {
|
||||
testName := "IAMListRoles_empty_result"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
pathPrefix := "/list-roles-" + genRandString(16) + "/"
|
||||
input := &iam.ListRolesInput{PathPrefix: &pathPrefix}
|
||||
first, err := listIAMRoles(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
second, err := listIAMRoles(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolesOutput(first); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolesOutput(second); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(first.Roles) != 0 || len(second.Roles) != 0 {
|
||||
return fmt.Errorf("expected consistent empty results, instead got %v and %v", iamListRoleNames(first.Roles), iamListRoleNames(second.Roles))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_success(s *S3Conf) error {
|
||||
testName := "IAMListRoles_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
path := "/list-roles-" + genRandString(16) + "/"
|
||||
roles := map[string]string{"list-roles-" + genRandString(16): path}
|
||||
return withIAMListRoles(client, roles, func() error {
|
||||
out, err := listIAMRoles(client, &iam.ListRolesInput{PathPrefix: &path})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolesOutput(out); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMListRoles(out.Roles, roles)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_path_prefix(s *S3Conf) error {
|
||||
testName := "IAMListRoles_path_prefix"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
basePath := "/list-roles-" + genRandString(16) + "/"
|
||||
engineeringPath := basePath + "engineering/"
|
||||
namePrefix := "list-roles-" + genRandString(8)
|
||||
roles := map[string]string{
|
||||
namePrefix + "-root": basePath,
|
||||
namePrefix + "-z": engineeringPath,
|
||||
namePrefix + "-a": engineeringPath + "platform/",
|
||||
namePrefix + "-ops": basePath + "operations/",
|
||||
}
|
||||
expected := map[string]string{
|
||||
namePrefix + "-a": engineeringPath + "platform/",
|
||||
namePrefix + "-z": engineeringPath,
|
||||
}
|
||||
return withIAMListRoles(client, roles, func() error {
|
||||
input := &iam.ListRolesInput{PathPrefix: &engineeringPath}
|
||||
first, err := listIAMRoles(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
second, err := listIAMRoles(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolesOutput(first); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRoles(first.Roles, expected); err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(iamListRoleNames(first.Roles), iamListRoleNames(second.Roles)) {
|
||||
return fmt.Errorf("expected consistent results, instead got %v and %v", iamListRoleNames(first.Roles), iamListRoleNames(second.Roles))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_pagination(s *S3Conf) error {
|
||||
testName := "IAMListRoles_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
path := "/list-roles-" + genRandString(16) + "/"
|
||||
roles := make(map[string]string, 5)
|
||||
for range 5 {
|
||||
roles["list-roles-"+genRandString(16)] = path
|
||||
}
|
||||
return withIAMListRoles(client, roles, func() error {
|
||||
input := iam.ListRolesInput{PathPrefix: &path, MaxItems: aws.Int32(2)}
|
||||
firstPages, err := collectIAMListRolePages(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secondPages, err := collectIAMListRolePages(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolePages(firstPages, []int{2, 2, 1}, roles); err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(iamListRolePageValues(firstPages), iamListRolePageValues(secondPages)) {
|
||||
return fmt.Errorf("expected consistent pagination results")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoles_path_prefix_pagination(s *S3Conf) error {
|
||||
testName := "IAMListRoles_path_prefix_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
basePath := "/list-roles-" + genRandString(16) + "/"
|
||||
matchingPath := basePath + "engineering/"
|
||||
namePrefix := "list-roles-" + genRandString(8)
|
||||
roles := map[string]string{
|
||||
namePrefix + "-outside": basePath,
|
||||
namePrefix + "-e": matchingPath,
|
||||
namePrefix + "-d": matchingPath,
|
||||
namePrefix + "-c": matchingPath + "platform/",
|
||||
namePrefix + "-b": matchingPath + "storage/",
|
||||
namePrefix + "-a": matchingPath + "storage/archive/",
|
||||
namePrefix + "-ops": basePath + "operations/",
|
||||
}
|
||||
expected := map[string]string{
|
||||
namePrefix + "-a": matchingPath + "storage/archive/",
|
||||
namePrefix + "-b": matchingPath + "storage/",
|
||||
namePrefix + "-c": matchingPath + "platform/",
|
||||
namePrefix + "-d": matchingPath,
|
||||
namePrefix + "-e": matchingPath,
|
||||
}
|
||||
return withIAMListRoles(client, roles, func() error {
|
||||
input := iam.ListRolesInput{PathPrefix: &matchingPath, MaxItems: aws.Int32(2)}
|
||||
firstPages, err := collectIAMListRolePages(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secondPages, err := collectIAMListRolePages(client, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMListRolePages(firstPages, []int{2, 2, 1}, expected); err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(iamListRolePageValues(firstPages), iamListRolePageValues(secondPages)) {
|
||||
return fmt.Errorf("expected consistent filtered pagination results")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func listIAMRoles(client *iam.Client, input *iam.ListRolesInput) (*iam.ListRolesOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.ListRoles(ctx, input)
|
||||
}
|
||||
|
||||
func withIAMListRoles(client *iam.Client, roles map[string]string, test func() error) (err error) {
|
||||
created := make([]string, 0, len(roles))
|
||||
defer func() {
|
||||
for _, name := range created {
|
||||
if deleteErr := deleteIAMRole(client, name); deleteErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("delete IAM role %q: %w", name, deleteErr))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for name, path := range roles {
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &name,
|
||||
Path: &path,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
created = append(created, name)
|
||||
}
|
||||
return test()
|
||||
}
|
||||
|
||||
func collectIAMListRolePages(client *iam.Client, input iam.ListRolesInput) ([]*iam.ListRolesOutput, error) {
|
||||
var pages []*iam.ListRolesOutput
|
||||
for {
|
||||
out, err := listIAMRoles(client, &input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkIAMListRolesOutput(out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages = append(pages, out)
|
||||
if !out.IsTruncated {
|
||||
return pages, nil
|
||||
}
|
||||
input.Marker = out.Marker
|
||||
}
|
||||
}
|
||||
|
||||
func checkIAMListRolesOutput(out *iam.ListRolesOutput) error {
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected ListRoles output")
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected ListRoles response request id")
|
||||
}
|
||||
if out.IsTruncated != (out.Marker != nil && aws.ToString(out.Marker) != "") {
|
||||
return fmt.Errorf("expected marker only when ListRoles output is truncated")
|
||||
}
|
||||
for _, role := range out.Roles {
|
||||
if aws.ToString(role.Path) == "" || aws.ToString(role.RoleName) == "" || aws.ToString(role.RoleId) == "" || aws.ToString(role.Arn) == "" || role.CreateDate == nil || role.CreateDate.IsZero() {
|
||||
return fmt.Errorf("expected all required fields for listed role, instead got %#v", role)
|
||||
}
|
||||
if !integrationIAMRoleIDPattern.MatchString(aws.ToString(role.RoleId)) {
|
||||
return fmt.Errorf("expected AWS IAM role id, instead got %q", aws.ToString(role.RoleId))
|
||||
}
|
||||
if role.RoleLastUsed != nil {
|
||||
return fmt.Errorf("expected ListRoles RoleLastUsed to be nil (list/get asymmetry), instead got %#v", role.RoleLastUsed)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIAMListRoles(roles []iamtypes.Role, expected map[string]string) error {
|
||||
if len(roles) != len(expected) {
|
||||
return fmt.Errorf("expected %d roles, instead got %d: %v", len(expected), len(roles), iamListRoleNames(roles))
|
||||
}
|
||||
names := iamListRoleNames(roles)
|
||||
if !sort.StringsAreSorted(names) {
|
||||
return fmt.Errorf("expected roles sorted by role name, instead got %v", names)
|
||||
}
|
||||
for _, role := range roles {
|
||||
name := aws.ToString(role.RoleName)
|
||||
path, ok := expected[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected listed role %q", name)
|
||||
}
|
||||
if aws.ToString(role.Path) != path {
|
||||
return fmt.Errorf("expected role %q path %q, instead got %q", name, path, aws.ToString(role.Path))
|
||||
}
|
||||
if want := "arn:aws:iam::000000000000:role" + path + name; aws.ToString(role.Arn) != want {
|
||||
return fmt.Errorf("expected role %q ARN %q, instead got %q", name, want, aws.ToString(role.Arn))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIAMListRolePages(pages []*iam.ListRolesOutput, sizes []int, expected map[string]string) error {
|
||||
if len(pages) != len(sizes) {
|
||||
return fmt.Errorf("expected %d pages, instead got %d", len(sizes), len(pages))
|
||||
}
|
||||
var roles []iamtypes.Role
|
||||
for i, page := range pages {
|
||||
if len(page.Roles) != sizes[i] {
|
||||
return fmt.Errorf("expected page %d to contain %d roles, instead got %d", i+1, sizes[i], len(page.Roles))
|
||||
}
|
||||
if page.IsTruncated != (i < len(pages)-1) {
|
||||
return fmt.Errorf("unexpected IsTruncated value on page %d", i+1)
|
||||
}
|
||||
roles = append(roles, page.Roles...)
|
||||
}
|
||||
return checkIAMListRoles(roles, expected)
|
||||
}
|
||||
|
||||
func iamListRolePageValues(pages []*iam.ListRolesOutput) [][]string {
|
||||
values := make([][]string, len(pages))
|
||||
for i, page := range pages {
|
||||
values[i] = append([]string{fmt.Sprint(page.IsTruncated), aws.ToString(page.Marker)}, iamListRoleNames(page.Roles)...)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func iamListRoleNames(roles []iamtypes.Role) []string {
|
||||
names := make([]string, len(roles))
|
||||
for i, role := range roles {
|
||||
names[i] = aws.ToString(role.RoleName)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/policy"
|
||||
)
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UpdateAssumeRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"PolicyDocument": {validTrustPolicyDocument},
|
||||
}.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.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_missing_policy_document(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_missing_policy_document"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UpdateAssumeRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
}.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.MissingValue("policyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: aws.String("invalid/role"),
|
||||
PolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 129)),
|
||||
PolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
const roleName = "asdfadsf"
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: aws.String(roleName),
|
||||
PolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_non_ascii_policy_document(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_non_ascii_policy_document"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: aws.String("asdfadsf"),
|
||||
PolicyDocument: aws.String("emoji\U0001F600test"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidCharset("policyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
oversized := `{"Version":"2012-10-17","Statement":[{"Sid":"` + strings.Repeat("x", 2000) + `","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyDocument: aws.String(oversized),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TrustPolicySizeLimitExceeded(policy.MaxTrustPolicyBytes))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_success(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
created, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
const updatedDocument = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sts.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
|
||||
out, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyDocument: aws.String(updatedDocument),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected UpdateAssumeRolePolicy output")
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected UpdateAssumeRolePolicy response request id")
|
||||
}
|
||||
|
||||
got, err := getIAMRole(client, roleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got == nil || got.Role == nil || created == nil || created.Role == nil {
|
||||
return fmt.Errorf("expected created and updated roles")
|
||||
}
|
||||
gotDocument, err := url.QueryUnescape(aws.ToString(got.Role.AssumeRolePolicyDocument))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to url-decode assume role policy document %q: %w", aws.ToString(got.Role.AssumeRolePolicyDocument), err)
|
||||
}
|
||||
if gotDocument != updatedDocument {
|
||||
return fmt.Errorf("expected updated assume role policy document %q, instead got %q", updatedDocument, gotDocument)
|
||||
}
|
||||
if aws.ToString(got.Role.RoleId) != aws.ToString(created.Role.RoleId) {
|
||||
return fmt.Errorf("expected UpdateAssumeRolePolicy to preserve role id, want %q, instead got %q", aws.ToString(created.Role.RoleId), aws.ToString(got.Role.RoleId))
|
||||
}
|
||||
if got.Role.CreateDate == nil || created.Role.CreateDate == nil || !got.Role.CreateDate.Equal(*created.Role.CreateDate) {
|
||||
return fmt.Errorf("expected UpdateAssumeRolePolicy to preserve role create date")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func updateIAMAssumeRolePolicy(client *iam.Client, input *iam.UpdateAssumeRolePolicyInput) (*iam.UpdateAssumeRolePolicyOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.UpdateAssumeRolePolicy(ctx, input)
|
||||
}
|
||||
|
||||
func IAMUpdateAssumeRolePolicy_trust_policy_document_grammar(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_trust_policy_document_grammar"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, tt := range trustPolicyGrammarCases {
|
||||
if err := checkUpdateAssumeRolePolicyTrustPolicyCase(client, tt.doc, tt.wantErr); err != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// checkUpdateAssumeRolePolicyTrustPolicyCase verifies doc is accepted/rejected
|
||||
// as expected when used to update an existing role's trust policy.
|
||||
func checkUpdateAssumeRolePolicyTrustPolicyCase(client *iam.Client, doc string, wantErr iamerr.APIError) (err error) {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create base role: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if deleteErr := deleteIAMRole(client, roleName); deleteErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("cleanup: %w", deleteErr))
|
||||
}
|
||||
}()
|
||||
|
||||
_, updateErr := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyDocument: aws.String(doc),
|
||||
})
|
||||
if wantErr == nil {
|
||||
if updateErr != nil {
|
||||
return fmt.Errorf("UpdateAssumeRolePolicy: %w", updateErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return checkIAMApiErr(updateErr, wantErr)
|
||||
}
|
||||
@@ -934,6 +934,60 @@ func checkIAMApiErr(err error, expected iamerr.APIError) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type trustPolicyGrammarCase struct {
|
||||
name string
|
||||
doc string
|
||||
wantErr iamerr.APIError // nil means the document must be accepted
|
||||
}
|
||||
|
||||
// trustPolicyGrammarCases covers the role trust-policy grammar
|
||||
var trustPolicyGrammarCases = []trustPolicyGrammarCase{
|
||||
{"valid AWS principal", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:root"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid without version", `{"Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid Service principal", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid multiple principal type keys together", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*","Service":"sts.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid Federated non-cognito provider (looks suspicious, is valid)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"bogus.example.com"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid non-AssumeRole sts action", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:TagSession"}]}`, nil},
|
||||
{"valid NotAction with sts prefix", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":{"AWS":"*"},"NotAction":"sts:AssumeRole"}]}`, nil},
|
||||
{"valid action array all sts prefixed", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":["sts:AssumeRole","sts:TagSession"]}]}`, nil},
|
||||
{"valid multiple unique sids", `{"Version":"2012-10-17","Statement":[{"Sid":"A","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"},{"Sid":"B","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, nil},
|
||||
{"cognito federated with condition", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"cognito-identity.amazonaws.com"},"Action":"sts:AssumeRole","Condition":{"StringEquals":{"cognito-identity.amazonaws.com:aud":"us-east-1:abc"}}}]}`, nil},
|
||||
{"unrelated condition block ignored (looks suspicious, is valid)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole","Condition":{"StringEquals":{"aws:SourceAccount":"123456789012"}}}]}`, nil},
|
||||
|
||||
{"invalid json syntax", `{invalid json`, iamerr.MalformedPolicyDocument("This policy contains invalid Json")},
|
||||
{"invalid version", `{"Version":"2020-01-01","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("The policy must contain a valid version string")},
|
||||
{"empty statement array", `{"Version":"2012-10-17","Statement":[]}`, iamerr.MalformedPolicyDocument("Could not parse the policy: Statement is empty!")},
|
||||
{"missing statement", `{"Version":"2012-10-17"}`, iamerr.MalformedPolicyDocument("Could not parse the policy: Statement is empty!")},
|
||||
|
||||
{"invalid effect value", `{"Version":"2012-10-17","Statement":[{"Effect":"Maybe","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Invalid effect: Maybe")},
|
||||
{"missing effect field", `{"Version":"2012-10-17","Statement":[{"Principal":{"Service":"s3.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Missing required field Effect")},
|
||||
|
||||
{"missing principal (opposite of an identity policy, which forbids it)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Missing required field Principal")},
|
||||
{"empty principal object", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Missing required field Principal cannot be empty!")},
|
||||
{"principal as bare string", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Principal must be a JSON object.")},
|
||||
{"principal as array", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":["a"],"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Syntax error in policy.")},
|
||||
{"principal has invalid key", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"CanonicalUser":"abc"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument(`Invalid principal in policy: "CanonicalUser"`)},
|
||||
{"principal key wrong case (looks like it should work, key match is case-sensitive)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"service":"s3.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument(`Invalid principal in policy: "service"`)},
|
||||
{"principal has unrecognized service", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"invalid.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument(`Invalid principal in policy: "SERVICE":"invalid.amazonaws.com"`)},
|
||||
{"principal has ec2 service (valid on real AWS, unsupported by this gateway)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument(`Invalid principal in policy: "SERVICE":"ec2.amazonaws.com"`)},
|
||||
|
||||
{"allow with notprincipal", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","NotPrincipal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("Allow with NotPrincipal is not allowed.")},
|
||||
{"deny with notprincipal", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","NotPrincipal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy must not contain NotPrincipal field.")},
|
||||
|
||||
{"missing action and notaction", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"}}]}`, iamerr.MalformedPolicyDocument("Missing required field Action")},
|
||||
{"bare wildcard action rejected (legal in an identity policy, not here)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"*"}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy may only specify STS AssumeRole actions.")},
|
||||
{"non-sts vendor action rejected", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"s3:GetObject"}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy may only specify STS AssumeRole actions.")},
|
||||
{"non-sts notaction rejected even on deny", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":{"AWS":"*"},"NotAction":"s3:GetObject"}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy may only specify STS AssumeRole actions.")},
|
||||
{"one non-sts action in an otherwise-valid array rejected", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":["sts:AssumeRole","s3:GetObject"]}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy may only specify STS AssumeRole actions.")},
|
||||
|
||||
{"resource forbidden", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Has prohibited field Resource")},
|
||||
{"notresource forbidden", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole","NotResource":"*"}]}`, iamerr.MalformedPolicyDocument("AssumeRole policy must not contain resources.")},
|
||||
|
||||
{"duplicate sid across statements", `{"Version":"2012-10-17","Statement":[{"Sid":"Dup","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"},{"Sid":"Dup","Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("The Statement Ids in the policy are not unique")},
|
||||
|
||||
{"cognito federated without condition (looks valid, Cognito needs a Condition)", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"cognito-identity.amazonaws.com"},"Action":"sts:AssumeRole"}]}`, iamerr.MalformedPolicyDocument("A condition block must be present for the Cognito provider")},
|
||||
}
|
||||
|
||||
func putObjects(client *s3.Client, objs []string, bucket string) ([]types.Object, error) {
|
||||
var contents []types.Object
|
||||
var size int64
|
||||
|
||||
Reference in New Issue
Block a user