Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws: Allow defining blacklist/whitelist of account IDs #1595

Merged
merged 2 commits into from
Apr 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aws
import (
"fmt"
"log"
"strings"

"github.com/hashicorp/terraform/helper/multierror"

Expand All @@ -21,6 +22,9 @@ type Config struct {
SecretKey string
Token string
Region string

AllowedAccountIds []interface{}
ForbiddenAccountIds []interface{}
}

type AWSClient struct {
Expand Down Expand Up @@ -71,6 +75,12 @@ func (c *Config) Client() (interface{}, error) {

log.Println("[INFO] Initializing IAM Connection")
client.iamconn = iam.New(awsConfig)

err := c.ValidateAccountId(client.iamconn)
if err != nil {
errs = append(errs, err)
}

log.Println("[INFO] Initializing AutoScaling connection")
client.autoscalingconn = autoscaling.New(awsConfig)

Expand Down Expand Up @@ -109,3 +119,37 @@ func (c *Config) ValidateRegion() error {
}
return fmt.Errorf("Not a valid region: %s", c.Region)
}

func (c *Config) ValidateAccountId(iamconn *iam.IAM) error {
if c.AllowedAccountIds == nil && c.ForbiddenAccountIds == nil {
return nil
}

log.Printf("[INFO] Validating account ID")

out, err := iamconn.GetUser(nil)
if err != nil {
return fmt.Errorf("Failed getting account ID from IAM: %s", err)
}

account_id := strings.Split(*out.User.ARN, ":")[4]

if c.ForbiddenAccountIds != nil {
for _, id := range c.ForbiddenAccountIds {
if id == account_id {
return fmt.Errorf("Forbidden account ID (%s)", id)
}
}
}

if c.AllowedAccountIds != nil {
for _, id := range c.AllowedAccountIds {
if id == account_id {
return nil
}
}
return fmt.Errorf("Account ID not allowed (%s)", account_id)
}

return nil
}
29 changes: 29 additions & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package aws

import (
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
Expand Down Expand Up @@ -42,6 +43,26 @@ func Provider() terraform.ResourceProvider {
Description: descriptions["region"],
InputDefault: "us-east-1",
},

"allowed_account_ids": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ConflictsWith: []string{"forbidden_account_ids"},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},

"forbidden_account_ids": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ConflictsWith: []string{"allowed_account_ids"},
Set: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
},

ResourcesMap: map[string]*schema.Resource{
Expand Down Expand Up @@ -97,5 +118,13 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
Region: d.Get("region").(string),
}

if v, ok := d.GetOk("allowed_account_ids"); ok {
config.AllowedAccountIds = v.(*schema.Set).List()
}

if v, ok := d.GetOk("forbidden_account_ids"); ok {
config.ForbiddenAccountIds = v.(*schema.Set).List()
}

return config.Client()
}
45 changes: 45 additions & 0 deletions helper/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ type Schema struct {
// NOTE: This currently does not work.
ComputedWhen []string

// ConflictsWith is a set of schema keys that conflict with this schema
ConflictsWith []string

// When Deprecated is set, this attribute is deprecated.
//
// A deprecated field still works, but will probably stop working in near
Expand Down Expand Up @@ -381,6 +384,8 @@ func (m schemaMap) Input(
fallthrough
case TypeFloat:
fallthrough
case TypeSet:
continue
case TypeString:
value, err = m.inputString(input, k, v)
default:
Expand Down Expand Up @@ -436,6 +441,22 @@ func (m schemaMap) InternalValidate() error {
return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k)
}

if len(v.ConflictsWith) > 0 && v.Required {
return fmt.Errorf("%s: ConflictsWith cannot be set with Required", k)
}

if len(v.ConflictsWith) > 0 {
for _, key := range v.ConflictsWith {
if m[key].Required {
return fmt.Errorf("%s: ConflictsWith cannot contain Required attribute (%s)", k, key)
}

if m[key].Computed || len(m[key].ComputedWhen) > 0 {
return fmt.Errorf("%s: ConflictsWith cannot contain Computed(When) attribute (%s)", k, key)
}
}
}

if v.Type == TypeList || v.Type == TypeSet {
if v.Elem == nil {
return fmt.Errorf("%s: Elem must be set for lists", k)
Expand Down Expand Up @@ -913,9 +934,33 @@ func (m schemaMap) validate(
"%q: this field cannot be set", k)}
}

err := m.validateConflictingAttributes(k, schema, c)
if err != nil {
return nil, []error{err}
}

return m.validateType(k, raw, schema, c)
}

