Skip to content

Commit

Permalink
Add IAM Conditions support; enable it in service account IAM (#2372)
Browse files Browse the repository at this point in the history
Merged PR #2372.
  • Loading branch information
danawillow authored and modular-magician committed Oct 28, 2019
1 parent 109ee4f commit 8ba5798
Show file tree
Hide file tree
Showing 11 changed files with 983 additions and 116 deletions.
2 changes: 1 addition & 1 deletion build/terraform
2 changes: 1 addition & 1 deletion build/terraform-beta
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% autogen_exception -%>
package google

import (
Expand Down Expand Up @@ -41,6 +42,29 @@ func dataSourceGoogleIamPolicy() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
<% unless version == 'ga' -%>
"condition": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"expression": {
Type: schema.TypeString,
Required: true,
},
"title": {
Type: schema.TypeString,
Required: true,
},
"description": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
<% end -%>
},
},
},
Expand Down Expand Up @@ -99,13 +123,19 @@ func dataSourceGoogleIamPolicyRead(d *schema.ResourceData, meta interface{}) err
for i, v := range bset.List() {
binding := v.(map[string]interface{})
members := convertStringSet(binding["members"].(*schema.Set))
<% unless version == 'ga' -%>
condition := expandIamCondition(binding["condition"])
<% end -%>

// Sort members to get simpler diffs as it's what the API does
sort.Strings(members)

policy.Bindings[i] = &cloudresourcemanager.Binding{
Role: binding["role"].(string),
Members: members,
Role: binding["role"].(string),
Members: members,
<% unless version == 'ga' -%>
Condition: condition,
<% end -%>
}
}

Expand Down
154 changes: 140 additions & 14 deletions third_party/terraform/resources/resource_iam_binding.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,33 @@ var iamBindingSchema = map[string]*schema.Schema{
return schema.HashString(strings.ToLower(v.(string)))
},
},
<% unless version == 'ga' -%>
"condition": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"expression": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"title": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
},
},
<% end -%>
"etag": {
Type: schema.TypeString,
Computed: true,
Expand All @@ -47,7 +74,7 @@ func ResourceIamBindingWithBatching(parentSpecificSchema map[string]*schema.Sche
Delete: resourceIamBindingDelete(newUpdaterFunc, enableBatching),
Schema: mergeSchemas(iamBindingSchema, parentSpecificSchema),
Importer: &schema.ResourceImporter{
State: iamBindingImport(resourceIdParser),
State: iamBindingImport(newUpdaterFunc, resourceIdParser),
},
}
}
Expand All @@ -62,8 +89,11 @@ func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc, en

binding := getResourceIamBinding(d)
modifyF := func(ep *cloudresourcemanager.Policy) error {
cleaned := removeAllBindingsWithRole(ep.Bindings, binding.Role)
cleaned := filterBindingsWithRoleAndCondition(ep.Bindings, binding.Role, binding.Condition)
ep.Bindings = append(cleaned, binding)
<% unless version == 'ga' -%>
ep.Version = iamPolicyVersion
<% end -%>
return nil
}

Expand All @@ -76,7 +106,13 @@ func resourceIamBindingCreateUpdate(newUpdaterFunc newResourceIamUpdaterFunc, en
if err != nil {
return err
}

d.SetId(updater.GetResourceId() + "/" + binding.Role)
<% unless version == 'ga' -%>
if k := conditionKeyFromCondition(binding.Condition); !k.Empty() {
d.SetId(d.Id() + "/" + k.String())
}
<% end -%>
return resourceIamBindingRead(newUpdaterFunc)(d, meta)
}
}
Expand All @@ -90,46 +126,67 @@ func resourceIamBindingRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.Rea
}

eBinding := getResourceIamBinding(d)
eCondition := conditionKeyFromCondition(eBinding.Condition)
p, err := iamPolicyReadWithRetry(updater)
if err != nil {
return handleNotFoundError(err, d, fmt.Sprintf("Resource %q with IAM Binding (Role %q)", updater.DescribeResource(), eBinding.Role))
}
log.Printf("[DEBUG]: Retrieved policy for %s: %+v", updater.DescribeResource(), p)
log.Printf("[DEBUG] Retrieved policy for %s: %+v", updater.DescribeResource(), p)
log.Printf("[DEBUG] Looking for binding with role %q and condition %+v", eBinding.Role, eCondition)

var binding *cloudresourcemanager.Binding
for _, b := range p.Bindings {
if b.Role != eBinding.Role {
continue
if b.Role == eBinding.Role && conditionKeyFromCondition(b.Condition) == eCondition {
binding = b
break
}
binding = b
break
}

if binding == nil {
log.Printf("[DEBUG]: Binding for role %q not found in policy for %s, assuming it has no members.", eBinding.Role, updater.DescribeResource())
log.Printf("[DEBUG] Binding for role %q and condition %+v not found in policy for %s, assuming it has no members.", eBinding.Role, eCondition, updater.DescribeResource())
d.Set("role", eBinding.Role)
d.Set("members", nil)
return nil
} else {
d.Set("role", binding.Role)
d.Set("members", binding.Members)
<% unless version == 'ga' -%>
d.Set("condition", flattenIamCondition(binding.Condition))
<% end -%>
}
d.Set("etag", p.Etag)
return nil
}
}

