Skip to content

Commit 345e98d

Browse files
fix(#11218): Added logic for mergi… (#11352) (#19121)
[upstream:649cb8b86339253af356ad48f0b523b2acfb099f] Signed-off-by: Modular Magician <[email protected]>
1 parent db573cc commit 345e98d

File tree

4 files changed

+404
-3
lines changed

4 files changed

+404
-3
lines changed

.changelog/11352.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
bigquery: made `google_bigquery_dataset_iam_member` non-authoritative. To remove a bigquery dataset iam member, use an authoritative resource like `google_bigquery_dataset_iam_policy`
3+
```

google/provider/provider_mmv1_resources.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,7 @@ var handwrittenIAMResources = map[string]*schema.Resource{
12141214
"google_bigtable_table_iam_member": tpgiamresource.ResourceIamMember(bigtable.IamBigtableTableSchema, bigtable.NewBigtableTableUpdater, bigtable.BigtableTableIdParseFunc),
12151215
"google_bigtable_table_iam_policy": tpgiamresource.ResourceIamPolicy(bigtable.IamBigtableTableSchema, bigtable.NewBigtableTableUpdater, bigtable.BigtableTableIdParseFunc),
12161216
"google_bigquery_dataset_iam_binding": tpgiamresource.ResourceIamBinding(bigquery.IamBigqueryDatasetSchema, bigquery.NewBigqueryDatasetIamUpdater, bigquery.BigqueryDatasetIdParseFunc),
1217-
"google_bigquery_dataset_iam_member": tpgiamresource.ResourceIamMember(bigquery.IamBigqueryDatasetSchema, bigquery.NewBigqueryDatasetIamUpdater, bigquery.BigqueryDatasetIdParseFunc),
1217+
"google_bigquery_dataset_iam_member": tpgiamresource.ResourceIamMember(bigquery.IamMemberBigqueryDatasetSchema, bigquery.NewBigqueryDatasetIamMemberUpdater, bigquery.BigqueryDatasetIdParseFunc),
12181218
"google_bigquery_dataset_iam_policy": tpgiamresource.ResourceIamPolicy(bigquery.IamBigqueryDatasetSchema, bigquery.NewBigqueryDatasetIamUpdater, bigquery.BigqueryDatasetIdParseFunc),
12191219
"google_billing_account_iam_binding": tpgiamresource.ResourceIamBinding(billing.IamBillingAccountSchema, billing.NewBillingAccountIamUpdater, billing.BillingAccountIdParseFunc),
12201220
"google_billing_account_iam_member": tpgiamresource.ResourceIamMember(billing.IamBillingAccountSchema, billing.NewBillingAccountIamUpdater, billing.BillingAccountIdParseFunc),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
// Copyright (c) HashiCorp, Inc.
4+
// SPDX-License-Identifier: MPL-2.0
5+
/*
6+
This file is a copy of mmv1/third_party/terraform/services/bigquery/iam_bigquery_dataset.go
7+
with new functions mergeAccess and GetCurrentResourceAccess
8+
*/
9+
package bigquery
10+
11+
import (
12+
"errors"
13+
"fmt"
14+
"strings"
15+
16+
"github.com/hashicorp/terraform-provider-google/google/tpgiamresource"
17+
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
18+
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
19+
20+
"github.com/hashicorp/errwrap"
21+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
22+
"google.golang.org/api/cloudresourcemanager/v1"
23+
)
24+
25+
var IamMemberBigqueryDatasetSchema = map[string]*schema.Schema{
26+
"dataset_id": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ForceNew: true,
30+
},
31+
"project": {
32+
Type: schema.TypeString,
33+
Optional: true,
34+
Computed: true,
35+
ForceNew: true,
36+
},
37+
}
38+
39+
var bigqueryIamMemberAccessPrimitiveToRoleMap = map[string]string{
40+
"OWNER": "roles/bigquery.dataOwner",
41+
"WRITER": "roles/bigquery.dataEditor",
42+
"READER": "roles/bigquery.dataViewer",
43+
}
44+
45+
type BigqueryDatasetIamMemberUpdater struct {
46+
project string
47+
datasetId string
48+
d tpgresource.TerraformResourceData
49+
Config *transport_tpg.Config
50+
}
51+
52+
func NewBigqueryDatasetIamMemberUpdater(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (tpgiamresource.ResourceIamUpdater, error) {
53+
project, err := tpgresource.GetProject(d, config)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
if err := d.Set("project", project); err != nil {
59+
return nil, fmt.Errorf("Error setting project: %s", err)
60+
}
61+
62+
return &BigqueryDatasetIamMemberUpdater{
63+
project: project,
64+
datasetId: d.Get("dataset_id").(string),
65+
d: d,
66+
Config: config,
67+
}, nil
68+
}
69+
70+
func (u *BigqueryDatasetIamMemberUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
71+
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
72+
73+
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
79+
Config: u.Config,
80+
Method: "GET",
81+
Project: u.project,
82+
RawURL: url,
83+
UserAgent: userAgent,
84+
})
85+
if err != nil {
86+
return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
87+
}
88+
89+
policy, err := accessToPolicyForIamMember(res["access"])
90+
if err != nil {
91+
return nil, err
92+
}
93+
return policy, nil
94+
}
95+
96+
func GetCurrentResourceAccess(u *BigqueryDatasetIamMemberUpdater) ([]interface{}, error) {
97+
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
98+
99+
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
105+
Config: u.Config,
106+
Method: "GET",
107+
Project: u.project,
108+
RawURL: url,
109+
UserAgent: userAgent,
110+
})
111+
if err != nil {
112+
return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
113+
}
114+
115+
var access []interface{}
116+
if accessVal, ok := res["access"].([]interface{}); ok {
117+
access = accessVal
118+
} else if res["access"] == nil {
119+
access = []interface{}{} // Return an empty slice if the key is missing
120+
} else {
121+
return nil, fmt.Errorf("value under 'access' is not a slice of interface{}")
122+
}
123+
124+
return access, nil
125+
}
126+
127+
func mergeAccess(newAccess []map[string]interface{}, currAccess []interface{}) []map[string]interface{} {
128+
mergedAccess := make([]map[string]interface{}, 0, len(newAccess)+len(currAccess))
129+
mergedAccess = append(mergedAccess, newAccess...)
130+
131+
for _, item := range currAccess {
132+
// Type assertion to check if it's amap
133+
if itemMap, ok := item.(map[string]interface{}); ok {
134+
mergedAccess = append(mergedAccess, itemMap)
135+
}
136+
}
137+
return mergedAccess
138+
}
139+
140+
func (u *BigqueryDatasetIamMemberUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
141+
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
142+
143+
newAccess, err := policyToAccessForIamMember(policy)
144+
if err != nil {
145+
return err
146+
}
147+
148+
currAccess, err := GetCurrentResourceAccess(u)
149+
if err != nil {
150+
return err
151+
}
152+
153+
access := mergeAccess(newAccess, currAccess)
154+
155+
obj := map[string]interface{}{
156+
"access": access,
157+
}
158+
159+
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
160+
if err != nil {
161+
return err
162+
}
163+
164+
_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
165+
Config: u.Config,
166+
Method: "PATCH",
167+
Project: u.project,
168+
RawURL: url,
169+
UserAgent: userAgent,
170+
Body: obj,
171+
})
172+
if err != nil {
173+
return fmt.Errorf("Error creating DatasetAccess: %s", err)
174+
}
175+
176+
return nil
177+
}
178+
179+
func accessToPolicyForIamMember(access interface{}) (*cloudresourcemanager.Policy, error) {
180+
if access == nil {
181+
return nil, nil
182+
}
183+
roleToBinding := make(map[string]*cloudresourcemanager.Binding)
184+
185+
accessArr := access.([]interface{})
186+
for _, v := range accessArr {
187+
memberRole := v.(map[string]interface{})
188+
rawRole, ok := memberRole["role"]
189+
if !ok {
190+
// "view" allows role to not be defined. It is a special dataset access construct, so ignore
191+
// If a user wants to manage "view" access they should use the `bigquery_dataset_access` resource
192+
continue
193+
}
194+
role := rawRole.(string)
195+
if iamRole, ok := bigqueryIamMemberAccessPrimitiveToRoleMap[role]; ok {
196+
// API changes certain IAM roles to legacy roles. Revert these changes
197+
role = iamRole
198+
}
199+
member, err := accessToIamMemberForIamMember(memberRole)
200+
if err != nil {
201+
return nil, err
202+
}
203+
// We have to combine bindings manually
204+
binding, ok := roleToBinding[role]
205+
if !ok {
206+
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}}
207+
}
208+
binding.Members = append(binding.Members, member)
209+
210+
roleToBinding[role] = binding
211+
}
212+
bindings := make([]*cloudresourcemanager.Binding, 0)
213+
for _, v := range roleToBinding {
214+
bindings = append(bindings, v)
215+
}
216+
217+
return &cloudresourcemanager.Policy{Bindings: bindings}, nil
218+
}
219+
220+
func policyToAccessForIamMember(policy *cloudresourcemanager.Policy) ([]map[string]interface{}, error) {
221+
res := make([]map[string]interface{}, 0)
222+
if len(policy.AuditConfigs) != 0 {
223+
return nil, errors.New("Access policies not allowed on BigQuery Dataset IAM policies")
224+
}
225+
for _, binding := range policy.Bindings {
226+
if binding.Condition != nil {
227+
return nil, errors.New("IAM conditions not allowed on BigQuery Dataset IAM")
228+
}
229+
if fullRole, ok := bigqueryIamMemberAccessPrimitiveToRoleMap[binding.Role]; ok {
230+
return nil, fmt.Errorf("BigQuery Dataset legacy role %s is not allowed when using google_bigquery_dataset_iam resources. Please use the full form: %s", binding.Role, fullRole)
231+
}
232+
for _, member := range binding.Members {
233+
// Do not append any deleted members
234+
if strings.HasPrefix(member, "deleted:") {
235+
continue
236+
}
237+
access := map[string]interface{}{
238+
"role": binding.Role,
239+
}
240+
memberType, member, err := iamMemberToAccessForIamMember(member)
241+
if err != nil {
242+
return nil, err
243+
}
244+
access[memberType] = member
245+
res = append(res, access)
246+
}
247+
}
248+
249+
return res, nil
250+
}
251+
252+
// Returns the member access type and member for an IAM member.
253+
// Dataset access uses different member types to identify groups, domains, etc.
254+
// these types are used as keys in the access JSON payload
255+
func iamMemberToAccessForIamMember(member string) (string, string, error) {
256+
if strings.HasPrefix(member, "deleted:") {
257+
return "", "", fmt.Errorf("BigQuery Dataset IAM member is deleted: %s", member)
258+
}
259+
pieces := strings.SplitN(member, ":", 2)
260+
if len(pieces) > 1 {
261+
switch pieces[0] {
262+
case "group":
263+
return "groupByEmail", pieces[1], nil
264+
case "domain":
265+
return "domain", pieces[1], nil
266+
case "iamMember":
267+
return "iamMember", pieces[1], nil
268+
case "user":
269+
return "userByEmail", pieces[1], nil
270+
case "serviceAccount":
271+
return "userByEmail", pieces[1], nil
272+
}
273+
}
274+
if member == "projectOwners" || member == "projectReaders" || member == "projectWriters" || member == "allAuthenticatedUsers" {
275+
// These are special BigQuery Dataset permissions
276+
return "specialGroup", member, nil
277+
}
278+
return "", "", fmt.Errorf("Failed to parse BigQuery Dataset IAM member type: %s", member)
279+
}
280+
281+
func accessToIamMemberForIamMember(access map[string]interface{}) (string, error) {
282+
// One of the fields must be set, we have to find which IAM member type this newAccess to
283+
if member, ok := access["groupByEmail"]; ok {
284+
return fmt.Sprintf("group:%s", member.(string)), nil
285+
}
286+
if member, ok := access["domain"]; ok {
287+
return fmt.Sprintf("domain:%s", member.(string)), nil
288+
}
289+
if member, ok := access["specialGroup"]; ok {
290+
return member.(string), nil
291+
}
292+
if member, ok := access["iamMember"]; ok {
293+
return fmt.Sprintf("iamMember:%s", member.(string)), nil
294+
}
295+
if _, ok := access["view"]; ok {
296+
// view does not map to an IAM member, use access instead
297+
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
298+
}
299+
if _, ok := access["dataset"]; ok {
300+
// dataset does not map to an IAM member, use access instead
301+
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
302+
}
303+
if _, ok := access["routine"]; ok {
304+
// dataset does not map to an IAM member, use access instead
305+
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
306+
}
307+
if member, ok := access["userByEmail"]; ok {
308+
// service accounts have "gservice" in their email. This is best guess due to lost information
309+
if strings.Contains(member.(string), "gserviceaccount") {
310+
return fmt.Sprintf("serviceAccount:%s", member.(string)), nil
311+
}
312+
return fmt.Sprintf("user:%s", member.(string)), nil
313+
}
314+
return "", fmt.Errorf("Failed to identify IAM member from BigQuery Dataset access: %v", access)
315+
}
316+
317+
func (u *BigqueryDatasetIamMemberUpdater) GetResourceId() string {
318+
return fmt.Sprintf("projects/%s/datasets/%s", u.project, u.datasetId)
319+
}
320+
321+
// Matches the mutex of google_big_query_dataset_access
322+
func (u *BigqueryDatasetIamMemberUpdater) GetMutexKey() string {
323+
return fmt.Sprintf("%s", u.datasetId)
324+
}
325+
326+
func (u *BigqueryDatasetIamMemberUpdater) DescribeResource() string {
327+
return fmt.Sprintf("Bigquery Dataset %s/%s", u.project, u.datasetId)
328+
}

0 commit comments

Comments
 (0)