Skip to content

Commit 3680ab8

Browse files
bigquery: support for IAM conditions in all google_bigquery_dataset_iam_* resources (#15337)
1 parent 5130d4c commit 3680ab8

File tree

5 files changed

+416
-34
lines changed

5 files changed

+416
-34
lines changed

mmv1/third_party/terraform/services/bigquery/iam_bigquery_dataset.go

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ import (
1414
"google.golang.org/api/cloudresourcemanager/v1"
1515
)
1616

17+
type conditionKey struct {
18+
Description string
19+
Expression string
20+
Title string
21+
}
22+
23+
type iamBindingKey struct {
24+
Role string
25+
Condition conditionKey
26+
}
27+
28+
func conditionKeyFromCondition(condition *cloudresourcemanager.Expr) conditionKey {
29+
if condition == nil {
30+
return conditionKey{}
31+
}
32+
return conditionKey{Description: condition.Description, Expression: condition.Expression, Title: condition.Title}
33+
}
34+
1735
var IamBigqueryDatasetSchema = map[string]*schema.Schema{
1836
"dataset_id": {
1937
Type: schema.TypeString,
@@ -77,8 +95,12 @@ func BigqueryDatasetIdParseFunc(d *schema.ResourceData, config *transport_tpg.Co
7795
return nil
7896
}
7997

98+
func (u *BigqueryDatasetIamUpdater) policyURL() string {
99+
return fmt.Sprintf("%s%s?accessPolicyVersion=3", u.Config.BigQueryBasePath, u.GetResourceId())
100+
}
101+
80102
func (u *BigqueryDatasetIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
81-
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
103+
url := u.policyURL()
82104

83105
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
84106
if err != nil {
@@ -104,7 +126,7 @@ func (u *BigqueryDatasetIamUpdater) GetResourceIamPolicy() (*cloudresourcemanage
104126
}
105127

106128
func (u *BigqueryDatasetIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
107-
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
129+
url := u.policyURL()
108130

109131
access, err := policyToAccess(policy)
110132
if err != nil {
@@ -138,7 +160,7 @@ func accessToPolicy(access interface{}) (*cloudresourcemanager.Policy, error) {
138160
if access == nil {
139161
return nil, nil
140162
}
141-
roleToBinding := make(map[string]*cloudresourcemanager.Binding)
163+
roleToBinding := make(map[iamBindingKey]*cloudresourcemanager.Binding)
142164

143165
accessArr := access.([]interface{})
144166
for _, v := range accessArr {
@@ -158,20 +180,32 @@ func accessToPolicy(access interface{}) (*cloudresourcemanager.Policy, error) {
158180
if err != nil {
159181
return nil, err
160182
}
161-
// We have to combine bindings manually
162-
binding, ok := roleToBinding[role]
183+
184+
var condition *cloudresourcemanager.Expr
185+
if rawCondition, ok := memberRole["condition"]; ok {
186+
conditionMap := rawCondition.(map[string]interface{})
187+
expr := conditionMap["expression"].(string)
188+
condition = &cloudresourcemanager.Expr{Expression: expr}
189+
if title, ok := conditionMap["title"].(string); ok {
190+
condition.Title = title
191+
}
192+
if desc, ok := conditionMap["description"].(string); ok {
193+
condition.Description = desc
194+
}
195+
}
196+
197+
key := iamBindingKey{Role: role, Condition: conditionKeyFromCondition(condition)}
198+
binding, ok := roleToBinding[key]
163199
if !ok {
164-
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}}
200+
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}, Condition: condition}
165201
}
166202
binding.Members = append(binding.Members, member)
167-
168-
roleToBinding[role] = binding
203+
roleToBinding[key] = binding
169204
}
170-
bindings := make([]*cloudresourcemanager.Binding, 0)
205+
bindings := make([]*cloudresourcemanager.Binding, 0, len(roleToBinding))
171206
for _, v := range roleToBinding {
172207
bindings = append(bindings, v)
173208
}
174-
175209
return &cloudresourcemanager.Policy{Bindings: bindings}, nil
176210
}
177211

@@ -181,9 +215,6 @@ func policyToAccess(policy *cloudresourcemanager.Policy) ([]map[string]interface
181215
return nil, errors.New("Access policies not allowed on BigQuery Dataset IAM policies")
182216
}
183217
for _, binding := range policy.Bindings {
184-
if binding.Condition != nil {
185-
return nil, errors.New("IAM conditions not allowed on BigQuery Dataset IAM")
186-
}
187218
if fullRole, ok := bigqueryAccessPrimitiveToRoleMap[binding.Role]; ok {
188219
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)
189220
}
@@ -195,6 +226,9 @@ func policyToAccess(policy *cloudresourcemanager.Policy) ([]map[string]interface
195226
access := map[string]interface{}{
196227
"role": binding.Role,
197228
}
229+
if binding.Condition != nil {
230+
access["condition"] = binding.Condition
231+
}
198232
memberType, member, err := iamMemberToAccess(member)
199233
if err != nil {
200234
return nil, err
@@ -263,11 +297,14 @@ func accessToIamMember(access map[string]interface{}) (string, error) {
263297
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
264298
}
265299
if member, ok := access["userByEmail"]; ok {
266-
// service accounts have "gservice" in their email. This is best guess due to lost information
267-
if strings.Contains(member.(string), "gserviceaccount") {
268-
return fmt.Sprintf("serviceAccount:%s", member.(string)), nil
300+
ms := member.(string)
301+
if strings.HasPrefix(ms, "deleted:serviceAccount:") {
302+
return ms, nil
303+
}
304+
if strings.Contains(ms, "gserviceaccount") {
305+
return fmt.Sprintf("serviceAccount:%s", ms), nil
269306
}
270-
return fmt.Sprintf("user:%s", member.(string)), nil
307+
return fmt.Sprintf("user:%s", ms), nil
271308
}
272309
return "", fmt.Errorf("Failed to identify IAM member from BigQuery Dataset access: %v", access)
273310
}

mmv1/third_party/terraform/services/bigquery/iam_bigquery_member_dataset.go

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ func NewBigqueryDatasetIamMemberUpdater(d tpgresource.TerraformResourceData, con
6565
}, nil
6666
}
6767

68+
func (u *BigqueryDatasetIamMemberUpdater) policyURL() string {
69+
return fmt.Sprintf("%s%s?accessPolicyVersion=3", u.Config.BigQueryBasePath, u.GetResourceId())
70+
}
71+
6872
func (u *BigqueryDatasetIamMemberUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
69-
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
73+
url := u.policyURL()
7074

7175
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
7276
if err != nil {
@@ -92,7 +96,7 @@ func (u *BigqueryDatasetIamMemberUpdater) GetResourceIamPolicy() (*cloudresource
9296
}
9397

9498
func GetCurrentResourceAccess(u *BigqueryDatasetIamMemberUpdater) ([]interface{}, error) {
95-
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
99+
url := u.policyURL()
96100

97101
userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent)
98102
if err != nil {
@@ -141,7 +145,7 @@ func mergeAccess(newAccess []map[string]interface{}, currAccess []interface{}) [
141145
}
142146

143147
func (u *BigqueryDatasetIamMemberUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
144-
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
148+
url := u.policyURL()
145149

146150
newAccess, err := policyToAccessForIamMember(policy)
147151
if err != nil {
@@ -183,7 +187,7 @@ func accessToPolicyForIamMember(access interface{}) (*cloudresourcemanager.Polic
183187
if access == nil {
184188
return nil, nil
185189
}
186-
roleToBinding := make(map[string]*cloudresourcemanager.Binding)
190+
roleToBinding := make(map[iamBindingKey]*cloudresourcemanager.Binding)
187191

188192
accessArr := access.([]interface{})
189193
for _, v := range accessArr {
@@ -203,16 +207,29 @@ func accessToPolicyForIamMember(access interface{}) (*cloudresourcemanager.Polic
203207
if err != nil {
204208
return nil, err
205209
}
206-
// We have to combine bindings manually
207-
binding, ok := roleToBinding[role]
210+
211+
var condition *cloudresourcemanager.Expr
212+
if rawCondition, ok := memberRole["condition"]; ok {
213+
conditionMap := rawCondition.(map[string]interface{})
214+
expr := conditionMap["expression"].(string)
215+
condition = &cloudresourcemanager.Expr{Expression: expr}
216+
if title, ok := conditionMap["title"].(string); ok {
217+
condition.Title = title
218+
}
219+
if desc, ok := conditionMap["description"].(string); ok {
220+
condition.Description = desc
221+
}
222+
}
223+
224+
key := iamBindingKey{Role: role, Condition: conditionKeyFromCondition(condition)}
225+
binding, ok := roleToBinding[key]
208226
if !ok {
209-
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}}
227+
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}, Condition: condition}
210228
}
211229
binding.Members = append(binding.Members, member)
212-
213-
roleToBinding[role] = binding
230+
roleToBinding[key] = binding
214231
}
215-
bindings := make([]*cloudresourcemanager.Binding, 0)
232+
bindings := make([]*cloudresourcemanager.Binding, 0, len(roleToBinding))
216233
for _, v := range roleToBinding {
217234
bindings = append(bindings, v)
218235
}
@@ -226,9 +243,6 @@ func policyToAccessForIamMember(policy *cloudresourcemanager.Policy) ([]map[stri
226243
return nil, errors.New("Access policies not allowed on BigQuery Dataset IAM policies")
227244
}
228245
for _, binding := range policy.Bindings {
229-
if binding.Condition != nil {
230-
return nil, errors.New("IAM conditions not allowed on BigQuery Dataset IAM")
231-
}
232246
if fullRole, ok := bigqueryIamMemberAccessPrimitiveToRoleMap[binding.Role]; ok {
233247
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)
234248
}
@@ -240,6 +254,9 @@ func policyToAccessForIamMember(policy *cloudresourcemanager.Policy) ([]map[stri
240254
access := map[string]interface{}{
241255
"role": binding.Role,
242256
}
257+
if binding.Condition != nil {
258+
access["condition"] = binding.Condition
259+
}
243260
memberType, member, err := iamMemberToAccessForIamMember(member)
244261
if err != nil {
245262
return nil, err
@@ -308,11 +325,14 @@ func accessToIamMemberForIamMember(access map[string]interface{}) (string, error
308325
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
309326
}
310327
if member, ok := access["userByEmail"]; ok {
311-
// service accounts have "gservice" in their email. This is best guess due to lost information
312-
if strings.Contains(member.(string), "gserviceaccount") {
313-
return fmt.Sprintf("serviceAccount:%s", member.(string)), nil
328+
ms := member.(string)
329+
if strings.HasPrefix(ms, "deleted:") || strings.Contains(ms, "?uid=") {
330+
return ms, nil
331+
}
332+
if strings.Contains(ms, "gserviceaccount") {
333+
return fmt.Sprintf("serviceAccount:%s", ms), nil
314334
}
315-
return fmt.Sprintf("user:%s", member.(string)), nil
335+
return fmt.Sprintf("user:%s", ms), nil
316336
}
317337
return "", fmt.Errorf("Failed to identify IAM member from BigQuery Dataset access: %v", access)
318338
}

mmv1/third_party/terraform/services/bigquery/resource_bigquery_dataset_iam_member_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import (
1414
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
1515
)
1616

17+
const (
18+
condTitle2050 = "Expire access on 2050-12-31"
19+
condExpr2050 = "request.time < timestamp('2050-12-31T23:59:59Z')"
20+
condDesc2050 = "This condition will automatically remove access after 2050-12-31"
21+
condTitle2040 = "Expire access on 2040-12-31"
22+
condExpr2040 = "request.time < timestamp('2040-12-31T23:59:59Z')"
23+
condDesc2040 = "This condition will automatically remove access after 2040-12-31"
24+
)
25+
1726
func TestAccBigqueryDatasetIamMember_afterDatasetCreation(t *testing.T) {
1827
t.Parallel()
1928

@@ -110,6 +119,36 @@ func TestAccBigqueryDatasetIamMember_serviceAccount(t *testing.T) {
110119
})
111120
}
112121

122+
func TestAccBigqueryDatasetIamMember_iamMemberWithIAMCondition(t *testing.T) {
123+
t.Parallel()
124+
125+
datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10))
126+
wifIDs := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
127+
128+
acctest.VcrTest(t, resource.TestCase{
129+
PreCheck: func() { acctest.AccTestPreCheck(t) },
130+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
131+
Steps: []resource.TestStep{
132+
{
133+
Config: testAccBigqueryDatasetIamMember_withServiceAccountAndIAMCondition(datasetID, wifIDs),
134+
Check: resource.ComposeTestCheckFunc(
135+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.title", condTitle2050),
136+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.expression", condExpr2050),
137+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.description", condDesc2050),
138+
),
139+
},
140+
{
141+
Config: testAccBigqueryDatasetIamMember_withServiceAccountAndIAMCondition_update(datasetID, wifIDs),
142+
Check: resource.ComposeTestCheckFunc(
143+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.title", condTitle2040),
144+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.expression", condExpr2040),
145+
resource.TestCheckResourceAttr("google_bigquery_dataset_iam_member.access", "condition.0.description", ""),
146+
),
147+
},
148+
},
149+
})
150+
}
151+
113152
func TestAccBigqueryDatasetIamMember_iamMember(t *testing.T) {
114153
t.Parallel()
115154

@@ -405,3 +444,50 @@ resource "google_bigquery_dataset_iam_member" "access" {
405444
}
406445
`, datasetID, serviceAccountEmail)
407446
}
447+
448+
func testAccBigqueryDatasetIamMember_withServiceAccountAndIAMCondition(datasetID, serviceAccountID string) string {
449+
return fmt.Sprintf(`
450+
resource "google_bigquery_dataset" "dataset" {
451+
dataset_id = "%s"
452+
}
453+
454+
resource "google_service_account" "sa" {
455+
account_id = "%s"
456+
}
457+
458+
resource "google_bigquery_dataset_iam_member" "access" {
459+
dataset_id = google_bigquery_dataset.dataset.dataset_id
460+
role = "roles/bigquery.dataViewer"
461+
member = "serviceAccount:${google_service_account.sa.email}"
462+
463+
condition {
464+
title = "%s"
465+
description = "%s"
466+
expression = "%s"
467+
}
468+
}
469+
`, datasetID, serviceAccountID, condTitle2050, condDesc2050, condExpr2050)
470+
}
471+
472+
func testAccBigqueryDatasetIamMember_withServiceAccountAndIAMCondition_update(datasetID, serviceAccountID string) string {
473+
return fmt.Sprintf(`
474+
resource "google_bigquery_dataset" "dataset" {
475+
dataset_id = "%s"
476+
}
477+
478+
resource "google_service_account" "sa" {
479+
account_id = "%s"
480+
}
481+
482+
resource "google_bigquery_dataset_iam_member" "access" {
483+
dataset_id = google_bigquery_dataset.dataset.dataset_id
484+
role = "roles/bigquery.dataViewer"
485+
member = "serviceAccount:${google_service_account.sa.email}"
486+
487+
condition {
488+
title = "%s"
489+
expression = "%s"
490+
}
491+
}
492+
`, datasetID, serviceAccountID, condTitle2040, condExpr2040)
493+
}

0 commit comments

Comments
 (0)