| // 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) |
| } |
| } |