blob: 5814031ed29b978b3ad43f3fc146f4d77055ac08 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tpgiamresource
import (
"errors"
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"google.golang.org/api/cloudresourcemanager/v1"
)
var iamBindingSchema = map[string]*schema.Schema{
"role": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"members": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
DiffSuppressFunc: tpgresource.CaseDiffSuppress,
ValidateFunc: validateIAMMember,
},
Set: func(v interface{}) int {
return schema.HashString(strings.ToLower(v.(string)))
},
},
"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,
},
},
},
},
"etag": {
Type: schema.TypeString,
Computed: true,
},
}
func ResourceIamBinding(parentSpecificSchema map[string]*schema.Schema, newUpdaterFunc NewResourceIamUpdaterFunc, resourceIdParser ResourceIdParserFunc, options ...func(*IamSettings)) *schema.Resource {
settings := NewIamSettings(options...)
return &schema.Resource{
Create: resourceIamBindingCreateUpdate(newUpdaterFunc, settings.EnableBatching),
Read: resourceIamBindingRead(newUpdaterFunc),
Update: resourceIamBindingCreateUpdate(newUpdaterFunc, settings.EnableBatching),
Delete: resourceIamBindingDelete(newUpdaterFunc, settings.EnableBatching),
// if non-empty, this will be used to send a deprecation message when the
// resource is used.
DeprecationMessage: settings.DeprecationMessage,
Schema: tpgresource.MergeSchemas(iamBindingSchema, parentSpecificSchema),
Importer: &schema.ResourceImporter{
State: iamBindingImport(newUpdaterFunc, resourceIdParser),
},
UseJSONNumber: true,
}
}
func resourceIamBindingCreateUpdate(newUpdaterFunc NewResourceIamUpdaterFunc, enableBatching bool) func(*schema.ResourceData, interface{}) error {
return func(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
updater, err := newUpdaterFunc(d, config)
if err != nil {
return err
}
binding := getResourceIamBinding(d)
modifyF := func(ep *cloudresourcemanager.Policy) error {
cleaned := filterBindingsWithRoleAndCondition(ep.Bindings, binding.Role, binding.Condition)
ep.Bindings = append(cleaned, binding)
ep.Version = IamPolicyVersion
return nil
}
if enableBatching {
err = BatchRequestModifyIamPolicy(updater, modifyF, config, fmt.Sprintf(
"Set IAM Binding for role %q on %q", binding.Role, updater.DescribeResource()))
} else {
err = iamPolicyReadModifyWrite(updater, modifyF)
}
if err != nil {
return err
}
d.SetId(updater.GetResourceId() + "/" + binding.Role)
if k := conditionKeyFromCondition(binding.Condition); !k.Empty() {
d.SetId(d.Id() + "/" + k.String())
}
return resourceIamBindingRead(newUpdaterFunc)(d, meta)
}
}
func resourceIamBindingRead(newUpdaterFunc NewResourceIamUpdaterFunc) schema.ReadFunc {
return func(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
updater, err := newUpdaterFunc(d, config)
if err != nil {
return err
}
eBinding := getResourceIamBinding(d)
eCondition := conditionKeyFromCondition(eBinding.Condition)
p, err := iamPolicyReadWithRetry(updater)
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Resource %q with IAM Binding (Role %q)", updater.DescribeResource(), eBinding.Role))
}
log.Print(spew.Sprintf("[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 && conditionKeyFromCondition(b.Condition) == eCondition {
binding = b
break
}
}
if binding == nil {
log.Printf("[WARNING] Binding for role %q not found, assuming it has no members. If you expected existing members bound for this role, make sure your role is correctly formatted.", eBinding.Role)
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())
if err := d.Set("role", eBinding.Role); err != nil {
return fmt.Errorf("Error setting role: %s", err)
}
if err := d.Set("members", nil); err != nil {
return fmt.Errorf("Error setting members: %s", err)
}
return nil
} else {
if err := d.Set("role", binding.Role); err != nil {
return fmt.Errorf("Error setting role: %s", err)
}
if err := d.Set("members", binding.Members); err != nil {
return fmt.Errorf("Error setting members: %s", err)
}
if err := d.Set("condition", FlattenIamCondition(binding.Condition)); err != nil {
return fmt.Errorf("Error setting condition: %s", err)
}
}
if err := d.Set("etag", p.Etag); err != nil {
return fmt.Errorf("Error setting etag: %s", err)
}
return nil
}
}
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.(*transport_tpg.Config)
s := strings.Fields(d.Id())
var id, role string
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:], " ")
}
// Set the ID only to the first part so all IAM types can share the same ResourceIdParserFunc.
d.SetId(id)
if err := d.Set("role", role); err != nil {
return nil, fmt.Errorf("Error setting role: %s", err)
}
err := resourceIdParser(d, config)
if err != nil {
return nil, err
}
// 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)
// 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 {
if err := d.Set("condition", FlattenIamCondition(binding.Condition)); err != nil {
return nil, fmt.Errorf("Error setting condition: %s", err)
}
if k := conditionKeyFromCondition(binding.Condition); !k.Empty() {
d.SetId(d.Id() + "/" + k.String())
}
}
// 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
// to order this list in a useful way. In the event of a complex set of bindings, the user
// will have a terribly confusing set of imported resources and no way to know what matches
// up to what. And since the only users who will do a terraform import on their IAM bindings
// are users who aren't too familiar with Google Cloud IAM (because a "create" for bindings or
// members is idempotent), it's reasonable to expect that the user will be very alarmed by the
// plan that terraform will output which mentions destroying a dozen-plus IAM bindings. With
// that in mind, we return only the binding that matters.
return []*schema.ResourceData{d}, nil
}
}
func resourceIamBindingDelete(newUpdaterFunc NewResourceIamUpdaterFunc, enableBatching bool) schema.DeleteFunc {
return func(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
updater, err := newUpdaterFunc(d, config)
if err != nil {
return err
}
binding := getResourceIamBinding(d)
modifyF := func(p *cloudresourcemanager.Policy) error {
p.Bindings = filterBindingsWithRoleAndCondition(p.Bindings, binding.Role, binding.Condition)
return nil
}
if enableBatching {
err = BatchRequestModifyIamPolicy(updater, modifyF, config, fmt.Sprintf(
"Delete IAM Binding for role %q on %q", binding.Role, updater.DescribeResource()))
} else {
err = iamPolicyReadModifyWrite(updater, modifyF)
}
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Resource %q for IAM binding with role %q", updater.DescribeResource(), binding.Role))
}
return resourceIamBindingRead(newUpdaterFunc)(d, meta)
}
}
func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding {
members := d.Get("members").(*schema.Set).List()
b := &cloudresourcemanager.Binding{
Members: tpgresource.ConvertStringArr(members),
Role: d.Get("role").(string),
}
if c := ExpandIamCondition(d.Get("condition")); c != nil {
b.Condition = c
}
return b
}
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{"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,
},
}
}