Skip to content

Commit 4c1fb6f

Browse files
Bigquery dataset iam (#3608) (#2147)
* Saving work, not ready * Add iam start for dataset, not compiling * Test commit * Converting from IAM member to access * Add docs * Remove unused decoder * Lint * Add apply-time check for primitive roles in config * PR feedback * Fix compile Signed-off-by: Modular Magician <[email protected]>
1 parent 0c7a289 commit 4c1fb6f

File tree

7 files changed

+636
-0
lines changed

7 files changed

+636
-0
lines changed

.changelog/3608.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
```release-note:new-resource
2+
`google_bigquery_dataset_iam_binding`
3+
```
4+
```release-note:new-resource
5+
`google_bigquery_dataset_iam_member`
6+
```
7+
```release-note:new-resource
8+
`google_bigquery_dataset_iam_policy`
9+
```
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package google
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/errwrap"
9+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
10+
"google.golang.org/api/cloudresourcemanager/v1"
11+
)
12+
13+
var IamBigqueryDatasetSchema = map[string]*schema.Schema{
14+
"dataset_id": {
15+
Type: schema.TypeString,
16+
Required: true,
17+
ForceNew: true,
18+
},
19+
"project": {
20+
Type: schema.TypeString,
21+
Optional: true,
22+
Computed: true,
23+
ForceNew: true,
24+
},
25+
}
26+
27+
var bigqueryAccessPrimitiveToRoleMap = map[string]string{
28+
"OWNER": "roles/bigquery.dataOwner",
29+
"WRITER": "roles/bigquery.dataEditor",
30+
"READER": "roles/bigquery.dataViewer",
31+
}
32+
33+
type BigqueryDatasetIamUpdater struct {
34+
project string
35+
datasetId string
36+
Config *Config
37+
}
38+
39+
func NewBigqueryDatasetIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) {
40+
project, err := getProject(d, config)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
d.Set("project", project)
46+
47+
return &BigqueryDatasetIamUpdater{
48+
project: project,
49+
datasetId: d.Get("dataset_id").(string),
50+
Config: config,
51+
}, nil
52+
}
53+
54+
func BigqueryDatasetIdParseFunc(d *schema.ResourceData, config *Config) error {
55+
fv, err := parseProjectFieldValue("datasets", d.Id(), "project", d, config, false)
56+
if err != nil {
57+
return err
58+
}
59+
60+
d.Set("project", fv.Project)
61+
d.Set("dataset_id", fv.Name)
62+
63+
// Explicitly set the id so imported resources have the same ID format as non-imported ones.
64+
d.SetId(fv.RelativeLink())
65+
return nil
66+
}
67+
68+
func (u *BigqueryDatasetIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
69+
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
70+
71+
res, err := sendRequest(u.Config, "GET", u.project, url, nil)
72+
if err != nil {
73+
return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
74+
}
75+
76+
policy, err := accessToPolicy(res["access"])
77+
if err != nil {
78+
return nil, err
79+
}
80+
return policy, nil
81+
}
82+
83+
func (u *BigqueryDatasetIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
84+
url := fmt.Sprintf("%s%s", u.Config.BigQueryBasePath, u.GetResourceId())
85+
86+
access, err := policyToAccess(policy)
87+
if err != nil {
88+
return err
89+
}
90+
obj := map[string]interface{}{
91+
"access": access,
92+
}
93+
94+
_, err = sendRequest(u.Config, "PATCH", u.project, url, obj)
95+
if err != nil {
96+
return fmt.Errorf("Error creating DatasetAccess: %s", err)
97+
}
98+
99+
return nil
100+
}
101+
102+
func accessToPolicy(access interface{}) (*cloudresourcemanager.Policy, error) {
103+
if access == nil {
104+
return nil, nil
105+
}
106+
roleToBinding := make(map[string]*cloudresourcemanager.Binding)
107+
108+
accessArr := access.([]interface{})
109+
for _, v := range accessArr {
110+
memberRole := v.(map[string]interface{})
111+
rawRole, ok := memberRole["role"]
112+
if !ok {
113+
// "view" allows role to not be defined. It is a special dataset access construct, so ignore
114+
// If a user wants to manage "view" access they should use the `bigquery_dataset_access` resource
115+
continue
116+
}
117+
role := rawRole.(string)
118+
if iamRole, ok := bigqueryAccessPrimitiveToRoleMap[role]; ok {
119+
// API changes certain IAM roles to legacy roles. Revert these changes
120+
role = iamRole
121+
}
122+
member, err := accessToIamMember(memberRole)
123+
if err != nil {
124+
return nil, err
125+
}
126+
// We have to combine bindings manually
127+
binding, ok := roleToBinding[role]
128+
if !ok {
129+
binding = &cloudresourcemanager.Binding{Role: role, Members: []string{}}
130+
}
131+
binding.Members = append(binding.Members, member)
132+
133+
roleToBinding[role] = binding
134+
}
135+
bindings := make([]*cloudresourcemanager.Binding, 0)
136+
for _, v := range roleToBinding {
137+
bindings = append(bindings, v)
138+
}
139+
140+
return &cloudresourcemanager.Policy{Bindings: bindings}, nil
141+
}
142+
143+
func policyToAccess(policy *cloudresourcemanager.Policy) ([]map[string]interface{}, error) {
144+
res := make([]map[string]interface{}, 0)
145+
if len(policy.AuditConfigs) != 0 {
146+
return nil, errors.New("Access policies not allowed on BigQuery Dataset IAM policies")
147+
}
148+
for _, binding := range policy.Bindings {
149+
if binding.Condition != nil {
150+
return nil, errors.New("IAM conditions not allowed on BigQuery Dataset IAM")
151+
}
152+
if fullRole, ok := bigqueryAccessPrimitiveToRoleMap[binding.Role]; ok {
153+
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)
154+
}
155+
for _, member := range binding.Members {
156+
access := map[string]interface{}{
157+
"role": binding.Role,
158+
}
159+
memberType, member, err := iamMemberToAccess(member)
160+
if err != nil {
161+
return nil, err
162+
}
163+
access[memberType] = member
164+
res = append(res, access)
165+
}
166+
}
167+
168+
return res, nil
169+
}
170+
171+
// Returns the member access type and member for an IAM member.
172+
// Dataset access uses different member types to identify groups, domains, etc.
173+
// these types are used as keys in the access JSON payload
174+
func iamMemberToAccess(member string) (string, string, error) {
175+
pieces := strings.SplitN(member, ":", 2)
176+
if len(pieces) > 1 {
177+
switch pieces[0] {
178+
case "group":
179+
return "groupByEmail", pieces[1], nil
180+
case "domain":
181+
return "domain", pieces[1], nil
182+
case "user":
183+
return "userByEmail", pieces[1], nil
184+
case "serviceAccount":
185+
return "userByEmail", pieces[1], nil
186+
default:
187+
return "", "", fmt.Errorf("Failed to parse BigQuery Dataset IAM member type: %s", member)
188+
}
189+
}
190+
if member == "projectOwners" || member == "projectReaders" || member == "projectWriters" || member == "allAuthenticatedUsers" {
191+
// These are special BigQuery Dataset permissions
192+
return "specialGroup", member, nil
193+
}
194+
return "iamMember", member, nil
195+
}
196+
197+
func accessToIamMember(access map[string]interface{}) (string, error) {
198+
// One of the fields must be set, we have to find which IAM member type this maps to
199+
if member, ok := access["groupByEmail"]; ok {
200+
return fmt.Sprintf("group:%s", member.(string)), nil
201+
}
202+
if member, ok := access["domain"]; ok {
203+
return fmt.Sprintf("domain:%s", member.(string)), nil
204+
}
205+
if member, ok := access["specialGroup"]; ok {
206+
return member.(string), nil
207+
}
208+
if member, ok := access["iamMember"]; ok {
209+
return member.(string), nil
210+
}
211+
if _, ok := access["view"]; ok {
212+
// view does not map to an IAM member, use access instead
213+
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
214+
}
215+
if member, ok := access["userByEmail"]; ok {
216+
// service accounts have "gservice" in their email. This is best guess due to lost information
217+
if strings.Contains(member.(string), "gserviceaccount") {
218+
return fmt.Sprintf("serviceAccount:%s", member.(string)), nil
219+
}
220+
return fmt.Sprintf("user:%s", member.(string)), nil
221+
}
222+
return "", fmt.Errorf("Failed to identify IAM member from BigQuery Dataset access: %v", access)
223+
}
224+
225+
func (u *BigqueryDatasetIamUpdater) GetResourceId() string {
226+
return fmt.Sprintf("projects/%s/datasets/%s", u.project, u.datasetId)
227+
}
228+
229+
// Matches the mutex of google_big_query_dataset_access
230+
func (u *BigqueryDatasetIamUpdater) GetMutexKey() string {
231+
return fmt.Sprintf("%s", u.datasetId)
232+
}
233+
234+
func (u *BigqueryDatasetIamUpdater) DescribeResource() string {
235+
return fmt.Sprintf("Bigquery Dataset %s/%s", u.project, u.datasetId)
236+
}

