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

Add IAM Conditions support; enable it in service account IAM #2372

Merged
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
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