blob: c8aebce588d6bf300740b1ce27d2ee828515bdd4 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tpgiamresource
import (
"errors"
"fmt"
"log"
"regexp"
"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"
)
func iamMemberCaseDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
isCaseSensitive := iamMemberIsCaseSensitive(old) || iamMemberIsCaseSensitive(new)
if isCaseSensitive {
return old == new
}
return tpgresource.CaseDiffSuppress(k, old, new, d)
}
func validateIAMMember(i interface{}, k string) ([]string, []error) {
v, ok := i.(string)
if !ok {
return nil, []error{fmt.Errorf("expected type of %s to be string", k)}
}
if matched, err := regexp.MatchString("^deleted", v); err != nil {
return nil, []error{fmt.Errorf("error validating %s: %v", k, err)}
} else if matched {
return nil, []error{fmt.Errorf("invalid value for %s (Terraform does not support IAM members for deleted principals)", k)}
}
if matched, err := regexp.MatchString("(.+:.+|projectOwners|projectReaders|projectWriters|allUsers|allAuthenticatedUsers)", v); err != nil {
return nil, []error{fmt.Errorf("error validating %s: %v", k, err)}
} else if !matched {
return nil, []error{fmt.Errorf("invalid value for %s (IAM members must have one of the values outlined here: https://cloud.google.com/billing/docs/reference/rest/v1/Policy#Binding)", k)}
}
return nil, nil
}
var IamMemberBaseSchema = map[string]*schema.Schema{
"role": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"member": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: iamMemberCaseDiffSuppress,
ValidateFunc: validateIAMMember,
},
"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 iamMemberImport(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, member string
if len(s) < 3 {
d.SetId("")
return nil, fmt.Errorf("Wrong number of parts to Member id %s; expected 'resource_name role member [condition_title]'.", s)
}
var conditionTitle string
if len(s) == 3 {
id, role, member = s[0], s[1], s[2]
} else {
// condition titles can have any characters in them, so re-join the split string
id, role, member, conditionTitle = s[0], s[1], s[2], strings.Join(s[3:], " ")
}
// 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)
}
if err := d.Set("member", normalizeIamMemberCasing(member)); err != nil {
return nil, fmt.Errorf("Error setting member: %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 + "/" + normalizeIamMemberCasing(member))
// Read the upstream policy so we can set 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 {
containsMember := false
for _, m := range b.Members {
if strings.ToLower(m) == strings.ToLower(member) {
containsMember = true
}
}
if !containsMember {
continue
}
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, member %q, and condition title %q", updater.DescribeResource(), role, member, conditionTitle)
}
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())
}
return []*schema.ResourceData{d}, nil
}
}
func ResourceIamMember(parentSpecificSchema map[string]*schema.Schema, newUpdaterFunc NewResourceIamUpdaterFunc, resourceIdParser ResourceIdParserFunc, options ...func(*IamSettings)) *schema.Resource {
settings := NewIamSettings(options...)
return &schema.Resource{
Create: resourceIamMemberCreate(newUpdaterFunc, settings.EnableBatching),
Read: resourceIamMemberRead(newUpdaterFunc),
Delete: resourceIamMemberDelete(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(IamMemberBaseSchema, parentSpecificSchema),
Importer: &schema.ResourceImporter{
State: iamMemberImport(newUpdaterFunc, resourceIdParser),
},
UseJSONNumber: true,
}
}
func getResourceIamMember(d *schema.ResourceData) *cloudresourcemanager.Binding {
b := &cloudresourcemanager.Binding{
Members: []string{d.Get("member").(string)},
Role: d.Get("role").(string),
}
if c := ExpandIamCondition(d.Get("condition")); c != nil {
b.Condition = c
}
return b
}
func resourceIamMemberCreate(newUpdaterFunc NewResourceIamUpdaterFunc, enableBatching bool) schema.CreateFunc {
return func(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
updater, err := newUpdaterFunc(d, config)
if err != nil {
return err
}
memberBind := getResourceIamMember(d)
modifyF := func(ep *cloudresourcemanager.Policy) error {
// Merge the bindings together
ep.Bindings = MergeBindings(append(ep.Bindings, memberBind))
ep.Version = IamPolicyVersion
return nil
}
if enableBatching {
err = BatchRequestModifyIamPolicy(updater, modifyF, config,
fmt.Sprintf("Create IAM Members %s %+v for %s", memberBind.Role, memberBind.Members[0], updater.DescribeResource()))
} else {
err = iamPolicyReadModifyWrite(updater, modifyF)
}
if err != nil {
return err
}
d.SetId(updater.GetResourceId() + "/" + memberBind.Role + "/" + normalizeIamMemberCasing(memberBind.Members[0]))
if k := conditionKeyFromCondition(memberBind.Condition); !k.Empty() {
d.SetId(d.Id() + "/" + k.String())
}
return resourceIamMemberRead(newUpdaterFunc)(d, meta)
}
}
func resourceIamMemberRead(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
}
eMember := getResourceIamMember(d)
eCondition := conditionKeyFromCondition(eMember.Condition)
p, err := iamPolicyReadWithRetry(updater)
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Resource %q with IAM Member: Role %q Member %q", updater.DescribeResource(), eMember.Role, eMember.Members[0]))
}
log.Print(spew.Sprintf("[DEBUG]: Retrieved policy for %s: %#v\n", updater.DescribeResource(), p))
log.Printf("[DEBUG]: Looking for binding with role %q and condition %#v", eMember.Role, eCondition)
var binding *cloudresourcemanager.Binding
for _, b := range p.Bindings {
if b.Role == eMember.Role && conditionKeyFromCondition(b.Condition) == eCondition {
binding = b
break
}
}
if binding == nil {
log.Printf("[DEBUG]: Binding for role %q with condition %#v does not exist in policy of %s, removing member %q from state.", eMember.Role, eCondition, updater.DescribeResource(), eMember.Members[0])
d.SetId("")
return nil
}
log.Printf("[DEBUG]: Looking for member %q in found binding", eMember.Members[0])
var member string
for _, m := range binding.Members {
if strings.ToLower(m) == strings.ToLower(eMember.Members[0]) {
member = m
}
}
if member == "" {
log.Printf("[DEBUG]: Member %q for binding for role %q with condition %#v does not exist in policy of %s, removing from state.", eMember.Members[0], eMember.Role, eCondition, updater.DescribeResource())
d.SetId("")
return nil
}
if err := d.Set("etag", p.Etag); err != nil {
return fmt.Errorf("Error setting etag: %s", err)
}
if err := d.Set("member", member); err != nil {
return fmt.Errorf("Error setting member: %s", err)
}
if err := d.Set("role", binding.Role); err != nil {
return fmt.Errorf("Error setting role: %s", err)
}
if err := d.Set("condition", FlattenIamCondition(binding.Condition)); err != nil {
return fmt.Errorf("Error setting condition: %s", err)
}
return nil
}
}
func resourceIamMemberDelete(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
}
memberBind := getResourceIamMember(d)
modifyF := func(ep *cloudresourcemanager.Policy) error {
// Merge the bindings together
ep.Bindings = subtractFromBindings(ep.Bindings, memberBind)
return nil
}
if enableBatching {
err = BatchRequestModifyIamPolicy(updater, modifyF, config,
fmt.Sprintf("Delete IAM Members %s %s for %q", memberBind.Role, memberBind.Members[0], updater.DescribeResource()))
} else {
err = iamPolicyReadModifyWrite(updater, modifyF)
}
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Resource %s for IAM Member (role %q, %q)", updater.GetResourceId(), memberBind.Members[0], memberBind.Role))
}
return resourceIamMemberRead(newUpdaterFunc)(d, meta)
}
}