mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* fix(iam): return a valid user ARN from CreateUser and GetUser The terraform aws provider 6.41 reads a user back after creating it and blocks until GetUser returns a value that passes arn.IsARN. We only set UserName, so the ARN was empty and apply hung until the 2m timeout. Populate Arn (and Path) via a shared iam.NewUser helper in both the embedded and standalone IAM handlers. * fix(iam): use the userName parameter directly in NewUser Drop the redundant local copy; the value parameter is already function-local. * fix(iam): return full user objects with ARNs from GetGroup GetGroup listed members with only UserName set. Build them via the shared NewUser helper so group members carry a valid Arn and Path like the other user responses, in both the embedded and standalone IAM handlers.
915 lines
30 KiB
Go
915 lines
30 KiB
Go
package iamapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"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)
|
|
}
|
|
|
|
// TestUserArnIsValid is a regression test for issue #9786: the terraform aws
|
|
// provider (>= 6.41) reads a user back after creating it and blocks until
|
|
// GetUser returns a value that passes arn.IsARN.
|
|
func TestUserArnIsValid(t *testing.T) {
|
|
iama := newTestIamApiServer(Policies{})
|
|
s3cfg := &iam_pb.S3ApiConfiguration{}
|
|
|
|
createResp := iama.CreateUser(s3cfg, url.Values{"UserName": []string{"alice"}})
|
|
assert.True(t, arn.IsARN(aws.StringValue(createResp.CreateUserResult.User.Arn)),
|
|
"CreateUser must return a valid ARN, got %q", aws.StringValue(createResp.CreateUserResult.User.Arn))
|
|
|
|
getResp, iamErr := iama.GetUser(s3cfg, "alice")
|
|
assert.Nil(t, iamErr)
|
|
assert.True(t, arn.IsARN(aws.StringValue(getResp.GetUserResult.User.Arn)),
|
|
"GetUser must return a valid ARN, got %q", aws.StringValue(getResp.GetUserResult.User.Arn))
|
|
}
|
|
|
|
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)
|
|
}
|