mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* feat(iam): support caller-supplied AccessKeyId and SecretAccessKey in CreateAccessKey Both IAM implementations (standalone and embedded) now check for caller-supplied AccessKeyId and SecretAccessKey form parameters before generating random credentials. If provided, the caller-supplied values are used. If empty, random keys are generated as before. This enables programmatic identity provisioning where the caller needs to control the S3 credentials. Backward-compatible: no behavior change for callers that omit these parameters. * refactor(iam): extract shared caller-supplied credential validation Move the AccessKeyId/SecretAccessKey format checks and the in-memory collision scan into weed/iam so the standalone IAM API, the embedded IAM in s3api, and the admin dashboard all enforce the same rules. - ValidateCallerSuppliedAccessKeyId: 4-128 alphanumeric (rejects SigV4-breaking characters like '/' and '='). - ValidateCallerSuppliedSecretAccessKey: 8-128 chars. - FindAccessKeyOwner: scans identities and service accounts and returns the owning entity type + name for debug logging, without exposing the owner in caller-facing error messages. The admin dashboard previously only length-checked caller-supplied keys; it now enforces the same alphanumeric rule, which matches what SigV4 actually accepts anyway. * fix(iam): reject partial caller-supplied AccessKeyId/SecretAccessKey Previously, if a caller supplied only one of AccessKeyId or SecretAccessKey, CreateAccessKey logged a warning and auto-generated the missing half. That silently returns a credential the caller did not fully choose, which is surprising and easy to miss in a response they expected to echo back their input. Return ErrCodeInvalidInputException instead: either both are supplied or neither is. Updates the mixed-supply tests in weed/iamapi and weed/s3api to assert the rejection. * chore(iam): centralize and broaden sensitive form redaction DoActions and ExecuteAction both had an inline loop that redacted SecretAccessKey from their debug-level request log. Replace the two copies with iam.RedactSensitiveFormValues, backed by an explicit sensitive-keys set. The set now also covers Password, OldPassword, NewPassword, PrivateKey, and SessionToken. None of those parameters are used by today's IAM actions, but naming them here makes the log-safety guarantee survive future additions such as LoginProfile / STS. * test(iam): cover the upper length bound for CreateAccessKey TestCreateAccessKeyBoundary / TestEmbeddedIamCreateAccessKeyBoundary only exercised the 3/4-char lower edge. Add cases for 128 (accepted) and 129 (rejected) for AccessKeyId, plus 7 / 128 / 129-char cases for SecretAccessKey, so both ends of the validator are locked in at the handler level (the pure validators in weed/iam already cover this). * fix(s3api/iam): verify user existence before RNG and collision scan In the embedded IAM CreateAccessKey, the user lookup ran last: a request for a non-existent user still walked the whole identity / service-account list for collisions and, if no caller-supplied keys were present, generated fresh random credentials with crypto/rand before the NoSuchEntity error finally surfaced. Reorder: validate inputs, then find the target identity, then do the collision scan, then generate keys. A missing user now fails fast and consumes no entropy, and the handler returns NoSuchEntity instead of a misleading EntityAlreadyExists when both the user is missing and the supplied AccessKeyId happens to collide with another identity's key. Add TestEmbeddedIamCreateAccessKeyRejectsMissingUser to lock in the "no mutation on unknown user" guarantee. The standalone iamapi CreateAccessKey intentionally keeps its pre-existing "create-or-attach" semantics where a missing user is implicitly provisioned — that is a behavior change beyond the scope of this PR. * test(iam): tighten collision leak assertion and cover 8-char secret - Rename the collision-owner identity in TestCreateAccessKeyRejectsCollision (both iamapi and the embedded s3api test) from "existing" / "ExistingUser" to "ownerAlpha". The old assert.NotContains check was effectively a no-op because the error message never contained those substrings; a distinctive name shared with no part of the expected error body makes the leak guard actually meaningful if the wording ever drifts. The embedded test also adds a NotContains assertion that was previously missing entirely. - Add an explicit 8-char SecretAccessKey pass case to both boundary tests so the lower edge of the validator is locked in at the handler level alongside the 7 / 128 / 129-char cases. * fix(iamapi): enforce both-or-none before the collision lookup In the standalone IAM CreateAccessKey, FindAccessKeyOwner ran before the partial-credential check. If a caller supplied only AccessKeyId and it happened to collide with an existing key, the response was EntityAlreadyExists instead of the more fundamental InvalidInput for omitting SecretAccessKey — wrong error class, and leaked the fact that the probed key is already in use. Swap the order: validate both-or-none first, then do the collision scan. Matches the embedded IAM path and AWS behavior. Add a case to TestCreateAccessKeyRejectsPartialSupply that combines partial supply with a collision to lock in the ordering. * fix(admin): reject partial caller-supplied AccessKey/SecretKey The admin dashboard path silently generated the missing half when a caller supplied only one of AccessKey or SecretKey, while the IAM API and embedded IAM paths now reject this. Align the three: if exactly one is provided, return ErrInvalidInput. Also simplifies the generator block — either both are provided or neither is, so there is no mixed path to handle. * test(s3api/iam): guard dereferences in caller-supplied-keys test TestEmbeddedIamCreateAccessKeyWithCallerSuppliedKeys dereferenced *AccessKeyId/*SecretAccessKey/*UserName and indexed Identities[0].Credentials[0] without first verifying shape, so any future regression that returns a partial response or skips the config mutation would panic mid-assertion instead of failing with a clear message. Add require.NotNil on the response pointers and require.Len on the identities/credentials slices before the asserts. * test(iamapi): exercise the service-account branch of the collision check FindAccessKeyOwner scans both Identities[*].Credentials and ServiceAccounts[*].Credential, but TestCreateAccessKeyRejectsCollision only covered the identity branch. Split the test into two subtests — one per branch — so a future refactor that drops the service-account scan (or mutates the existing credential) trips a failure. Also asserts the existing service-account credential is not mutated and no credential is attached to the target identity on rejection. * test(iam): isolate 129-char secret subcase from prior credential In both TestCreateAccessKeyBoundary (iamapi) and TestEmbeddedIamCreateAccessKeyBoundary (s3api), the 129-char SecretAccessKey subcase reused the "validkey" AccessKeyId that the preceding 8-char subcase had just persisted into the config. The test still asserted the right outcome because the handler validates secret length before running the collision scan — but if the two checks ever swap, the subcase would pass (or fail) for the wrong reason. Reset the in-memory credentials before the 129-char subcase, matching the pattern already used by the 3/128/129-char AccessKeyId and 7-char secret subcases. No behavior change; purely test isolation. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com>
896 lines
29 KiB
Go
896 lines
29 KiB
Go
package iamapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// mockIamS3ApiConfig is a mock for testing
|
|
type mockIamS3ApiConfig struct {
|
|
policies Policies
|
|
}
|
|
|
|
func (m *mockIamS3ApiConfig) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockIamS3ApiConfig) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockIamS3ApiConfig) GetPolicies(policies *Policies) (err error) {
|
|
*policies = m.policies
|
|
if m.policies.Policies == nil {
|
|
return filer_pb.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockIamS3ApiConfig) PutPolicies(policies *Policies) (err error) {
|
|
m.policies = *policies
|
|
return nil
|
|
}
|
|
|
|
func TestGetActionsUserPath(t *testing.T) {
|
|
|
|
policyDocument := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:Put*", "s3:PutBucketAcl", "s3:Get*", "s3:GetBucketAcl", "s3:List*", "s3:Tagging*", "s3:DeleteBucket*"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::shared/user-Alice/*"),
|
|
},
|
|
},
|
|
}
|
|
|
|
actions, _ := GetActions(&policyDocument)
|
|
|
|
expectedActions := []string{
|
|
"Write:shared/user-Alice/*",
|
|
"WriteAcp:shared/user-Alice/*",
|
|
"Read:shared/user-Alice/*",
|
|
"ReadAcp:shared/user-Alice/*",
|
|
"List:shared/user-Alice/*",
|
|
"Tagging:shared/user-Alice/*",
|
|
"DeleteBucket:shared/user-Alice/*",
|
|
}
|
|
assert.Equal(t, expectedActions, actions)
|
|
}
|
|
|
|
func TestGetActionsWildcardPath(t *testing.T) {
|
|
|
|
policyDocument := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:Get*", "s3:PutBucketAcl"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
|
|
actions, _ := GetActions(&policyDocument)
|
|
|
|
expectedActions := []string{
|
|
"Read",
|
|
"WriteAcp",
|
|
}
|
|
assert.Equal(t, expectedActions, actions)
|
|
}
|
|
|
|
func TestGetActionsInvalidAction(t *testing.T) {
|
|
policyDocument := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:InvalidAction"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::shared/user-Alice/*"),
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err := GetActions(&policyDocument)
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, "not a valid action: 'InvalidAction'", err.Error())
|
|
}
|
|
|
|
func TestPutGetUserPolicyPreservesStatements(t *testing.T) {
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
policyJSON := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:GetObject",
|
|
"s3:ListBucket",
|
|
"s3:GetBucketLocation"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::my-bucket/*",
|
|
"arn:aws:s3:::test/*"
|
|
]
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:PutObject"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::my-bucket/*",
|
|
"arn:aws:s3:::test/*"
|
|
]
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:DeleteObject"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::my-bucket/*",
|
|
"arn:aws:s3:::test/*"
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
|
|
iama := &IamApiServer{
|
|
s3ApiConfig: &mockIamS3ApiConfig{},
|
|
}
|
|
putValues := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"inline-policy"},
|
|
"PolicyDocument": []string{policyJSON},
|
|
}
|
|
_, iamErr := iama.PutUserPolicy(s3cfg, putValues)
|
|
assert.Nil(t, iamErr)
|
|
|
|
getValues := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"inline-policy"},
|
|
}
|
|
resp, iamErr := iama.GetUserPolicy(s3cfg, getValues)
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Verify that key policy properties are preserved (not merged or lost)
|
|
var got policy_engine.PolicyDocument
|
|
assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))
|
|
|
|
// Assert we have exactly 3 statements (not merged into 1 or lost)
|
|
assert.Equal(t, 3, len(got.Statement))
|
|
|
|
// Assert that DeleteObject statement is present (was lost in the bug)
|
|
deleteObjectFound := false
|
|
for _, stmt := range got.Statement {
|
|
if len(stmt.Action.Strings()) > 0 {
|
|
for _, action := range stmt.Action.Strings() {
|
|
if action == "s3:DeleteObject" {
|
|
deleteObjectFound = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, deleteObjectFound, "s3:DeleteObject action was lost")
|
|
}
|
|
|
|
// TestPutGetUserPolicyIssue9008 is a regression test for
|
|
// https://github.com/seaweedfs/seaweedfs/issues/9008: put-user-policy followed
|
|
// by get-user-policy must return the same policy document that was submitted,
|
|
// with Action and Resource lists intact (no duplication, no collapsing).
|
|
func TestPutGetUserPolicyIssue9008(t *testing.T) {
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "steward"}},
|
|
}
|
|
policyJSON := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
|
|
"Resource": ["arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"]
|
|
}]
|
|
}`
|
|
|
|
mockCfg := &mockIamS3ApiConfig{}
|
|
iama := &IamApiServer{s3ApiConfig: mockCfg}
|
|
_, iamErr := iama.PutUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
"PolicyDocument": []string{policyJSON},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Part 1: verbatim round-trip. GetUserPolicy returns the exact document
|
|
// that was persisted, with Action and Resource lists intact.
|
|
resp, iamErr := iama.GetUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
var got policy_engine.PolicyDocument
|
|
assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))
|
|
|
|
assert.Equal(t, "2012-10-17", got.Version)
|
|
assert.Equal(t, 1, len(got.Statement))
|
|
stmt := got.Statement[0]
|
|
assert.Equal(t, policy_engine.PolicyEffectAllow, stmt.Effect)
|
|
assert.ElementsMatch(t, []string{"s3:GetObject", "s3:PutObject", "s3:ListBucket"}, stmt.Action.Strings())
|
|
assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, stmt.Resource.Strings())
|
|
|
|
// Part 2: fallback reconstruction. Clear the persisted inline policy so
|
|
// GetUserPolicy must rebuild the document from ident.Actions. The fallback
|
|
// is lossy (distinct S3 verbs collapse to wildcards like s3:Get*), but it
|
|
// must not duplicate actions nor conflate bucket-level and object-level
|
|
// resources.
|
|
mockCfg.policies = Policies{}
|
|
|
|
resp, iamErr = iama.GetUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
var fallback policy_engine.PolicyDocument
|
|
assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &fallback))
|
|
assert.Equal(t, 1, len(fallback.Statement), "fallback should merge equal-action statements")
|
|
fstmt := fallback.Statement[0]
|
|
|
|
// Each coarse verb appears exactly once (no duplication from the
|
|
// Read/Write/List -> s3:Get*/s3:Put*/s3:List* expansion).
|
|
assert.ElementsMatch(t, []string{"s3:Get*", "s3:Put*", "s3:List*"}, fstmt.Action.Strings())
|
|
|
|
// Bucket-level and object-level resources stay distinct — the bare bucket
|
|
// pattern must not be rewritten to an object ARN.
|
|
assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, fstmt.Resource.Strings())
|
|
}
|
|
|
|
func TestMultipleInlinePoliciesAggregateActions(t *testing.T) {
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
|
|
policy1JSON := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject", "s3:ListBucket"],
|
|
"Resource": ["arn:aws:s3:::bucket-a/*"]
|
|
}
|
|
]
|
|
}`
|
|
|
|
policy2JSON := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:PutObject"],
|
|
"Resource": ["arn:aws:s3:::bucket-b/*"]
|
|
}
|
|
]
|
|
}`
|
|
|
|
iama := &IamApiServer{
|
|
s3ApiConfig: &mockIamS3ApiConfig{},
|
|
}
|
|
|
|
// Put first inline policy
|
|
putValues1 := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"policy-read"},
|
|
"PolicyDocument": []string{policy1JSON},
|
|
}
|
|
_, iamErr := iama.PutUserPolicy(s3cfg, putValues1)
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Check that alice's actions include read operations
|
|
aliceIdent := s3cfg.Identities[0]
|
|
assert.Greater(t, len(aliceIdent.Actions), 0, "Actions should not be empty after first policy")
|
|
|
|
// Put second inline policy
|
|
putValues2 := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"policy-write"},
|
|
"PolicyDocument": []string{policy2JSON},
|
|
}
|
|
_, iamErr = iama.PutUserPolicy(s3cfg, putValues2)
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Check that alice now has aggregated actions from both policies
|
|
// Should include Read and List (from policy1) and Write (from policy2)
|
|
// with resource paths indicating which policy they came from
|
|
|
|
// Build a set of actual action strings for exact membership checks
|
|
actionSet := make(map[string]bool)
|
|
for _, action := range aliceIdent.Actions {
|
|
actionSet[action] = true
|
|
}
|
|
|
|
// Expected actions from both policies:
|
|
// - policy1: GetObject, ListBucket on bucket-a/* → "Read:bucket-a/*", "List:bucket-a/*"
|
|
// - policy2: PutObject on bucket-b/* → "Write:bucket-b/*"
|
|
expectedActions := []string{
|
|
"Read:bucket-a/*",
|
|
"List:bucket-a/*",
|
|
"Write:bucket-b/*",
|
|
}
|
|
|
|
for _, expectedAction := range expectedActions {
|
|
assert.True(t, actionSet[expectedAction], "Expected action '%s' not found in aggregated actions. Got: %v", expectedAction, aliceIdent.Actions)
|
|
}
|
|
}
|
|
|
|
// newTestIamApiServer creates a minimal IamApiServer for unit testing with only s3ApiConfig set.
|
|
// Other fields (iam, masterClient, etc.) are left nil — tests must not call code paths that use them.
|
|
func newTestIamApiServer(policies Policies) *IamApiServer {
|
|
return &IamApiServer{
|
|
s3ApiConfig: &mockIamS3ApiConfig{policies: policies},
|
|
}
|
|
}
|
|
|
|
func TestGetPolicy(t *testing.T) {
|
|
policyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
iama := newTestIamApiServer(Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{"my-policy": policyDoc},
|
|
})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{}
|
|
|
|
// Success case
|
|
values := url.Values{"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"}}
|
|
resp, iamErr := iama.GetPolicy(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, "my-policy", *resp.GetPolicyResult.Policy.PolicyName)
|
|
assert.Equal(t, "arn:aws:iam:::policy/my-policy", *resp.GetPolicyResult.Policy.Arn)
|
|
policyName := "my-policy"
|
|
expectedId := Hash(&policyName)
|
|
assert.Equal(t, expectedId, *resp.GetPolicyResult.Policy.PolicyId)
|
|
|
|
// Not found case
|
|
values = url.Values{"PolicyArn": []string{"arn:aws:iam:::policy/nonexistent"}}
|
|
_, iamErr = iama.GetPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
|
|
// Invalid ARN
|
|
values = url.Values{"PolicyArn": []string{"invalid-arn"}}
|
|
_, iamErr = iama.GetPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// Empty ARN
|
|
values = url.Values{"PolicyArn": []string{""}}
|
|
_, iamErr = iama.GetPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
}
|
|
|
|
func TestDeletePolicy(t *testing.T) {
|
|
policyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
mock := &mockIamS3ApiConfig{policies: Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{"my-policy": policyDoc},
|
|
}}
|
|
iama := &IamApiServer{s3ApiConfig: mock}
|
|
|
|
// Reject deletion when policy is attached to a user (AWS-compatible behavior)
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "alice",
|
|
PolicyNames: []string{"my-policy"},
|
|
}},
|
|
}
|
|
values := url.Values{"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"}}
|
|
_, iamErr := iama.DeletePolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeDeleteConflictException, iamErr.Code)
|
|
|
|
// Succeed when no users are attached
|
|
s3cfgEmpty := &iam_pb.S3ApiConfiguration{}
|
|
_, iamErr = iama.DeletePolicy(s3cfgEmpty, values)
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Verify deleted
|
|
_, iamErr = iama.GetPolicy(s3cfgEmpty, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
}
|
|
|
|
func TestListPolicies(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{}
|
|
|
|
// Empty case
|
|
resp, iamErr := iama.ListPolicies(s3cfg, url.Values{})
|
|
assert.Nil(t, iamErr)
|
|
assert.Empty(t, resp.ListPoliciesResult.Policies)
|
|
|
|
// Populated case
|
|
policyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
iama = newTestIamApiServer(Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{
|
|
"policy-a": policyDoc,
|
|
"policy-b": policyDoc,
|
|
},
|
|
})
|
|
|
|
resp, iamErr = iama.ListPolicies(s3cfg, url.Values{})
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, 2, len(resp.ListPoliciesResult.Policies))
|
|
for _, p := range resp.ListPoliciesResult.Policies {
|
|
name := *p.PolicyName
|
|
expectedId := Hash(&name)
|
|
assert.Equal(t, expectedId, *p.PolicyId, "PolicyId should be Hash(policyName) for %s", name)
|
|
}
|
|
}
|
|
|
|
func TestAttachUserPolicy(t *testing.T) {
|
|
policyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
iama := newTestIamApiServer(Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{"my-policy": policyDoc},
|
|
})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
|
|
// Success case
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"},
|
|
}
|
|
_, iamErr := iama.AttachUserPolicy(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Contains(t, s3cfg.Identities[0].PolicyNames, "my-policy")
|
|
// Verify actions were computed from the managed policy
|
|
assert.Greater(t, len(s3cfg.Identities[0].Actions), 0)
|
|
|
|
// Idempotent re-attach
|
|
_, iamErr = iama.AttachUserPolicy(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
// Should still have exactly one entry
|
|
count := 0
|
|
for _, name := range s3cfg.Identities[0].PolicyNames {
|
|
if name == "my-policy" {
|
|
count++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Policy not found
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyArn": []string{"arn:aws:iam:::policy/nonexistent"},
|
|
}
|
|
_, iamErr = iama.AttachUserPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
|
|
// User not found
|
|
values = url.Values{
|
|
"UserName": []string{"bob"},
|
|
"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"},
|
|
}
|
|
_, iamErr = iama.AttachUserPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
}
|
|
|
|
func TestManagedPolicyActionsPreservedAcrossInlineMutations(t *testing.T) {
|
|
managedPolicyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
iama := newTestIamApiServer(Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{"my-policy": managedPolicyDoc},
|
|
})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
|
|
// Attach managed policy
|
|
_, iamErr := iama.AttachUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
assert.Contains(t, s3cfg.Identities[0].Actions, "Read", "Managed policy should grant Read action")
|
|
|
|
// Add an inline policy
|
|
inlinePolicyJSON := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::bucket-x/*"}]}`
|
|
_, iamErr = iama.PutUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"inline-write"},
|
|
"PolicyDocument": []string{inlinePolicyJSON},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Should have both managed (Read) and inline (Write:bucket-x/*) actions
|
|
actionSet := make(map[string]bool)
|
|
for _, a := range s3cfg.Identities[0].Actions {
|
|
actionSet[a] = true
|
|
}
|
|
assert.True(t, actionSet["Read"], "Managed policy Read action should persist after PutUserPolicy")
|
|
assert.True(t, actionSet["Write:bucket-x/*"], "Inline policy Write action should be present")
|
|
|
|
// Delete the inline policy
|
|
_, iamErr = iama.DeleteUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyName": []string{"inline-write"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Managed policy actions should still be present
|
|
assert.Contains(t, s3cfg.Identities[0].PolicyNames, "my-policy", "Managed policy should still be attached")
|
|
assert.Contains(t, s3cfg.Identities[0].Actions, "Read", "Managed policy Read action should persist after DeleteUserPolicy")
|
|
}
|
|
|
|
func TestDetachUserPolicy(t *testing.T) {
|
|
policyDoc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
|
|
},
|
|
},
|
|
}
|
|
iama := newTestIamApiServer(Policies{
|
|
Policies: map[string]policy_engine.PolicyDocument{"my-policy": policyDoc},
|
|
})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice", PolicyNames: []string{"my-policy"}}},
|
|
}
|
|
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"PolicyArn": []string{"arn:aws:iam:::policy/my-policy"},
|
|
}
|
|
_, iamErr := iama.DetachUserPolicy(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Empty(t, s3cfg.Identities[0].PolicyNames)
|
|
|
|
// Detach again should fail (not attached)
|
|
_, iamErr = iama.DetachUserPolicy(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
}
|
|
|
|
func TestListAttachedUserPolicies(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice", PolicyNames: []string{"policy-a", "policy-b"}}},
|
|
}
|
|
|
|
values := url.Values{"UserName": []string{"alice"}}
|
|
resp, iamErr := iama.ListAttachedUserPolicies(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, 2, len(resp.ListAttachedUserPoliciesResult.AttachedPolicies))
|
|
assert.Equal(t, "policy-a", *resp.ListAttachedUserPoliciesResult.AttachedPolicies[0].PolicyName)
|
|
assert.Equal(t, "arn:aws:iam:::policy/policy-a", *resp.ListAttachedUserPoliciesResult.AttachedPolicies[0].PolicyArn)
|
|
|
|
// User not found
|
|
values = url.Values{"UserName": []string{"bob"}}
|
|
_, iamErr = iama.ListAttachedUserPolicies(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
}
|
|
|
|
func TestCreateAccessKeyWithCallerSuppliedKeys(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"myappkey"},
|
|
"SecretAccessKey": []string{"mysecret1234"},
|
|
}
|
|
resp, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, "myappkey", *resp.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
assert.Equal(t, "mysecret1234", *resp.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
assert.Equal(t, "alice", *resp.CreateAccessKeyResult.AccessKey.UserName)
|
|
assert.Equal(t, "myappkey", s3cfg.Identities[0].Credentials[0].AccessKey)
|
|
assert.Equal(t, "mysecret1234", s3cfg.Identities[0].Credentials[0].SecretKey)
|
|
}
|
|
|
|
func TestCreateAccessKeyRandomGeneration(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
}
|
|
resp, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.NotEmpty(t, *resp.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
assert.NotEmpty(t, *resp.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
assert.Len(t, s3cfg.Identities[0].Credentials, 1)
|
|
}
|
|
|
|
func TestCreateAccessKeyRejectsWeakKeys(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
// Too short
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"ab"},
|
|
"SecretAccessKey": []string{"validsecret1"},
|
|
}
|
|
_, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// Short secret
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"validkey"},
|
|
"SecretAccessKey": []string{"short"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// SigV4 delimiters
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"foo/bar=baz"},
|
|
"SecretAccessKey": []string{"validsecret1"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
}
|
|
|
|
func TestCreateAccessKeyRejectsCollision(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
// Use a distinctive owner name ("ownerAlpha") that shares no substring
|
|
// with the expected error message so the leak assertion is meaningful.
|
|
const ownerName = "ownerAlpha"
|
|
|
|
t.Run("identity credential", func(t *testing.T) {
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: ownerName,
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: "takenkey", SecretKey: "existingsecret"},
|
|
},
|
|
},
|
|
{Name: "newuser"},
|
|
},
|
|
}
|
|
values := url.Values{
|
|
"UserName": []string{"newuser"},
|
|
"AccessKeyId": []string{"takenkey"},
|
|
"SecretAccessKey": []string{"newsecret123"},
|
|
}
|
|
_, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeEntityAlreadyExistsException, iamErr.Code)
|
|
assert.NotContains(t, iamErr.Error.Error(), ownerName, "should not leak owner name")
|
|
assert.Len(t, s3cfg.Identities[1].Credentials, 0)
|
|
})
|
|
|
|
t.Run("service account credential", func(t *testing.T) {
|
|
const saId = "svcAlpha"
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "newuser"},
|
|
},
|
|
ServiceAccounts: []*iam_pb.ServiceAccount{
|
|
{
|
|
Id: saId,
|
|
Credential: &iam_pb.Credential{AccessKey: "takenkey", SecretKey: "existingsecret"},
|
|
},
|
|
},
|
|
}
|
|
values := url.Values{
|
|
"UserName": []string{"newuser"},
|
|
"AccessKeyId": []string{"takenkey"},
|
|
"SecretAccessKey": []string{"newsecret123"},
|
|
}
|
|
_, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeEntityAlreadyExistsException, iamErr.Code)
|
|
assert.NotContains(t, iamErr.Error.Error(), saId, "should not leak owner id")
|
|
// The service account's existing credential must be untouched, and
|
|
// no new credential should be attached to the identity.
|
|
assert.Equal(t, "takenkey", s3cfg.ServiceAccounts[0].Credential.AccessKey)
|
|
assert.Equal(t, "existingsecret", s3cfg.ServiceAccounts[0].Credential.SecretKey)
|
|
assert.Len(t, s3cfg.Identities[0].Credentials, 0)
|
|
})
|
|
}
|
|
|
|
func TestCreateAccessKeyBoundary(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
// Exactly 4 chars - should pass
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"abcd"},
|
|
"SecretAccessKey": []string{"secretkey123"},
|
|
}
|
|
resp, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, "abcd", *resp.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
|
|
// Exactly 3 chars - should fail
|
|
s3cfg.Identities[0].Credentials = nil
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"abc"},
|
|
"SecretAccessKey": []string{"secretkey123"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// Exactly 128 chars - should pass
|
|
s3cfg.Identities[0].Credentials = nil
|
|
ak128 := strings.Repeat("a", 128)
|
|
sk128 := strings.Repeat("s", 128)
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{ak128},
|
|
"SecretAccessKey": []string{sk128},
|
|
}
|
|
resp, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, ak128, *resp.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
assert.Equal(t, sk128, *resp.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
|
|
// 129 chars AccessKeyId - should fail
|
|
s3cfg.Identities[0].Credentials = nil
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{strings.Repeat("a", 129)},
|
|
"SecretAccessKey": []string{sk128},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// 7-char SecretAccessKey - should fail
|
|
s3cfg.Identities[0].Credentials = nil
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"validkey"},
|
|
"SecretAccessKey": []string{"1234567"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
|
|
// Exactly 8-char SecretAccessKey - should pass (lower boundary)
|
|
s3cfg.Identities[0].Credentials = nil
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"validkey"},
|
|
"SecretAccessKey": []string{"12345678"},
|
|
}
|
|
resp, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.Nil(t, iamErr)
|
|
assert.Equal(t, "12345678", *resp.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
|
|
// 129-char SecretAccessKey - should fail
|
|
s3cfg.Identities[0].Credentials = nil
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"validkey"},
|
|
"SecretAccessKey": []string{strings.Repeat("s", 129)},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
}
|
|
|
|
func TestCreateAccessKeyRejectsPartialSupply(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "alice"}},
|
|
}
|
|
// AccessKeyId supplied, SecretAccessKey omitted
|
|
values := url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"myappkey"},
|
|
}
|
|
_, iamErr := iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
assert.Len(t, s3cfg.Identities[0].Credentials, 0)
|
|
|
|
// SecretAccessKey supplied, AccessKeyId omitted
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"SecretAccessKey": []string{"secretkey123"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
assert.Len(t, s3cfg.Identities[0].Credentials, 0)
|
|
|
|
// Partial supply wins over collision: only AccessKeyId supplied, and
|
|
// it matches an existing credential. We must see InvalidInput, not
|
|
// EntityAlreadyExists — the both-or-none rule is more fundamental.
|
|
s3cfg = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "ownerAlpha",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: "takenkey", SecretKey: "s"},
|
|
},
|
|
},
|
|
{Name: "alice"},
|
|
},
|
|
}
|
|
values = url.Values{
|
|
"UserName": []string{"alice"},
|
|
"AccessKeyId": []string{"takenkey"},
|
|
}
|
|
_, iamErr = iama.CreateAccessKey(s3cfg, values)
|
|
assert.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeInvalidInputException, iamErr.Code)
|
|
}
|