Skip to content

Commit 20fccdc

Browse files
fix bug with data access iam member (#3741) (#2397)
* fix bug with data access iam member * update to still allow iam_member if prefix is not in map * update to add special groups without prefixes to map * updates post create rather than using customize diff * clean up map * undo unnecessary changes to nested_query * add comment Signed-off-by: Modular Magician <[email protected]>
1 parent aa9b8a3 commit 20fccdc

File tree

3 files changed

+257
-17
lines changed

3 files changed

+257
-17
lines changed

.changelog/3741.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
bigquery: fixed bug where `dataset_access.iam_member` would produce inconsistent results after apply.
3+
```

google-beta/resource_big_query_dataset_access.go

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"log"
2020
"reflect"
21+
"strings"
2122
"time"
2223

2324
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
@@ -37,6 +38,108 @@ func resourceBigQueryDatasetAccessRoleDiffSuppress(k, old, new string, d *schema
3738
return false
3839
}
3940

41+
// we want to diff suppress any iam_members that are configured as `iam_member`, but stored in state as a different member type
42+
func resourceBigQueryDatasetAccessIamMemberDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
43+
if primitiveRole, ok := bigqueryAccessRoleToPrimitiveMap[new]; ok {
44+
return primitiveRole == old
45+
}
46+
47+
if d.Get("api_updated_member") == true {
48+
expectedIamMember := d.Get("iam_member").(string)
49+
parts := strings.SplitAfter(expectedIamMember, ":")
50+
51+
strippedIamMember := parts[0]
52+
if len(parts) > 1 {
53+
strippedIamMember = parts[1]
54+
}
55+
56+
if memberInState := d.Get("user_by_email").(string); memberInState != "" {
57+
return memberInState == strippedIamMember
58+
}
59+
60+
if memberInState := d.Get("group_by_email").(string); memberInState != "" {
61+
return memberInState == strippedIamMember
62+
}
63+
64+
if memberInState := d.Get("domain").(string); memberInState != "" {
65+
return memberInState == strippedIamMember
66+
}
67+
68+
if memberInState := d.Get("special_group").(string); memberInState != "" {
69+
return memberInState == strippedIamMember
70+
}
71+
}
72+
73+
return false
74+
}
75+
76+
// this function will go through a response's access list and see if the iam_member has been reassigned to a different member_type
77+
// if it has, it will return the member type, and the member
78+
func resourceBigQueryDatasetAccessReassignIamMemberInNestedObjectList(d *schema.ResourceData, meta interface{}, items []interface{}) (member_type string, member interface{}, err error) {
79+
expectedRole, err := expandNestedBigQueryDatasetAccessRole(d.Get("role"), d, meta.(*Config))
80+
if err != nil {
81+
return "", nil, err
82+
}
83+
expectedFlattenedRole := flattenNestedBigQueryDatasetAccessRole(expectedRole, d, meta.(*Config))
84+
85+
expectedIamMember, err := expandNestedBigQueryDatasetAccessIamMember(d.Get("iam_member"), d, meta.(*Config))
86+
if err != nil {
87+
return "", nil, err
88+
}
89+
expectedFlattenedIamMember := flattenNestedBigQueryDatasetAccessIamMember(expectedIamMember, d, meta.(*Config))
90+
91+
parts := strings.SplitAfter(expectedFlattenedIamMember.(string), ":")
92+
93+
expectedStrippedIamMember := parts[0]
94+
if len(parts) > 1 {
95+
expectedStrippedIamMember = parts[1]
96+
}
97+
98+
// Search list for this resource.
99+
for _, itemRaw := range items {
100+
if itemRaw == nil {
101+
continue
102+
}
103+
item := itemRaw.(map[string]interface{})
104+
105+
itemRole := flattenNestedBigQueryDatasetAccessRole(item["role"], d, meta.(*Config))
106+
// isEmptyValue check so that if one is nil and the other is "", that's considered a match
107+
if !(isEmptyValue(reflect.ValueOf(itemRole)) && isEmptyValue(reflect.ValueOf(expectedFlattenedRole))) && !reflect.DeepEqual(itemRole, expectedFlattenedRole) {
108+
log.Printf("[DEBUG] Skipping item with role= %#v, looking for %#v)", itemRole, expectedFlattenedRole)
109+
continue
110+
}
111+
112+
itemUserByEmail := flattenNestedBigQueryDatasetAccessUserByEmail(item["userByEmail"], d, meta.(*Config))
113+
if reflect.DeepEqual(itemUserByEmail, expectedStrippedIamMember) {
114+
log.Printf("[DEBUG] Iam Member changed to userByEmail= %#v)", itemUserByEmail)
115+
return "user_by_email", itemUserByEmail, nil
116+
}
117+
itemGroupByEmail := flattenNestedBigQueryDatasetAccessGroupByEmail(item["groupByEmail"], d, meta.(*Config))
118+
if reflect.DeepEqual(itemGroupByEmail, expectedStrippedIamMember) {
119+
log.Printf("[DEBUG] Iam Member changed to groupByEmail= %#v)", itemGroupByEmail)
120+
return "group_by_email", itemGroupByEmail, nil
121+
}
122+
itemDomain := flattenNestedBigQueryDatasetAccessDomain(item["domain"], d, meta.(*Config))
123+
if reflect.DeepEqual(itemDomain, expectedStrippedIamMember) {
124+
log.Printf("[DEBUG] Iam Member changed to domain= %#v)", itemDomain)
125+
return "domain", itemDomain, nil
126+
}
127+
itemSpecialGroup := flattenNestedBigQueryDatasetAccessSpecialGroup(item["specialGroup"], d, meta.(*Config))
128+
if reflect.DeepEqual(itemSpecialGroup, expectedStrippedIamMember) {
129+
log.Printf("[DEBUG] Iam Member changed to specialGroup= %#v)", itemSpecialGroup)
130+
return "special_group", itemSpecialGroup, nil
131+
}
132+
itemIamMember := flattenNestedBigQueryDatasetAccessIamMember(item["iamMember"], d, meta.(*Config))
133+
if reflect.DeepEqual(itemIamMember, expectedFlattenedIamMember) {
134+
log.Printf("[DEBUG] Iam Member stayed as iamMember= %#v)", itemIamMember)
135+
return "", nil, nil
136+
}
137+
continue
138+
}
139+
log.Printf("[DEBUG] Did not find item for resource %q)", d.Id())
140+
return "", nil, nil
141+
}
142+
40143
func resourceBigQueryDatasetAccess() *schema.Resource {
41144
return &schema.Resource{
42145
Create: resourceBigQueryDatasetAccessCreate,
@@ -58,24 +161,27 @@ must contain only letters (a-z, A-Z), numbers (0-9), or
58161
underscores (_). The maximum length is 1,024 characters.`,
59162
},
60163
"domain": {
61-
Type: schema.TypeString,
62-
Optional: true,
63-
ForceNew: true,
164+
Type: schema.TypeString,
165+
Optional: true,
166+
ForceNew: true,
167+
DiffSuppressFunc: resourceBigQueryDatasetAccessIamMemberDiffSuppress,
64168
Description: `A domain to grant access to. Any users signed in with the
65169
domain specified will be granted the specified access`,
66170
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
67171
},
68172
"group_by_email": {
69-
Type: schema.TypeString,
70-
Optional: true,
71-
ForceNew: true,
72-
Description: `An email address of a Google Group to grant access to.`,
73-
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
173+
Type: schema.TypeString,
174+
Optional: true,
175+
ForceNew: true,
176+
DiffSuppressFunc: resourceBigQueryDatasetAccessIamMemberDiffSuppress,
177+
Description: `An email address of a Google Group to grant access to.`,
178+
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
74179
},
75180
"iam_member": {
76-
Type: schema.TypeString,
77-
Optional: true,
78-
ForceNew: true,
181+
Type: schema.TypeString,
182+
Optional: true,
183+
ForceNew: true,
184+
DiffSuppressFunc: resourceBigQueryDatasetAccessIamMemberDiffSuppress,
79185
Description: `Some other type of member that appears in the IAM Policy but isn't a user,
80186
group, domain, or special group. For example: 'allUsers'`,
81187
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
@@ -93,9 +199,10 @@ counterparts, and will show a diff post-create. See
93199
[official docs](https://cloud.google.com/bigquery/docs/access-control).`,
94200
},
95201
"special_group": {
96-
Type: schema.TypeString,
97-
Optional: true,
98-
ForceNew: true,
202+
Type: schema.TypeString,
203+
Optional: true,
204+
ForceNew: true,
205+
DiffSuppressFunc: resourceBigQueryDatasetAccessIamMemberDiffSuppress,
99206
Description: `A special group to grant access to. Possible values include:
100207
101208
@@ -112,9 +219,10 @@ counterparts, and will show a diff post-create. See
112219
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
113220
},
114221
"user_by_email": {
115-
Type: schema.TypeString,
116-
Optional: true,
117-
ForceNew: true,
222+
Type: schema.TypeString,
223+
Optional: true,
224+
ForceNew: true,
225+
DiffSuppressFunc: resourceBigQueryDatasetAccessIamMemberDiffSuppress,
118226
Description: `An email address of a user to grant access to. For example:
119227
120228
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
@@ -155,6 +263,11 @@ is 1,024 characters.`,
155263
},
156264
ExactlyOneOf: []string{"user_by_email", "group_by_email", "domain", "special_group", "iam_member", "view"},
157265
},
266+
"api_updated_member": {
267+
Type: schema.TypeBool,
268+
Computed: true,
269+
Description: "If true, represents that that the iam_member in the config was translated to a different member type by the API, and is stored in state as a different member type",
270+
},
158271
"project": {
159272
Type: schema.TypeString,
160273
Optional: true,
@@ -254,6 +367,26 @@ func resourceBigQueryDatasetAccessCreate(d *schema.ResourceData, meta interface{
254367

255368
log.Printf("[DEBUG] Finished creating DatasetAccess %q: %#v", d.Id(), res)
256369

370+
// by default, we are not updating the member
371+
d.Set("api_updated_member", false)
372+
373+
// iam_member is a generalized attribute, if the API can map it to a different member type on the backend, it will return
374+
// the correct member_type in the response. If it cannot be mapped to a different member type, it will stay in iam_member.
375+
if iamMemberProp != "" {
376+
member_type, member, err := resourceBigQueryDatasetAccessReassignIamMemberInNestedObjectList(d, meta, res["access"].([]interface{}))
377+
if err != nil {
378+
fmt.Println(err)
379+
}
380+
381+
// if the member type changed, we set that member_type in state (it's already in the response) and we clear iam_member
382+
// and we set "api_updated_member" to true to acknowledge that we are making this change
383+
if member_type != "" {
384+
d.Set(member_type, member.(string))
385+
d.Set("iam_member", "")
386+
d.Set("api_updated_member", true)
387+
}
388+
}
389+
257390
return resourceBigQueryDatasetAccessRead(d, meta)
258391
}
259392

google-beta/resource_bigquery_dataset_access_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,58 @@ func TestAccBigQueryDatasetAccess_predefinedRole(t *testing.T) {
151151
})
152152
}
153153

154+
func TestAccBigQueryDatasetAccess_iamMember(t *testing.T) {
155+
t.Parallel()
156+
157+
datasetID := fmt.Sprintf("tf_test_%s", randString(t, 10))
158+
sinkName := fmt.Sprintf("tf_test_%s", randString(t, 10))
159+
160+
vcrTest(t, resource.TestCase{
161+
PreCheck: func() { testAccPreCheck(t) },
162+
Providers: testAccProviders,
163+
Steps: []resource.TestStep{
164+
{
165+
Config: testAccBigQueryDatasetAccess_iamMember(datasetID, sinkName),
166+
},
167+
},
168+
})
169+
}
170+
171+
func TestAccBigQueryDatasetAccess_allUsers(t *testing.T) {
172+
t.Parallel()
173+
174+
datasetID := fmt.Sprintf("tf_test_%s", randString(t, 10))
175+
176+
vcrTest(t, resource.TestCase{
177+
PreCheck: func() { testAccPreCheck(t) },
178+
Providers: testAccProviders,
179+
Steps: []resource.TestStep{
180+
{
181+
Config: testAccBigQueryDatasetAccess_allUsers(datasetID),
182+
},
183+
{
184+
Config: testAccBigQueryDatasetAccess_allAuthenticatedUsers(datasetID),
185+
},
186+
},
187+
})
188+
}
189+
190+
func TestAccBigQueryDatasetAccess_allAuthenticatedUsers(t *testing.T) {
191+
t.Parallel()
192+
193+
datasetID := fmt.Sprintf("tf_test_%s", randString(t, 10))
194+
195+
vcrTest(t, resource.TestCase{
196+
PreCheck: func() { testAccPreCheck(t) },
197+
Providers: testAccProviders,
198+
Steps: []resource.TestStep{
199+
{
200+
Config: testAccBigQueryDatasetAccess_allAuthenticatedUsers(datasetID),
201+
},
202+
},
203+
})
204+
}
205+
154206
func testAccCheckBigQueryDatasetAccessPresent(t *testing.T, n string, expected map[string]interface{}) resource.TestCheckFunc {
155207
return testAccCheckBigQueryDatasetAccess(t, n, expected, true)
156208
}
@@ -283,3 +335,55 @@ resource "google_bigquery_dataset" "dataset" {
283335
}
284336
`, role, datasetID)
285337
}
338+
339+
func testAccBigQueryDatasetAccess_iamMember(datasetID, sinkName string) string {
340+
return fmt.Sprintf(`
341+
resource "google_bigquery_dataset_access" "dns_query_sink" {
342+
dataset_id = google_bigquery_dataset.dataset.dataset_id
343+
role = "roles/bigquery.dataEditor"
344+
iam_member = google_logging_project_sink.logging_sink.writer_identity
345+
}
346+
347+
resource "google_bigquery_dataset" "dataset" {
348+
dataset_id = "%s"
349+
}
350+
351+
resource "google_logging_project_sink" "logging_sink" {
352+
name = "%s_logging_project_sink"
353+
354+
destination = "bigquery.googleapis.com/${google_bigquery_dataset.dataset.id}"
355+
356+
filter = "resource.type=\"dns_query\""
357+
358+
unique_writer_identity = true
359+
}
360+
`, datasetID, sinkName)
361+
}
362+
363+
func testAccBigQueryDatasetAccess_allUsers(datasetID string) string {
364+
return fmt.Sprintf(`
365+
resource "google_bigquery_dataset_access" "dns_query_sink" {
366+
dataset_id = google_bigquery_dataset.dataset.dataset_id
367+
role = "roles/bigquery.dataEditor"
368+
iam_member = "allUsers"
369+
}
370+
371+
resource "google_bigquery_dataset" "dataset" {
372+
dataset_id = "%s"
373+
}
374+
`, datasetID)
375+
}
376+
377+
func testAccBigQueryDatasetAccess_allAuthenticatedUsers(datasetID string) string {
378+
return fmt.Sprintf(`
379+
resource "google_bigquery_dataset_access" "dns_query_sink" {
380+
dataset_id = google_bigquery_dataset.dataset.dataset_id
381+
role = "roles/bigquery.dataEditor"
382+
iam_member = "allAuthenticatedUsers"
383+
}
384+
385+
resource "google_bigquery_dataset" "dataset" {
386+
dataset_id = "%s"
387+
}
388+
`, datasetID)
389+
}

0 commit comments

Comments
 (0)