func iamBindingImport(resourceIdParser resourceIdParserFunc) schema.StateFunc {
func iamBindingImport(newUpdaterFunc newResourceIamUpdaterFunc, resourceIdParser resourceIdParserFunc) schema.StateFunc {
return func(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
if resourceIdParser == nil {
return nil, errors.New("Import not supported for this IAM resource.")
}
config := m.(*Config)
s := strings.Fields(d.Id())
var id, role string
<% if version == 'ga' -%>
if len(s) != 2 {
d.SetId("")
return nil, fmt.Errorf("Wrong number of parts to Binding id %s; expected 'resource_name role'.", s)
}
id, role := s[0], s[1]
id, role = s[0], s[1]
<% else -%>
if len(s) < 2 {
d.SetId("")
return nil, fmt.Errorf("Wrong number of parts to Binding id %s; expected 'resource_name role [condition_title]'.", s)
}

var conditionTitle string
if len(s) == 2 {
id, role = s[0], s[1]
} else {
// condition titles can have any characters in them, so re-join the split string
id, role, conditionTitle = s[0], s[1], strings.Join(s[2:], " ")
}
<% end -%>

// Set the ID only to the first part so all IAM types can share the same resourceIdParserFunc.
d.SetId(id)
Expand All @@ -142,6 +199,40 @@ func iamBindingImport(resourceIdParser resourceIdParserFunc) schema.StateFunc {
// Set the ID again so that the ID matches the ID it would have if it had been created via TF.
// Use the current ID in case it changed in the resourceIdParserFunc.
d.SetId(d.Id() + "/" + role)

<% unless version == 'ga' -%>
// Since condition titles can have any character in them, we can't separate them from any other
// field the user might set in import (like the condition description and expression). So, we
// have the user just specify the title and then read the upstream policy to set the full
// condition. We can't rely on the read fn to do this for us because it looks for a match of the
// full condition.
updater, err := newUpdaterFunc(d, config)
if err != nil {
return nil, err
}
p, err := iamPolicyReadWithRetry(updater)
if err != nil {
return nil, err
}
var binding *cloudresourcemanager.Binding
for _, b := range p.Bindings {
if (b.Role == role && conditionKeyFromCondition(b.Condition).Title == conditionTitle) {
if binding != nil {
return nil, fmt.Errorf("Cannot import IAM member with condition title %q, it matches multiple conditions", conditionTitle)
}
binding = b
}
}
if binding == nil {
return nil, fmt.Errorf("Cannot find binding for %q with role %q and condition title %q", updater.DescribeResource(), role, conditionTitle)
}

d.Set("condition", flattenIamCondition(binding.Condition))
if k := conditionKeyFromCondition(binding.Condition); !k.Empty() {
d.SetId(d.Id() + "/" + k.String())
}
<% end -%>

// It is possible to return multiple bindings, since we can learn about all the bindings
// for this resource here. Unfortunately, `terraform import` has some messy behavior here -
// there's no way to know at this point which resource is being imported, so it's not possible
Expand All @@ -166,7 +257,7 @@ func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc, enableBa

binding := getResourceIamBinding(d)
modifyF := func(p *cloudresourcemanager.Policy) error {
p.Bindings = removeAllBindingsWithRole(p.Bindings, binding.Role)
p.Bindings = filterBindingsWithRoleAndCondition(p.Bindings, binding.Role, binding.Condition)
return nil
}

Expand All @@ -186,8 +277,43 @@ func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc, enableBa

func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding {
members := d.Get("members").(*schema.Set).List()
return &cloudresourcemanager.Binding{
Members: convertStringArr(members),
Role: d.Get("role").(string),
b := &cloudresourcemanager.Binding{
Members: convertStringArr(members),
Role: d.Get("role").(string),
}
<% unless version == 'ga' -%>
if c := expandIamCondition(d.Get("condition")); c != nil {
b.Condition = c
}
<% end -%>
return b
}

<% unless version == 'ga' -%>
func expandIamCondition(v interface{}) *cloudresourcemanager.Expr {
l := v.([]interface{})
if len(l) == 0 || l[0] == nil {
return nil
}
original := l[0].(map[string]interface{})
return &cloudresourcemanager.Expr{
Description: original["description"].(string),
Expression: original["expression"].(string),
Title: original["title"].(string),
ForceSendFields: []string{"Description", "Expression", "Title"},
}
}

func flattenIamCondition(condition *cloudresourcemanager.Expr) []map[string]interface{} {
if conditionKeyFromCondition(condition).Empty() {
return nil
}
return []map[string]interface{}{
{
"expression": condition.Expression,
"title": condition.Title,
"description": condition.Description,
},
}
}
<% end -%>
Loading

0 comments on commit 8ba5798

Please sign in to comment.