google-beta/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,9 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) {
903903
"google_bigtable_instance_iam_member": ResourceIamMember(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
904904
"google_bigtable_instance_iam_policy": ResourceIamPolicy(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
905905
"google_bigtable_table": resourceBigtableTable(),
906+
"google_bigquery_dataset_iam_binding": ResourceIamBinding(IamBigqueryDatasetSchema, NewBigqueryDatasetIamUpdater, BigqueryDatasetIdParseFunc),
907+
"google_bigquery_dataset_iam_member": ResourceIamMember(IamBigqueryDatasetSchema, NewBigqueryDatasetIamUpdater, BigqueryDatasetIdParseFunc),
908+
"google_bigquery_dataset_iam_policy": ResourceIamPolicy(IamBigqueryDatasetSchema, NewBigqueryDatasetIamUpdater, BigqueryDatasetIdParseFunc),
906909
"google_billing_account_iam_binding": ResourceIamBinding(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
907910
"google_billing_account_iam_member": ResourceIamMember(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
908911
"google_billing_account_iam_policy": ResourceIamPolicy(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
8+
)
9+
10+
func TestAccBigqueryDatasetIamMember_basic(t *testing.T) {
11+
t.Parallel()
12+
13+
datasetID := fmt.Sprintf("tf_test_%s", randString(t, 10))
14+
saID := fmt.Sprintf("tf-test-%s", randString(t, 10))
15+
16+
expected := map[string]interface{}{
17+
"role": "roles/viewer",
18+
"userByEmail": fmt.Sprintf("%s@%s.iam.gserviceaccount.com", saID, getTestProjectFromEnv()),
19+
}
20+
21+
vcrTest(t, resource.TestCase{
22+
PreCheck: func() { testAccPreCheck(t) },
23+
Providers: testAccProviders,
24+
Steps: []resource.TestStep{
25+
{
26+
Config: testAccBigqueryDatasetIamMember_basic(datasetID, saID),
27+
Check: testAccCheckBigQueryDatasetAccessPresent(t, "google_bigquery_dataset.dataset", expected),
28+
},
29+
{
30+
// Destroy step instead of CheckDestroy so we can check the access is removed without deleting the dataset
31+
Config: testAccBigqueryDatasetIamMember_destroy(datasetID, "dataset"),
32+
Check: testAccCheckBigQueryDatasetAccessAbsent(t, "google_bigquery_dataset.dataset", expected),
33+
},
34+
},
35+
})
36+
}
37+
38+
func testAccBigqueryDatasetIamMember_destroy(datasetID, rs string) string {
39+
return fmt.Sprintf(`
40+
resource "google_bigquery_dataset" "%s" {
41+
dataset_id = "%s"
42+
}
43+
`, rs, datasetID)
44+
}
45+
46+
func testAccBigqueryDatasetIamMember_basic(datasetID, saID string) string {
47+
return fmt.Sprintf(`
48+
resource "google_bigquery_dataset_iam_member" "access" {
49+
dataset_id = google_bigquery_dataset.dataset.dataset_id
50+
role = "roles/viewer"
51+
member = "serviceAccount:${google_service_account.bqviewer.email}"
52+
}
53+
54+
resource "google_bigquery_dataset" "dataset" {
55+
dataset_id = "%s"
56+
}
57+
58+
resource "google_service_account" "bqviewer" {
59+
account_id = "%s"
60+
}
61+
`, datasetID, saID)
62+
}

0 commit comments

Comments
 (0)