Skip to content

Commit

Permalink
provider/aws: aws_iam_policy_document data source
Browse files Browse the repository at this point in the history
This brings over the work done by @apparentlymart and @radeksimko in
PR #3124, and converts it into a data source for the AWS provider:

This commit adds a helper to construct IAM policy documents using
familiar Terraform concepts. It makes Terraform-style interpolations
easier and resolves the syntax conflict between Terraform interpolations
and IAM policy variables by changing the latter to use &{...} for its
interpolations.

Its use is completely optional and users are free to go on using literal
heredocs, file interpolations or whatever else; this just adds another
option that fits more naturally into a Terraform config.
  • Loading branch information
jen20 committed May 27, 2016
1 parent 3355c15 commit 2c32037
Show file tree
Hide file tree
Showing 6 changed files with 604 additions and 1 deletion.
210 changes: 210 additions & 0 deletions builtin/providers/aws/data_source_aws_iam_policy_document.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package aws

import (
"encoding/json"
"strings"

"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"strconv"
)

var resourceAwsIamPolicyDocumentVarReplacer = strings.NewReplacer("&{", "${")

func dataSourceAwsIamPolicyDocument() *schema.Resource {
setOfString := &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
}

return &schema.Resource{
Read: dataSourceAwsIamPolicyDocumentRead,

Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"statement": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"effect": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "Allow",
},
"actions": setOfString,
"not_actions": setOfString,
"resources": setOfString,
"not_resources": setOfString,
"principals": dataSourceAwsIamPolicyPrincipalSchema(),
"not_principals": dataSourceAwsIamPolicyPrincipalSchema(),
"condition": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"test": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"variable": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"values": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
},
},
},
"json": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

func dataSourceAwsIamPolicyDocumentRead(d *schema.ResourceData, meta interface{}) error {
doc := &IAMPolicyDoc{
Version: "2012-10-17",
}

if policyId, hasPolicyId := d.GetOk("id"); hasPolicyId {
doc.Id = policyId.(string)
}

var cfgStmts = d.Get("statement").(*schema.Set).List()
stmts := make([]*IAMPolicyStatement, len(cfgStmts))
doc.Statements = stmts
for i, stmtI := range cfgStmts {
cfgStmt := stmtI.(map[string]interface{})
stmt := &IAMPolicyStatement{
Effect: cfgStmt["effect"].(string),
}

if actions := cfgStmt["actions"].(*schema.Set).List(); len(actions) > 0 {
stmt.Actions = iamPolicyDecodeConfigStringList(actions)
}
if actions := cfgStmt["not_actions"].(*schema.Set).List(); len(actions) > 0 {
stmt.NotActions = iamPolicyDecodeConfigStringList(actions)
}

if resources := cfgStmt["resources"].(*schema.Set).List(); len(resources) > 0 {
stmt.Resources = dataSourceAwsIamPolicyDocumentReplaceVarsInList(
iamPolicyDecodeConfigStringList(resources),
)
}
if resources := cfgStmt["not_resources"].(*schema.Set).List(); len(resources) > 0 {
stmt.NotResources = dataSourceAwsIamPolicyDocumentReplaceVarsInList(
iamPolicyDecodeConfigStringList(resources),
)
}

if principals := cfgStmt["principals"].(*schema.Set).List(); len(principals) > 0 {
stmt.Principals = dataSourceAwsIamPolicyDocumentMakePrincipals(principals)
}

if principals := cfgStmt["not_principals"].(*schema.Set).List(); len(principals) > 0 {
stmt.NotPrincipals = dataSourceAwsIamPolicyDocumentMakePrincipals(principals)
}

if conditions := cfgStmt["condition"].(*schema.Set).List(); len(conditions) > 0 {
stmt.Conditions = dataSourceAwsIamPolicyDocumentMakeConditions(conditions)
}

stmts[i] = stmt
}

jsonDoc, err := json.MarshalIndent(doc, "", " ")
if err != nil {
// should never happen if the above code is correct
return err
}
jsonString := string(jsonDoc)

d.Set("json", jsonString)
d.SetId(strconv.Itoa(hashcode.String(jsonString)))

return nil
}

