From 34f48b3e06a105da52c7208cfbee2b43c8e09a7a Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Sun, 19 Apr 2015 11:54:45 +0100 Subject: [PATCH 1/2] Add schema.ConflictsWith[] - this will allow defining logically conflicting attributes --- helper/schema/schema.go | 43 +++++++++++ helper/schema/schema_test.go | 133 ++++++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 33c8b8310..04b242bbe 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -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 @@ -436,6 +439,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) @@ -913,9 +932,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{}, diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index c1233ae50..e71e7777f 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -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{ @@ -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) } } @@ -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 { From 150fd00932690bebe1f757f165a648cfb56a8107 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Sun, 19 Apr 2015 23:54:42 +0100 Subject: [PATCH 2/2] AWS account ID protection added --- builtin/providers/aws/config.go | 44 +++++++++++++++++++ builtin/providers/aws/provider.go | 29 ++++++++++++ helper/schema/schema.go | 2 + .../docs/providers/aws/index.html.markdown | 11 +++++ 4 files changed, 86 insertions(+) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index d9e076841..9c6599258 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/multierror" @@ -21,6 +22,9 @@ type Config struct { SecretKey string Token string Region string + + AllowedAccountIds []interface{} + ForbiddenAccountIds []interface{} } type AWSClient struct { @@ -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) @@ -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 +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 50596512e..75f0fda35 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -1,6 +1,7 @@ package aws import ( + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) @@ -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{ @@ -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() } diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 04b242bbe..fd53aa603 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -384,6 +384,8 @@ func (m schemaMap) Input( fallthrough case TypeFloat: fallthrough + case TypeSet: + continue case TypeString: value, err = m.inputString(input, k, v) default: diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown index dc5fe92b9..57619f11f 100644 --- a/website/source/docs/providers/aws/index.html.markdown +++ b/website/source/docs/providers/aws/index.html.markdown @@ -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 @@ -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.