AWS: Add iam_groups parameter to role create/update (#8811)
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>
This commit is contained in:
parent
315d1ba9c5
commit
e1a432a167
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/vault/helper/testhelpers"
|
||||
|
@ -357,6 +358,49 @@ func createUser(t *testing.T, userName string, accessKey *awsAccessKey) {
|
|||
accessKey.SecretAccessKey = *genAccessKey.SecretAccessKey
|
||||
}
|
||||
|
||||
// Create an IAM Group and add an inline policy and managed policies if specified
|
||||
func createGroup(t *testing.T, groupName string, inlinePolicy string, managedPolicies []string) {
|
||||
awsConfig := &aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
HTTPClient: cleanhttp.DefaultClient(),
|
||||
}
|
||||
sess, err := session.NewSession(awsConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := iam.New(sess)
|
||||
createGroupInput := &iam.CreateGroupInput{
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
log.Printf("[INFO] AWS CreateGroup: %s", groupName)
|
||||
if _, err := svc.CreateGroup(createGroupInput); err != nil {
|
||||
t.Fatalf("AWS CreateGroup failed: %v", err)
|
||||
}
|
||||
|
||||
if len(inlinePolicy) > 0 {
|
||||
putPolicyInput := &iam.PutGroupPolicyInput{
|
||||
PolicyDocument: aws.String(inlinePolicy),
|
||||
PolicyName: aws.String("InlinePolicy"),
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
_, err = svc.PutGroupPolicy(putPolicyInput)
|
||||
if err != nil {
|
||||
t.Fatalf("AWS PutGroupPolicy failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range managedPolicies {
|
||||
attachGroupPolicyInput := &iam.AttachGroupPolicyInput{
|
||||
PolicyArn: aws.String(mp),
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
_, err = svc.AttachGroupPolicy(attachGroupPolicyInput)
|
||||
if err != nil {
|
||||
t.Fatalf("AWS AttachGroupPolicy failed, %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTestRole(roleName string) error {
|
||||
awsConfig := &aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
|
@ -452,6 +496,71 @@ func deleteTestUser(accessKey *awsAccessKey, userName string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func deleteTestGroup(groupName string) error {
|
||||
awsConfig := &aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
HTTPClient: cleanhttp.DefaultClient(),
|
||||
}
|
||||
sess, err := session.NewSession(awsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svc := iam.New(sess)
|
||||
|
||||
// Detach any managed group policies
|
||||
getGroupsInput := &iam.ListAttachedGroupPoliciesInput{
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
getGroupsOutput, err := svc.ListAttachedGroupPolicies(getGroupsInput)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] AWS ListAttachedGroupPolicies failed: %v", err)
|
||||
return err
|
||||
}
|
||||
for _, g := range getGroupsOutput.AttachedPolicies {
|
||||
detachGroupInput := &iam.DetachGroupPolicyInput{
|
||||
GroupName: aws.String(groupName),
|
||||
PolicyArn: g.PolicyArn,
|
||||
}
|
||||
if _, err := svc.DetachGroupPolicy(detachGroupInput); err != nil {
|
||||
log.Printf("[WARN] AWS DetachGroupPolicy failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any inline policies
|
||||
listGroupPoliciesInput := &iam.ListGroupPoliciesInput{
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
listGroupPoliciesOutput, err := svc.ListGroupPolicies(listGroupPoliciesInput)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] AWS ListGroupPolicies failed: %v", err)
|
||||
return err
|
||||
}
|
||||
for _, g := range listGroupPoliciesOutput.PolicyNames {
|
||||
deleteGroupPolicyInput := &iam.DeleteGroupPolicyInput{
|
||||
GroupName: aws.String(groupName),
|
||||
PolicyName: g,
|
||||
}
|
||||
if _, err := svc.DeleteGroupPolicy(deleteGroupPolicyInput); err != nil {
|
||||
log.Printf("[WARN] AWS DeleteGroupPolicy failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the group
|
||||
deleteTestGroupInput := &iam.DeleteGroupInput{
|
||||
GroupName: aws.String(groupName),
|
||||
}
|
||||
log.Printf("[INFO] AWS DeleteGroup: %s", groupName)
|
||||
_, err = svc.DeleteGroup(deleteTestGroupInput)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] AWS DeleteGroup failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testAccStepConfig(t *testing.T) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
|
@ -674,6 +783,25 @@ func listDynamoTablesTest(accessKey, secretKey, token string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func listS3BucketsTest(accessKey, secretKey, token string) error {
|
||||
creds := credentials.NewStaticCredentials(accessKey, secretKey, token)
|
||||
awsConfig := &aws.Config{
|
||||
Credentials: creds,
|
||||
Region: aws.String("us-east-1"),
|
||||
HTTPClient: cleanhttp.DefaultClient(),
|
||||
}
|
||||
sess, err := session.NewSession(awsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := s3.New(sess)
|
||||
log.Printf("[WARN] Verifying that the generated credentials work with s3:ListBuckets...")
|
||||
return retryUntilSuccess(func() error {
|
||||
_, err := client.ListBuckets(&s3.ListBucketsInput{})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func retryUntilSuccess(op func() error) error {
|
||||
retryCount := 0
|
||||
success := false
|
||||
|
@ -743,6 +871,7 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest.
|
|||
"max_sts_ttl": int64(0),
|
||||
"user_path": "",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": []string(nil),
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
@ -769,6 +898,20 @@ const testDynamoPolicy = `{
|
|||
}
|
||||
`
|
||||
|
||||
const testS3Policy = `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:Get*",
|
||||
"s3:List*"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
const adminAccessPolicyArn = "arn:aws:iam::aws:policy/AdministratorAccess"
|
||||
const ec2PolicyArn = "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
|
||||
const iamPolicyArn = "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
|
||||
|
@ -825,15 +968,17 @@ func TestBackend_basicPolicyArnRef(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_iamUserManagedInlinePolicies(t *testing.T) {
|
||||
func TestBackend_iamUserManagedInlinePoliciesGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
compacted, err := compactJSON(testDynamoPolicy)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %#v", err)
|
||||
}
|
||||
groupName := generateUniqueName(t.Name())
|
||||
roleData := map[string]interface{}{
|
||||
"policy_document": testDynamoPolicy,
|
||||
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
|
||||
"iam_groups": []string{groupName},
|
||||
"credential_type": iamUserCred,
|
||||
"user_path": "/path/",
|
||||
}
|
||||
|
@ -846,18 +991,72 @@ func TestBackend_iamUserManagedInlinePolicies(t *testing.T) {
|
|||
"max_sts_ttl": int64(0),
|
||||
"user_path": "/path/",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": []string{groupName},
|
||||
}
|
||||
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
PreCheck: func() {
|
||||
testAccPreCheck(t)
|
||||
createGroup(t, groupName, testS3Policy, []string{})
|
||||
},
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t),
|
||||
testAccStepWriteRole(t, "test", roleData),
|
||||
testAccStepReadRole(t, "test", expectedRoleData),
|
||||
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, assertCreatedIAMUser}),
|
||||
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest}),
|
||||
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, assertCreatedIAMUser, listS3BucketsTest}),
|
||||
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, listS3BucketsTest}),
|
||||
},
|
||||
Teardown: func() error {
|
||||
return deleteTestGroup(groupName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Similar to TestBackend_iamUserManagedInlinePoliciesGroups() but managing
|
||||
// policies only with groups
|
||||
func TestBackend_iamUserGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
group1Name := generateUniqueName(t.Name())
|
||||
group2Name := generateUniqueName(t.Name())
|
||||
roleData := map[string]interface{}{
|
||||
"iam_groups": []string{group1Name, group2Name},
|
||||
"credential_type": iamUserCred,
|
||||
"user_path": "/path/",
|
||||
}
|
||||
expectedRoleData := map[string]interface{}{
|
||||
"policy_document": "",
|
||||
"policy_arns": []string(nil),
|
||||
"credential_type": iamUserCred,
|
||||
"role_arns": []string(nil),
|
||||
"default_sts_ttl": int64(0),
|
||||
"max_sts_ttl": int64(0),
|
||||
"user_path": "/path/",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": []string{group1Name, group2Name},
|
||||
}
|
||||
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() {
|
||||
testAccPreCheck(t)
|
||||
createGroup(t, group1Name, testS3Policy, []string{ec2PolicyArn, iamPolicyArn})
|
||||
createGroup(t, group2Name, testDynamoPolicy, []string{})
|
||||
},
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t),
|
||||
testAccStepWriteRole(t, "test", roleData),
|
||||
testAccStepReadRole(t, "test", expectedRoleData),
|
||||
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, assertCreatedIAMUser, listS3BucketsTest}),
|
||||
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, listIamUsersTest, listDynamoTablesTest, listS3BucketsTest}),
|
||||
},
|
||||
Teardown: func() error {
|
||||
if err := deleteTestGroup(group1Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteTestGroup(group2Name)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -948,6 +1147,63 @@ func TestBackend_AssumedRoleWithPolicyARN(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_AssumedRoleWithGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
roleName := generateUniqueName(t.Name())
|
||||
groupName := generateUniqueName(t.Name())
|
||||
// This looks a bit curious. The policy document and the role document act
|
||||
// as a logical intersection of policies. The role allows ec2:Describe*
|
||||
// (among other permissions). This policy allows everything BUT
|
||||
// ec2:DescribeAvailabilityZones. Thus, the logical intersection of the two
|
||||
// is all ec2:Describe* EXCEPT ec2:DescribeAvailabilityZones, and so the
|
||||
// describeAZs call should fail
|
||||
allowAllButDescribeAzs := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"NotAction": "ec2:DescribeAvailabilityZones",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
awsAccountID, err := getAccountID()
|
||||
if err != nil {
|
||||
t.Logf("Unable to retrive user via sts:GetCallerIdentity: %#v", err)
|
||||
t.Skip("Could not determine AWS account ID from sts:GetCallerIdentity for acceptance tests, skipping")
|
||||
}
|
||||
|
||||
roleData := map[string]interface{}{
|
||||
"iam_groups": []string{groupName},
|
||||
"role_arns": []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", awsAccountID, roleName)},
|
||||
"credential_type": assumedRoleCred,
|
||||
}
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() {
|
||||
testAccPreCheck(t)
|
||||
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn})
|
||||
createGroup(t, groupName, allowAllButDescribeAzs, []string{})
|
||||
// Sleep sometime because AWS is eventually consistent
|
||||
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
|
||||
time.Sleep(10 * time.Second)
|
||||
},
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t),
|
||||
testAccStepWriteRole(t, "test", roleData),
|
||||
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, describeAzsTestUnauthorized}),
|
||||
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, describeAzsTestUnauthorized}),
|
||||
},
|
||||
Teardown: func() error {
|
||||
if err := deleteTestGroup(groupName); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteTestRole(roleName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackend_FederationTokenWithPolicyARN(t *testing.T) {
|
||||
t.Parallel()
|
||||
userName := generateUniqueName(t.Name())
|
||||
|
@ -979,6 +1235,56 @@ func TestBackend_FederationTokenWithPolicyARN(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_FederationTokenWithGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
userName := generateUniqueName(t.Name())
|
||||
groupName := generateUniqueName(t.Name())
|
||||
accessKey := &awsAccessKey{}
|
||||
|
||||
// IAM policy where Statement is a single element, not a list
|
||||
iamSingleStatementPolicy := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": {
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:Get*",
|
||||
"s3:List*"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
}`
|
||||
|
||||
roleData := map[string]interface{}{
|
||||
"iam_groups": []string{groupName},
|
||||
"policy_document": iamSingleStatementPolicy,
|
||||
"credential_type": federationTokenCred,
|
||||
}
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() {
|
||||
testAccPreCheck(t)
|
||||
createUser(t, userName, accessKey)
|
||||
createGroup(t, groupName, "", []string{dynamoPolicyArn})
|
||||
// Sleep sometime because AWS is eventually consistent
|
||||
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
|
||||
time.Sleep(10 * time.Second)
|
||||
},
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfigWithCreds(t, accessKey),
|
||||
testAccStepWriteRole(t, "test", roleData),
|
||||
testAccStepRead(t, "sts", "test", []credentialTestFunc{listDynamoTablesTest, describeAzsTestUnauthorized, listS3BucketsTest}),
|
||||
testAccStepRead(t, "creds", "test", []credentialTestFunc{listDynamoTablesTest, describeAzsTestUnauthorized, listS3BucketsTest}),
|
||||
},
|
||||
Teardown: func() error {
|
||||
if err := deleteTestGroup(groupName); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteTestUser(accessKey, userName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackend_RoleDefaultSTSTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
roleName := generateUniqueName(t.Name())
|
||||
|
@ -1051,6 +1357,7 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
|
|||
"max_sts_ttl": int64(0),
|
||||
"user_path": "",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": []string(nil),
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
@ -1071,6 +1378,65 @@ func testAccStepWriteArnRoleRef(t *testing.T, vaultRoleName, awsRoleName, awsAcc
|
|||
}
|
||||
}
|
||||
|
||||
func TestBackend_iamGroupsCrud(t *testing.T) {
|
||||
t.Parallel()
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t),
|
||||
testAccStepWriteIamGroups(t, "test", []string{"group1", "group2"}),
|
||||
testAccStepReadIamGroups(t, "test", []string{"group1", "group2"}),
|
||||
testAccStepDeletePolicy(t, "test"),
|
||||
testAccStepReadIamGroups(t, "test", []string{}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccStepWriteIamGroups(t *testing.T, name string, groups []string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "roles/" + name,
|
||||
Data: map[string]interface{}{
|
||||
"credential_type": iamUserCred,
|
||||
"iam_groups": groups,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepReadIamGroups(t *testing.T, name string, groups []string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "roles/" + name,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp == nil {
|
||||
if len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("bad: %#v", resp)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"policy_arns": []string(nil),
|
||||
"role_arns": []string(nil),
|
||||
"policy_document": "",
|
||||
"credential_type": iamUserCred,
|
||||
"default_sts_ttl": int64(0),
|
||||
"max_sts_ttl": int64(0),
|
||||
"user_path": "",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": groups,
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generateUniqueName(prefix string) string {
|
||||
return testhelpers.RandomWithPrefix(prefix)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// PolicyDocument represents an IAM policy document
|
||||
type PolicyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statements StatementEntries `json:"Statement"`
|
||||
}
|
||||
|
||||
// StatementEntries is a slice of statements that make up a PolicyDocument
|
||||
type StatementEntries []interface{}
|
||||
|
||||
// UnmarshalJSON is defined here for StatementEntries because the Statement
|
||||
// portion of an IAM Policy can either be a list or a single element, so if it's
|
||||
// a single element this wraps it in a []interface{} so that it's easy to
|
||||
// combine with other policy statements:
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_statement.html
|
||||
func (se *StatementEntries) UnmarshalJSON(b []byte) error {
|
||||
var out StatementEntries
|
||||
|
||||
var data interface{}
|
||||
if err := json.Unmarshal(b, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t := data.(type) {
|
||||
case []interface{}:
|
||||
out = t
|
||||
case interface{}:
|
||||
out = []interface{}{t}
|
||||
default:
|
||||
return fmt.Errorf("unsupported data type %T for StatementEntries", t)
|
||||
}
|
||||
*se = out
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGroupPolicies takes a list of IAM Group names and returns a list of their
|
||||
// inline policy documents, and a list of the attached managed policy ARNs
|
||||
func (b *backend) getGroupPolicies(ctx context.Context, s logical.Storage, iamGroups []string) ([]string, []string, error) {
|
||||
var groupPolicies []string
|
||||
var groupPolicyARNs []string
|
||||
var err error
|
||||
var agp *iam.ListAttachedGroupPoliciesOutput
|
||||
var inlinePolicies *iam.ListGroupPoliciesOutput
|
||||
var inlinePolicyDoc *iam.GetGroupPolicyOutput
|
||||
var iamClient iamiface.IAMAPI
|
||||
|
||||
// Return early if there are no groups, to avoid creating an IAM client
|
||||
// needlessly
|
||||
if len(iamGroups) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
iamClient, err = b.clientIAM(ctx, s)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, g := range iamGroups {
|
||||
// Collect managed policy ARNs from the IAM Group
|
||||
agp, err = iamClient.ListAttachedGroupPolicies(&iam.ListAttachedGroupPoliciesInput{
|
||||
GroupName: aws.String(g),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, p := range agp.AttachedPolicies {
|
||||
groupPolicyARNs = append(groupPolicyARNs, *p.PolicyArn)
|
||||
}
|
||||
|
||||
// Collect inline policy names from the IAM Group
|
||||
inlinePolicies, err = iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{
|
||||
GroupName: aws.String(g),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, iP := range inlinePolicies.PolicyNames {
|
||||
inlinePolicyDoc, err = iamClient.GetGroupPolicy(&iam.GetGroupPolicyInput{
|
||||
GroupName: &g,
|
||||
PolicyName: iP,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if inlinePolicyDoc != nil && inlinePolicyDoc.PolicyDocument != nil {
|
||||
var policyStr string
|
||||
if policyStr, err = url.QueryUnescape(*inlinePolicyDoc.PolicyDocument); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupPolicies = append(groupPolicies, policyStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return groupPolicies, groupPolicyARNs, nil
|
||||
}
|
||||
|
||||
// combinePolicyDocuments takes policy strings as input, and combines them into
|
||||
// a single policy document string
|
||||
func combinePolicyDocuments(policies ...string) (string, error) {
|
||||
var policy string
|
||||
var err error
|
||||
var policyBytes []byte
|
||||
var newPolicy = PolicyDocument{
|
||||
// 2012-10-17 is the current version of the AWS policy language:
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
|
||||
Version: "2012-10-17",
|
||||
}
|
||||
newPolicy.Statements = make(StatementEntries, 0, len(policies))
|
||||
|
||||
for _, p := range policies {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
var tmpDoc PolicyDocument
|
||||
err = json.Unmarshal([]byte(p), &tmpDoc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newPolicy.Statements = append(newPolicy.Statements, tmpDoc.Statements...)
|
||||
}
|
||||
|
||||
policyBytes, err = json.Marshal(&newPolicy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
policy = string(policyBytes)
|
||||
return policy, nil
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -80,6 +80,19 @@ will be passed in as the Policy parameter to the AssumeRole or
|
|||
GetFederationToken API call, acting as a filter on permissions available.`,
|
||||
},
|
||||
|
||||
"iam_groups": &framework.FieldSchema{
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `Names of IAM groups that generated IAM users will be added to. 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.`,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
Name: "IAM Groups",
|
||||
Value: "group1,group2",
|
||||
},
|
||||
},
|
||||
|
||||
"default_sts_ttl": &framework.FieldSchema{
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: fmt.Sprintf("Default TTL for %s and %s credential types when no TTL is explicitly requested with the credentials", assumedRoleCred, federationTokenCred),
|
||||
|
@ -284,6 +297,10 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
|
|||
roleEntry.PermissionsBoundaryARN = permissionsBoundaryARNRaw.(string)
|
||||
}
|
||||
|
||||
if iamGroups, ok := d.GetOk("iam_groups"); ok {
|
||||
roleEntry.IAMGroups = iamGroups.([]string)
|
||||
}
|
||||
|
||||
if legacyRole != "" {
|
||||
roleEntry = upgradeLegacyPolicyEntry(legacyRole)
|
||||
if roleEntry.InvalidData != "" {
|
||||
|
@ -468,6 +485,7 @@ type awsRoleEntry struct {
|
|||
PolicyArns []string `json:"policy_arns"` // ARNs of managed policies to attach to an IAM user
|
||||
RoleArns []string `json:"role_arns"` // ARNs of roles to assume for AssumedRole credentials
|
||||
PolicyDocument string `json:"policy_document"` // JSON-serialized inline policy to attach to IAM users and/or to specify as the Policy parameter in AssumeRole calls
|
||||
IAMGroups []string `json:"iam_groups"` // Names of IAM groups that generated IAM users will be added to
|
||||
InvalidData string `json:"invalid_data,omitempty"` // Invalid role data. Exists to support converting the legacy role data into the new format
|
||||
ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse
|
||||
Version int `json:"version"` // Version number of the role format
|
||||
|
@ -483,6 +501,7 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} {
|
|||
"policy_arns": r.PolicyArns,
|
||||
"role_arns": r.RoleArns,
|
||||
"policy_document": r.PolicyDocument,
|
||||
"iam_groups": r.IAMGroups,
|
||||
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
|
||||
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
|
||||
"user_path": r.UserPath,
|
||||
|
|
|
@ -126,9 +126,9 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
|
|||
case !strutil.StrListContains(role.RoleArns, roleArn):
|
||||
return logical.ErrorResponse(fmt.Sprintf("role_arn %q not in allowed role arns for Vault role %q", roleArn, roleName)), nil
|
||||
}
|
||||
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, ttl)
|
||||
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl)
|
||||
case federationTokenCred:
|
||||
return b.getFederationToken(ctx, req.Storage, req.DisplayName, roleName, role.PolicyDocument, role.PolicyArns, ttl)
|
||||
return b.getFederationToken(ctx, req.Storage, req.DisplayName, roleName, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl)
|
||||
default:
|
||||
return logical.ErrorResponse(fmt.Sprintf("unknown credential_type: %q", credentialType)), nil
|
||||
}
|
||||
|
|
|
@ -67,7 +67,23 @@ func genUsername(displayName, policyName, userType string) (ret string, warning
|
|||
|
||||
func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
|
||||
displayName, policyName, policy string, policyARNs []string,
|
||||
lifeTimeInSeconds int64) (*logical.Response, error) {
|
||||
iamGroups []string, lifeTimeInSeconds int64) (*logical.Response, error) {
|
||||
|
||||
groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
if groupPolicies != nil {
|
||||
groupPolicies = append(groupPolicies, policy)
|
||||
policy, err = combinePolicyDocuments(groupPolicies...)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
}
|
||||
if len(groupPolicyARNs) > 0 {
|
||||
policyARNs = append(policyARNs, groupPolicyARNs...)
|
||||
}
|
||||
|
||||
stsClient, err := b.clientSTS(ctx, s)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
|
@ -91,14 +107,13 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
|
|||
// that by default; the behavior can be explicitly opted in to by associating the Vault role
|
||||
// with a policy ARN or document that allows the appropriate permissions.
|
||||
if policy == "" && len(policyARNs) == 0 {
|
||||
return logical.ErrorResponse(fmt.Sprintf("must specify at least one of policy_arns or policy_document with %s credential_type", federationTokenCred)), nil
|
||||
return logical.ErrorResponse("must specify at least one of policy_arns or policy_document with %s credential_type", federationTokenCred), nil
|
||||
}
|
||||
|
||||
tokenResp, err := stsClient.GetFederationToken(getTokenInput)
|
||||
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error generating STS keys: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error generating STS keys: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
|
||||
resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
|
||||
|
@ -126,7 +141,25 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
|
|||
|
||||
func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
|
||||
displayName, roleName, roleArn, policy string, policyARNs []string,
|
||||
lifeTimeInSeconds int64) (*logical.Response, error) {
|
||||
iamGroups []string, lifeTimeInSeconds int64) (*logical.Response, error) {
|
||||
|
||||
// grab any IAM group policies associated with the vault role, both inline
|
||||
// and managed
|
||||
groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
if len(groupPolicies) > 0 {
|
||||
groupPolicies = append(groupPolicies, policy)
|
||||
policy, err = combinePolicyDocuments(groupPolicies...)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
}
|
||||
if len(groupPolicyARNs) > 0 {
|
||||
policyARNs = append(policyARNs, groupPolicyARNs...)
|
||||
}
|
||||
|
||||
stsClient, err := b.clientSTS(ctx, s)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
|
@ -148,8 +181,7 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
|
|||
tokenResp, err := stsClient.AssumeRole(assumeRoleInput)
|
||||
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error assuming role: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error assuming role: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
|
||||
resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
|
||||
|
@ -217,8 +249,7 @@ func (b *backend) secretAccessKeysCreate(
|
|||
iamErr := errwrap.Wrapf("error creating IAM user: {{err}}", err)
|
||||
return nil, errwrap.Wrap(errwrap.Wrapf("failed to delete WAL entry: {{err}}", walErr), iamErr)
|
||||
}
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error creating IAM user: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error creating IAM user: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
|
||||
for _, arn := range role.PolicyArns {
|
||||
|
@ -228,8 +259,7 @@ func (b *backend) secretAccessKeysCreate(
|
|||
PolicyArn: aws.String(arn),
|
||||
})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error attaching user policy: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error attaching user policy: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -241,8 +271,18 @@ func (b *backend) secretAccessKeysCreate(
|
|||
PolicyDocument: aws.String(role.PolicyDocument),
|
||||
})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error putting user policy: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error putting user policy: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range role.IAMGroups {
|
||||
// Add user to IAM groups
|
||||
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
||||
UserName: aws.String(username),
|
||||
GroupName: aws.String(group),
|
||||
})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("Error adding user to group: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,8 +291,7 @@ func (b *backend) secretAccessKeysCreate(
|
|||
UserName: aws.String(username),
|
||||
})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error creating access keys: %s", err)), awsutil.CheckAWSError(err)
|
||||
return logical.ErrorResponse("Error creating access keys: %s", err), awsutil.CheckAWSError(err)
|
||||
}
|
||||
|
||||
// Remove the WAL entry, we succeeded! If we fail, we don't return
|
||||
|
|
|
@ -254,6 +254,13 @@ updated with the new attributes.
|
|||
user has. With `assumed_role` and `federation_token`, the policy document will
|
||||
act as a filter on what the credentials can do, similar to `policy_arns`.
|
||||
|
||||
- `iam_groups` `(list: [])` - A list of IAM group names. IAM users generated
|
||||
against this vault role will be added to these 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.
|
||||
|
||||
- `default_sts_ttl` `(string)` - The default TTL for STS credentials. When a TTL is not
|
||||
specified when STS credentials are requested, and a default TTL is specified
|
||||
on the role, then this default TTL will be used. Valid only when
|
||||
|
@ -313,6 +320,15 @@ Using an ARN:
|
|||
}
|
||||
```
|
||||
|
||||
Using groups:
|
||||
|
||||
```json
|
||||
{
|
||||
"credential_type": "assumed_role",
|
||||
"iam_groups": ["group1", "group2"]
|
||||
}
|
||||
```
|
||||
|
||||
## Read Role
|
||||
|
||||
This endpoint queries an existing role by the given name. If the role does not
|
||||
|
@ -348,7 +364,8 @@ For an inline IAM policy:
|
|||
"policy_document": "{\"Version\": \"...\"}",
|
||||
"policy_arns": [],
|
||||
"credential_types": ["assumed_role"],
|
||||
"role_arns": []
|
||||
"role_arns": [],
|
||||
"iam_groups": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -361,7 +378,22 @@ For a role ARN:
|
|||
"policy_document": "",
|
||||
"policy_arns": [],
|
||||
"credential_types": ["assumed_role"],
|
||||
"role_arns": ["arn:aws:iam::123456789012:role/example-role"]
|
||||
"role_arns": ["arn:aws:iam::123456789012:role/example-role"],
|
||||
"iam_groups": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For IAM groups:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"policy_document": "",
|
||||
"policy_arns": [],
|
||||
"credential_types": ["assumed_role"],
|
||||
"role_arns": [],
|
||||
"iam_groups": ["group1", "group2"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -98,11 +98,12 @@ management tool.
|
|||
document to the IAM user. Vault will then create an access key and secret
|
||||
key for the IAM user and return these credentials. You supply a
|
||||
user inline policy and/or provide references to an existing AWS policy's full
|
||||
ARN:
|
||||
ARN and/or a list of IAM groups:
|
||||
|
||||
```text
|
||||
$ vault write aws/roles/my-other-role \
|
||||
policy_arns=arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess,arn:aws:iam::aws:policy/IAMReadOnlyAccess \
|
||||
iam_groups=group1,group2 \
|
||||
credential_type=iam_user \
|
||||
policy_document=-<<EOF
|
||||
{
|
||||
|
@ -177,6 +178,7 @@ permissions Vault needs:
|
|||
"iam:ListGroupsForUser",
|
||||
"iam:ListUserPolicies",
|
||||
"iam:PutUserPolicy",
|
||||
"iam:AddUserToGroup",
|
||||
"iam:RemoveUserFromGroup"
|
||||
],
|
||||
"Resource": ["arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/vault-*"]
|
||||
|
@ -203,6 +205,7 @@ user, you can use a policy like:
|
|||
"iam:ListAttachedUserPolicies",
|
||||
"iam:ListGroupsForUser",
|
||||
"iam:ListUserPolicies",
|
||||
"iam:AddUserToGroup",
|
||||
"iam:RemoveUserFromGroup"
|
||||
],
|
||||
"Resource": ["arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/vault-*"]
|
||||
|
@ -254,8 +257,9 @@ An STS federation token inherits a set of permissions that are the combination
|
|||
3. The managed policy ARNs configured in the Vault role
|
||||
4. An implicit deny policy on IAM or STS operations.
|
||||
|
||||
Roles with a `credential_type` of `federation_token` can specify both a
|
||||
`policy_document` and `policy_arns` parameter in the Vault role.
|
||||
Roles with a `credential_type` of `federation_token` can specify one or more of
|
||||
the `policy_document`, `policy_arns`, and `iam_groups` parameters in the Vault
|
||||
role.
|
||||
|
||||
The `aws/config/root` credentials require IAM permissions for
|
||||
`sts:GetFederationToken` and the permissions to delegate to the STS
|
||||
|
@ -373,20 +377,22 @@ specify more than one IAM role ARN. If you do so, Vault clients can select which
|
|||
role ARN they would like to assume when retrieving credentials from that role.
|
||||
|
||||
Further, you can specify both a `policy_document` and `policy_arns` parameters;
|
||||
if specified, each acts as a
|
||||
filter on the IAM permissions granted to the assumed role. For an action to be
|
||||
if specified, each acts as a filter on the IAM permissions granted to the
|
||||
assumed role. If `iam_groups` is specified, the inline and attached policies for
|
||||
each IAM group will be added to the `policy_document` and `policy_arns`
|
||||
parameters, respectively, when calling [sts:AssumeRole]. For an action to be
|
||||
allowed, it must be permitted by both the IAM policy on the AWS role that is
|
||||
assumed, the `policy_document` specified on the Vault role (if specified), and
|
||||
the managed policies specified by the `policy_arns` parameter. (The
|
||||
`policy_document` parameter is passed in as the `Policy` parameter to the
|
||||
[sts:AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
|
||||
API call, while the `policy_arns` parameter is passed in as the `PolicyArns`
|
||||
parameter to the same call.)
|
||||
[sts:AssumeRole] API call, while the `policy_arns` parameter is passed in as the
|
||||
`PolicyArns` parameter to the same call.)
|
||||
|
||||
Note: When multiple `role_arns` are specified, clients requesting credentials
|
||||
can specify any of the role ARNs that are defined on the Vault role in order to
|
||||
retrieve credentials. However, when a `policy_document` is specified, that will
|
||||
apply to ALL role credentials retrieved from AWS.
|
||||
retrieve credentials. However, when `policy_document`, `policy_arns`, or
|
||||
`iam_groups` are specified, that will apply to ALL role credentials retrieved
|
||||
from AWS.
|
||||
|
||||
Let's create a "deploy" policy using the arn of our role to assume:
|
||||
|
||||
|
@ -410,6 +416,8 @@ secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX
|
|||
security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX
|
||||
```
|
||||
|
||||
[sts:AssumeRole]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dynamic IAM user errors
|
||||
|
|
Loading…
Reference in New Issue