e1a432a167
Allows vault roles to be associated with IAM groups in the AWS secrets engine, since IAM groups are a recommended way to manage IAM user policies. IAM users generated against a vault role will be added to the IAM Groups. For a credential type of `assumed_role` or `federation_token`, the policies sent to the corresponding AWS call (sts:AssumeRole or sts:GetFederation) will be the policies from each group in `iam_groups` combined with the `policy_document` and `policy_arns` parameters. Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com>
256 lines
8.2 KiB
Go
256 lines
8.2 KiB
Go
package aws
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
const ec2DescribePolicy = `{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["ec2:DescribeInstances"], "Resource": "*"}]}`
|
|
|
|
// ec2AllPolicy also uses a string instead of a list for the Action
|
|
const ec2AllPolicy = `{"Version": "2012-10-17","Statement": [{"Effect": "Allow", "Action": "ec2:*", "Resource": "*"}]}`
|
|
|
|
// ec2SingleStatement is an example of the Statement portion containing a single statement that's not a list
|
|
const ec2SingleStatement = `{"Version": "2012-10-17", "Statement": {"Effect": "Allow", "Action": ["ec2:DescribeInstances"], "Resource": "*"}}`
|
|
|
|
type mockGroupIAMClient struct {
|
|
iamiface.IAMAPI
|
|
ListAttachedGroupPoliciesResp iam.ListAttachedGroupPoliciesOutput
|
|
ListGroupPoliciesResp iam.ListGroupPoliciesOutput
|
|
GetGroupPolicyResp iam.GetGroupPolicyOutput
|
|
}
|
|
|
|
func (m mockGroupIAMClient) ListAttachedGroupPolicies(in *iam.ListAttachedGroupPoliciesInput) (*iam.ListAttachedGroupPoliciesOutput, error) {
|
|
return &m.ListAttachedGroupPoliciesResp, nil
|
|
}
|
|
|
|
func (m mockGroupIAMClient) ListGroupPolicies(in *iam.ListGroupPoliciesInput) (*iam.ListGroupPoliciesOutput, error) {
|
|
return &m.ListGroupPoliciesResp, nil
|
|
}
|
|
|
|
func (m mockGroupIAMClient) GetGroupPolicy(in *iam.GetGroupPolicyInput) (*iam.GetGroupPolicyOutput, error) {
|
|
return &m.GetGroupPolicyResp, nil
|
|
}
|
|
|
|
func Test_getGroupPolicies(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
description string
|
|
listAGPResp iam.ListAttachedGroupPoliciesOutput
|
|
listGPResp iam.ListGroupPoliciesOutput
|
|
getGPResp iam.GetGroupPolicyOutput
|
|
iamGroupArg []string
|
|
wantGroupPolicies []string
|
|
wantGroupPolicyARNs []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
description: "All IAM calls respond with data",
|
|
listAGPResp: iam.ListAttachedGroupPoliciesOutput{
|
|
AttachedPolicies: []*iam.AttachedPolicy{
|
|
{
|
|
PolicyArn: aws.String("abcdefghijklmnopqrst"),
|
|
PolicyName: aws.String("test policy"),
|
|
},
|
|
},
|
|
},
|
|
listGPResp: iam.ListGroupPoliciesOutput{
|
|
PolicyNames: []*string{
|
|
aws.String("inline policy"),
|
|
},
|
|
},
|
|
getGPResp: iam.GetGroupPolicyOutput{
|
|
GroupName: aws.String("inline policy"),
|
|
PolicyDocument: aws.String(ec2DescribePolicy),
|
|
PolicyName: aws.String("ec2 describe"),
|
|
},
|
|
iamGroupArg: []string{"testgroup1"},
|
|
wantGroupPolicies: []string{ec2DescribePolicy},
|
|
wantGroupPolicyARNs: []string{"abcdefghijklmnopqrst"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
description: "No managed policies",
|
|
listAGPResp: iam.ListAttachedGroupPoliciesOutput{},
|
|
listGPResp: iam.ListGroupPoliciesOutput{
|
|
PolicyNames: []*string{
|
|
aws.String("inline policy"),
|
|
},
|
|
},
|
|
getGPResp: iam.GetGroupPolicyOutput{
|
|
GroupName: aws.String("inline policy"),
|
|
PolicyDocument: aws.String(ec2DescribePolicy),
|
|
PolicyName: aws.String("ec2 describe"),
|
|
},
|
|
iamGroupArg: []string{"testgroup1", "testgroup2"},
|
|
wantGroupPolicies: []string{ec2DescribePolicy, ec2DescribePolicy},
|
|
wantGroupPolicyARNs: []string(nil),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
description: "No inline policies",
|
|
listAGPResp: iam.ListAttachedGroupPoliciesOutput{
|
|
AttachedPolicies: []*iam.AttachedPolicy{
|
|
{
|
|
PolicyArn: aws.String("abcdefghijklmnopqrst"),
|
|
PolicyName: aws.String("test policy"),
|
|
},
|
|
},
|
|
},
|
|
listGPResp: iam.ListGroupPoliciesOutput{},
|
|
getGPResp: iam.GetGroupPolicyOutput{},
|
|
iamGroupArg: []string{"testgroup1"},
|
|
wantGroupPolicies: []string(nil),
|
|
wantGroupPolicyARNs: []string{"abcdefghijklmnopqrst"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
description: "No policies",
|
|
listAGPResp: iam.ListAttachedGroupPoliciesOutput{},
|
|
listGPResp: iam.ListGroupPoliciesOutput{},
|
|
getGPResp: iam.GetGroupPolicyOutput{},
|
|
iamGroupArg: []string{"testgroup1"},
|
|
wantGroupPolicies: []string(nil),
|
|
wantGroupPolicyARNs: []string(nil),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
description: "empty iam_groups arg",
|
|
listAGPResp: iam.ListAttachedGroupPoliciesOutput{},
|
|
listGPResp: iam.ListGroupPoliciesOutput{},
|
|
getGPResp: iam.GetGroupPolicyOutput{},
|
|
iamGroupArg: []string{},
|
|
wantGroupPolicies: []string(nil),
|
|
wantGroupPolicyARNs: []string(nil),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
// configure backend and iam client
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
|
|
b := Backend()
|
|
if err := b.Setup(context.Background(), config); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.iamClient = &mockGroupIAMClient{
|
|
ListAttachedGroupPoliciesResp: tc.listAGPResp,
|
|
ListGroupPoliciesResp: tc.listGPResp,
|
|
GetGroupPolicyResp: tc.getGPResp,
|
|
}
|
|
|
|
// run the test and compare results
|
|
groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(context.TODO(), config.StorageView, tc.iamGroupArg)
|
|
assert.Equal(t, tc.wantGroupPolicies, groupPolicies)
|
|
assert.Equal(t, tc.wantGroupPolicyARNs, groupPolicyARNs)
|
|
assert.Equal(t, tc.wantErr, err != nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_combinePolicyDocuments(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
description string
|
|
input []string
|
|
expectedOutput string
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
description: "one policy",
|
|
input: []string{
|
|
ec2AllPolicy,
|
|
},
|
|
expectedOutput: `{"Version":"2012-10-17","Statement":[{"Action":"ec2:*","Effect":"Allow","Resource":"*"}]}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "two policies",
|
|
input: []string{
|
|
ec2AllPolicy,
|
|
ec2DescribePolicy,
|
|
},
|
|
expectedOutput: `{"Version": "2012-10-17", "Statement":[
|
|
{"Action": "ec2:*", "Effect": "Allow", "Resource": "*"},
|
|
{"Action": ["ec2:DescribeInstances"], "Effect": "Allow", "Resource": "*"}]}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "two policies, one with empty statement",
|
|
input: []string{
|
|
ec2AllPolicy,
|
|
`{"Version": "2012-10-17", "Statement": []}`,
|
|
},
|
|
expectedOutput: `{"Version": "2012-10-17", "Statement": [{"Action": "ec2:*", "Effect": "Allow", "Resource": "*"}]}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "malformed json",
|
|
input: []string{
|
|
`"Version": "2012-10-17","Statement": [{"Effect": "Allow", "Action": "ec2:*", "Resource": "*"}]}`,
|
|
`{"Version": "2012-10-17", "Statement": []}`,
|
|
},
|
|
expectedOutput: ``,
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
description: "not action",
|
|
input: []string{
|
|
`{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "NotAction": "ec2:DescribeAvailabilityZones", "Resource": "*"}]}`,
|
|
},
|
|
expectedOutput: `{"Version": "2012-10-17","Statement":[{"Effect": "Allow","NotAction": "ec2:DescribeAvailabilityZones", "Resource": "*"}]}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "one blank policy",
|
|
input: []string{
|
|
"",
|
|
`{"Version": "2012-10-17", "Statement": []}`,
|
|
},
|
|
expectedOutput: `{"Version": "2012-10-17", "Statement": []}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "when statement is not a list",
|
|
input: []string{
|
|
ec2SingleStatement,
|
|
},
|
|
expectedOutput: `{"Version": "2012-10-17", "Statement": [{"Action": ["ec2:DescribeInstances"], "Effect": "Allow", "Resource": "*"}]}`,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
description: "statement is malformed json",
|
|
input: []string{
|
|
`{"Version": "2012-10-17", "Statement": {true}`,
|
|
},
|
|
expectedOutput: "",
|
|
expectedErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
policyOut, err := combinePolicyDocuments(tc.input...)
|
|
if (err != nil) != tc.expectedErr {
|
|
t.Fatalf("got unexpected error: %s", err)
|
|
}
|
|
if (err != nil) != tc.expectedErr {
|
|
t.Fatalf("got unexpected error: %s", err)
|
|
}
|
|
// remove whitespace
|
|
tc.expectedOutput, err = compactJSON(tc.expectedOutput)
|
|
if policyOut != tc.expectedOutput {
|
|
t.Fatalf("did not receive expected output: want %s, got %s", tc.expectedOutput, policyOut)
|
|
}
|
|
})
|
|
}
|
|
}
|