provider/aws: aws_iam_policy_document data source

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.
This commit is contained in:
James Nugent 2016-05-24 19:50:35 -06:00
parent adee6c8bed
commit c91d62fda0
6 changed files with 605 additions and 2 deletions

View File

@ -0,0 +1,210 @@
package aws
import (
"encoding/json"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"strconv"
)
var dataSourceAwsIamPolicyDocumentVarReplacer = 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] = dataSourceAwsIamPolicyDocumentVarReplacer.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,
},
},
},
},
}
}

View File

@ -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[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:::*"
]
}
]
}`

View File

@ -0,0 +1,74 @@
package aws
import (
"encoding/json"
)
type IAMPolicyDoc struct {
Id string `json:",omitempty"`
Version string `json:",omitempty"`
Statements []*IAMPolicyStatement `json:"Statement"`
}
type IAMPolicyStatement struct {
Sid string `json:",omitempty"`
Effect string `json:",omitempty"`
Actions []string `json:"Action,omitempty"`
NotActions []string `json:"NotAction,omitempty"`
Resources []string `json:"Resource,omitempty"`
NotResources []string `json:"NotResource,omitempty"`
Principals IAMPolicyStatementPrincipalSet `json:"Principal,omitempty"`
NotPrincipals IAMPolicyStatementPrincipalSet `json:"NotPrincipal,omitempty"`
Conditions IAMPolicyStatementConditionSet `json:"Condition,omitempty"`
}
type IAMPolicyStatementPrincipal struct {
Type string
Identifiers []string
}
type IAMPolicyStatementCondition struct {
Test string
Variable string
Values []string
}
type IAMPolicyStatementPrincipalSet []IAMPolicyStatementPrincipal
type IAMPolicyStatementConditionSet []IAMPolicyStatementCondition
func (ps IAMPolicyStatementPrincipalSet) MarshalJSON() ([]byte, error) {
raw := map[string][]string{}
for _, p := range ps {
if _, ok := raw[p.Type]; !ok {
raw[p.Type] = make([]string, 0, len(p.Identifiers))
}
raw[p.Type] = append(raw[p.Type], p.Identifiers...)
}
return json.Marshal(&raw)
}
func (cs IAMPolicyStatementConditionSet) MarshalJSON() ([]byte, error) {
raw := map[string]map[string][]string{}
for _, c := range cs {
if _, ok := raw[c.Test]; !ok {
raw[c.Test] = map[string][]string{}
}
if _, ok := raw[c.Test][c.Variable]; !ok {
raw[c.Test][c.Variable] = make([]string, 0, len(c.Values))
}
raw[c.Test][c.Variable] = append(raw[c.Test][c.Variable], c.Values...)
}
return json.Marshal(&raw)
}
func iamPolicyDecodeConfigStringList(lI []interface{}) []string {
ret := make([]string, len(lI))
for i, vI := range lI {
ret[i] = vI.(string)
}
return ret
}

View File

@ -111,8 +111,9 @@ func Provider() terraform.ResourceProvider {
},
DataSourcesMap: map[string]*schema.Resource{
"aws_ami": dataSourceAwsAmi(),
"aws_availability_zones": dataSourceAwsAvailabilityZones(),
"aws_ami": dataSourceAwsAmi(),
"aws_availability_zones": dataSourceAwsAvailabilityZones(),
"aws_iam_policy_document": dataSourceAwsIamPolicyDocument(),
},
ResourcesMap: map[string]*schema.Resource{

View File

@ -0,0 +1,141 @@
---
layout: "aws"
page_title: "AWS: aws_iam_policy_document"
sidebar_current: "docs-aws-resource-iam-policy-document"
description: |-
Generates an IAM policy document in JSON format
---
# aws\_iam\_policy\_document
Generates an IAM policy document in JSON format.
This is a data source which can be used to construct a JSON representation of
an IAM policy document, for use with resources which expect policy documents,
such as the `aws_iam_policy` resource.
```
data "aws_iam_policy_document" "example" {
statement {
actions = [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation",
]
resources = [
"arn:aws:s3:::*",
]
}
statement {
actions = [
"s3:ListBucket",
]
resources = [
"arn:aws:s3:::${var.s3_bucket_name}",
]
condition {
test = "StringLike"
variable = "s3:prefix"
values = [
"",
"home/",
"home/&{aws:username}/",
]
}
}
statement {
actions = [
"s3:*",
]
resources = [
"arn:aws:s3:::${var.s3_bucket_name}/home/&{aws:username}",
"arn:aws:s3:::${var.s3_bucket_name}/home/&{aws:username}/*",
]
}
}
resource "aws_iam_policy" "example" {
name = "example_policy"
path = "/"
policy = "${data.aws_iam_policy.example.json}"
}
```
Using this data source to generate policy documents is *optional*. It is also
valid to use literal JSON strings within your configuration, or to use the
`file` interpolation function to read a raw JSON policy document from a file.
## Argument Reference
The following arguments are supported:
* `id` (Optional) - An ID for the policy document.
* `statement` (Required) - A nested configuration block (described below)
configuring one *statement* to be included in the policy document.
Each document configuration must have one or more `statement` blocks, which
each accept the following arguments:
* `id` (Optional) - An ID for the policy statement.
* `effect` (Optional) - Either "Allow" or "Deny", to specify whether this
statement allows or denies the given actions. The default is "Allow".
* `actions` (Optional) - A list of actions that this statement either allows
or denies. For example, ``["ec2:RunInstances", "s3:*"]``.
* `not_actions` (Optional) - A list of actions that this statement does *not*
apply to. Used to apply a policy statement to all actions *except* those
listed.
* `resources` (Optional) - A list of resource ARNs that this statement applies
to.
* `not_resources` (Optional) - A list of resource ARNs that this statement
does *not* apply to. Used to apply a policy statement to all resources
*except* those listed.
* `principals` (Optional) - A nested configuration block (described below)
specifying a resource (or resource pattern) to which this statement applies.
* `not_principals` (Optional) - Like `principals` except gives resources that
the statement does *not* apply to.
* `condition` (Optional) - A nested configuration block (described below)
that defines a further, possibly-service-specific condition that constrains
whether this statement applies.
Each policy may have either zero or more `principals` blocks or zero or more
`not_principals` blocks, both of which each accept the following arguments:
* `type` (Required) The type of principal. For AWS accounts this is "AWS".
* `identifiers` (Required) List of identifiers for principals. When `type`
is "AWS", these are IAM user or role ARNs.
Each policy statement may have zero or more `condition` blocks, which each
accept the following arguments:
* `test` (Required) The name of the
[IAM condition type](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#AccessPolicyLanguage_ConditionType)
to evaluate.
* `variable` (Required) The name of a
[Context Variable](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#AvailableKeys)
to apply the condition to. Context variables may either be standard AWS
variables starting with `aws:`, or service-specific variables prefixed with
the service name.
* `values` (Required) The values to evaluate the condition against. If multiple
values are provided, the condition matches if at least one of them applies.
(That is, the tests are combined with the "OR" boolean operation.)
When multiple `condition` blocks are provided, they must *all* evaluate to true
for the policy statement to apply. (In other words, the conditions are combined
with the "AND" boolean operation.)
## Context Variable Interpolation
The IAM policy document format allows context variables to be interpolated
into various strings within a statement. The native IAM policy document format
uses `${...}`-style syntax that is in conflict with Terraform's interpolation
syntax, so this data source instead uses `&{...}` syntax for interpolations that
should be processed by AWS rather than by Terraform.
## Attributes Reference
The following attribute is exported:
* `json` - The above arguments serialized as a standard JSON policy document.

View File

@ -19,6 +19,10 @@
<li<%= sidebar_current("docs-aws-datasource-availability-zones") %>>
<a href="/docs/providers/aws/d/availability_zones.html">aws_availability_zones</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-iam-policy-document") %>>
<a href="/docs/providers/aws/d/iam_policy_document.html">aws_iam_policy_document</a>
</li>
</ul>
</li>
@ -402,6 +406,7 @@
<li<%= sidebar_current("docs-aws-resource-iam-policy") %>>
<a href="/docs/providers/aws/r/iam_policy.html">aws_iam_policy</a>
</li>
<li<%= sidebar_current("docs-aws-resource-iam-policy-attachment") %>>
<a href="/docs/providers/aws/r/iam_policy_attachment.html">aws_iam_policy_attachment</a>
</li>