func dataSourceAwsIamPolicyDocumentReplaceVarsInList(in []string) []string {
out := make([]string, len(in))
for i, item := range in {
out[i] = resourceAwsIamPolicyDocumentVarReplacer.Replace(item)
}
return out
}

func dataSourceAwsIamPolicyDocumentMakeConditions(in []interface{}) IAMPolicyStatementConditionSet {
out := make([]IAMPolicyStatementCondition, len(in))
for i, itemI := range in {
item := itemI.(map[string]interface{})
out[i] = IAMPolicyStatementCondition{
Test: item["test"].(string),
Variable: item["variable"].(string),
Values: dataSourceAwsIamPolicyDocumentReplaceVarsInList(
iamPolicyDecodeConfigStringList(
item["values"].(*schema.Set).List(),
),
),
}
}
return IAMPolicyStatementConditionSet(out)
}

func dataSourceAwsIamPolicyDocumentMakePrincipals(in []interface{}) IAMPolicyStatementPrincipalSet {
out := make([]IAMPolicyStatementPrincipal, len(in))
for i, itemI := range in {
item := itemI.(map[string]interface{})
out[i] = IAMPolicyStatementPrincipal{
Type: item["type"].(string),
Identifiers: dataSourceAwsIamPolicyDocumentReplaceVarsInList(
iamPolicyDecodeConfigStringList(
item["identifiers"].(*schema.Set).List(),
),
),
}
}
return IAMPolicyStatementPrincipalSet(out)
}

func dataSourceAwsIamPolicyPrincipalSchema() *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"identifiers": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
}
}
172 changes: 172 additions & 0 deletions builtin/providers/aws/data_source_aws_iam_policy_document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package aws

import (
"testing"

"fmt"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAWSIAMPolicyDocument(t *testing.T) {
// This really ought to be able to be a unit test rather than an
// acceptance test, but just instantiating the AWS provider requires
// some AWS API calls, and so this needs valid AWS credentials to work.
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSIAMPolicyDocumentConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue(
"data.aws_iam_policy_document.test",
"json",
testAccAWSIAMPolicyDocumentExpectedJSON,
),
),
},
},
})
}

func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[id]
if !ok {
return fmt.Errorf("Not found: %s", id)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}

v := rs.Primary.Attributes["output."+name]
if v != value {
return fmt.Errorf(
"Value for %s is %s, not %s", name, v, value)
}

return nil
}
}

var testAccAWSIAMPolicyDocumentConfig = `
data "aws_iam_policy_document" "test" {
statement {
actions = [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation",
]
resources = [
"arn:aws:s3:::*",
]
}
statement {
actions = [
"s3:ListBucket",
]
resources = [
"arn:aws:s3:::foo",
]
condition {
test = "StringLike"
variable = "s3:prefix"
values = [
"",
"home/",
"home/&{aws:username}/",
]
}
not_principals {
type = "AWS"
identifiers = ["arn:blahblah:example"]
}
}
statement {
actions = [
"s3:*",
]
resources = [
"arn:aws:s3:::foo/home/&{aws:username}",
"arn:aws:s3:::foo/home/&{aws:username}/*",
]
principals {
type = "AWS"
identifiers = ["arn:blahblah:example"]
}
}
statement {
effect = "Deny"
not_actions = ["s3:*"]
not_resources = ["arn:aws:s3:::*"]
}
}
`

var testAccAWSIAMPolicyDocumentExpectedJSON = `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListAllMyBuckets"
],
"Resource": [
"arn:aws:s3:::*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::foo"
],
"NotPrincipal": {
"AWS": [
"arn:blahblah:example"
]
},
"Condition": {
"StringLike": {
"s3:prefix": [
"",
"home/",
"home/${aws:username}/"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::foo/home/${aws:username}/*",
"arn:aws:s3:::foo/home/${aws:username}"
],
"Principal": {
"AWS": [
"arn:blahblah:example"
]
}
},
{
"Effect": "Deny",
"NotAction": [
"s3:*"
],
"NotResource": [
"arn:aws:s3:::*"
]
}
]
}`
Loading

0 comments on commit 2c32037

Please sign in to comment.