func (m schemaMap) validateConflictingAttributes(
k string,
schema *Schema,
c *terraform.ResourceConfig) error {

if len(schema.ConflictsWith) == 0 {
return nil
}

for _, conflicting_key := range schema.ConflictsWith {
if value, ok := c.Get(conflicting_key); ok {
return fmt.Errorf(
"%q: conflicts with %s (%#v)", k, conflicting_key, value)
}
}

return nil
}

func (m schemaMap) validateList(
k string,
raw interface{},
Expand Down
133 changes: 132 additions & 1 deletion helper/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,66 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
true,
},

// Conflicting attributes cannot be required
{
map[string]*Schema{
"blacklist": &Schema{
Type: TypeBool,
Required: true,
},
"whitelist": &Schema{
Type: TypeBool,
Optional: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},

// Attribute with conflicts cannot be required
{
map[string]*Schema{
"whitelist": &Schema{
Type: TypeBool,
Required: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},

// ConflictsWith cannot be used w/ Computed
{
map[string]*Schema{
"blacklist": &Schema{
Type: TypeBool,
Computed: true,
},
"whitelist": &Schema{
Type: TypeBool,
Optional: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},

// ConflictsWith cannot be used w/ ComputedWhen
{
map[string]*Schema{
"blacklist": &Schema{
Type: TypeBool,
ComputedWhen: []string{"foor"},
},
"whitelist": &Schema{
Type: TypeBool,
Required: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},

// Sub-resource invalid
{
map[string]*Schema{
Expand Down Expand Up @@ -2594,7 +2654,10 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
for i, tc := range cases {
err := schemaMap(tc.In).InternalValidate()
if (err != nil) != tc.Err {
t.Fatalf("%d: bad: %s\n\n%#v", i, err, tc.In)
if tc.Err {
t.Fatalf("%d: Expected error did not occur:\n\n%#v", i, tc.In)
}
t.Fatalf("%d: Unexpected error occured:\n\n%#v", i, tc.In)
}
}

Expand Down Expand Up @@ -3103,6 +3166,74 @@ func TestSchemaMap_Validate(t *testing.T) {

Err: false,
},

"Conflicting attributes generate error": {
Schema: map[string]*Schema{
"whitelist": &Schema{
Type: TypeString,
Optional: true,
},
"blacklist": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"whitelist"},
},
},

Config: map[string]interface{}{
"whitelist": "white-val",
"blacklist": "black-val",
},

Err: true,
Errors: []error{
fmt.Errorf("\"blacklist\": conflicts with whitelist (\"white-val\")"),
},
},

"Required attribute & undefined conflicting optional are good": {
Schema: map[string]*Schema{
"required_att": &Schema{
Type: TypeString,
Required: true,
},
"optional_att": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"required_att"},
},
},

Config: map[string]interface{}{
"required_att": "required-val",
},

Err: false,
},

"Required conflicting attribute & defined optional generate error": {
Schema: map[string]*Schema{
"required_att": &Schema{
Type: TypeString,
Required: true,
},
"optional_att": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"required_att"},
},
},

Config: map[string]interface{}{
"required_att": "required-val",
"optional_att": "optional-val",
},

Err: true,
Errors: []error{
fmt.Errorf("\"optional_att\": conflicts with required_att (\"required-val\")"),
},
},
}

for tn, tc := range cases {
Expand Down
11 changes: 11 additions & 0 deletions website/source/docs/providers/aws/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ provider "aws" {
access_key = "${var.aws_access_key}"
secret_key = "${var.aws_secret_key}"
region = "us-east-1"

# Not run this in live account
forbidden_account_ids = ["1234567890"]
}

# Create a web server
Expand All @@ -43,5 +46,13 @@ The following arguments are supported in the `provider` block:
* `region` - (Required) This is the AWS region. It must be provided, but
it can also be sourced from the `AWS_DEFAULT_REGION` environment variables.

* `allowed_account_ids` - (Optional) List of allowed AWS account IDs (whitelist)
to prevent you mistakenly using a wrong one (and end up destroying live environment).
Conflicts with `forbidden_account_ids`.

* `forbidden_account_ids` - (Optional) List of forbidden AWS account IDs (blacklist)
to prevent you mistakenly using a wrong one (and end up destroying live environment).
Conflicts with `allowed_account_ids`.

In addition to the above parameters, the `AWS_SECURITY_TOKEN` environmental
variable can be set to set an MFA token.