Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support BigQuery authorized routines #6680

Merged
merged 11 commits into from
Nov 8, 2022
65 changes: 65 additions & 0 deletions mmv1/products/bigquery/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,30 @@ objects:
but additional target types may be added in the future. Possible values: VIEWS
item_type: Api::Type::String
required: true
- !ruby/object:Api::Type::NestedObject
name: 'routine'
description: |
A routine from a different dataset to grant access to. Queries
executed against that routine will have read access to tables in
this dataset. The role field is not required when this field is
set. If that routine is updated by any user, access to the routine
needs to be granted again via an update operation.
properties:
- !ruby/object:Api::Type::String
name: 'datasetId'
description: The ID of the dataset containing this table.
required: true
- !ruby/object:Api::Type::String
name: 'projectId'
description: The ID of the project containing this table.
required: true
- !ruby/object:Api::Type::String
name: 'routineId'
description: |
The ID of the routine. The ID must contain only letters (a-z,
A-Z), numbers (0-9), or underscores (_). The maximum length
is 256 characters.
required: true
- !ruby/object:Api::Type::Integer
name: 'creationTime'
output: true
Expand Down Expand Up @@ -275,6 +299,7 @@ objects:
- iamMember
- view
- dataset
- routine
description: |
Gives dataset access for a single entity. This resource is intended to be used in cases where
it is not possible to compile a full list of access blocks to include in a
Expand Down Expand Up @@ -317,6 +342,7 @@ objects:
- iam_member
- view
- dataset
- routine
- !ruby/object:Api::Type::String
name: 'groupByEmail'
description: An email address of a Google Group to grant access to.
Expand All @@ -328,6 +354,7 @@ objects:
- iam_member
- view
- dataset
- routine
- !ruby/object:Api::Type::String
name: 'domain'
description: |
Expand All @@ -341,6 +368,7 @@ objects:
- iam_member
- view
- dataset
- routine
- !ruby/object:Api::Type::String
name: 'specialGroup'
description: |
Expand All @@ -365,6 +393,7 @@ objects:
- iam_member
- view
- dataset
- routine
- !ruby/object:Api::Type::String
name: 'iamMember'
description: |
Expand All @@ -378,6 +407,7 @@ objects:
- iam_member
- view
- dataset
- routine
- !ruby/object:Api::Type::NestedObject
name: 'view'
description: |
Expand All @@ -394,6 +424,7 @@ objects:
- iam_member
- view
- dataset
- routine
properties:
- !ruby/object:Api::Type::String
name: 'datasetId'
Expand Down Expand Up @@ -422,6 +453,7 @@ objects:
- iam_member
- view
- dataset
- routine
properties:
- !ruby/object:Api::Type::NestedObject
name: 'dataset'
Expand All @@ -444,6 +476,39 @@ objects:
but additional target types may be added in the future. Possible values: VIEWS
item_type: Api::Type::String
required: true
- !ruby/object:Api::Type::NestedObject
name: 'routine'
description: |
A routine from a different dataset to grant access to. Queries
executed against that routine will have read access to tables in
this dataset. The role field is not required when this field is
set. If that routine is updated by any user, access to the routine
needs to be granted again via an update operation.
exactly_one_of:
- user_by_email
- group_by_email
- domain
- special_group
- iam_member
- view
- dataset
- routine
properties:
- !ruby/object:Api::Type::String
name: 'datasetId'
description: The ID of the dataset containing this table.
required: true
- !ruby/object:Api::Type::String
name: 'projectId'
description: The ID of the project containing this table.
required: true
- !ruby/object:Api::Type::String
name: 'routineId'
description: |
The ID of the routine. The ID must contain only letters (a-z,
A-Z), numbers (0-9), or underscores (_). The maximum length
is 256 characters.
required: true
- !ruby/object:Api::Resource
name: 'Job'
kind: 'bigquery#job'
Expand Down
18 changes: 18 additions & 0 deletions mmv1/products/bigquery/terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ overrides: !ruby/object:Overrides::ResourceOverrides
private: "private"
public: "public"
account_name: "bqowner"
- !ruby/object:Provider::Terraform::Examples
name: "bigquery_dataset_authorized_routine"
primary_resource_id: "private"
vars:
private_dataset: "private_dataset"
public_dataset: "public_dataset"
public_routine: "public_routine"
test_env_vars:
service_account: :SERVICE_ACCT
virtual_fields:
- !ruby/object:Api::Type::Boolean
name: 'delete_contents_on_destroy'
Expand Down Expand Up @@ -109,6 +118,15 @@ overrides: !ruby/object:Overrides::ResourceOverrides
vars:
private: "private"
public: "public"
- !ruby/object:Provider::Terraform::Examples
name: "bigquery_dataset_access_authorized_routine"
skip_test: true # not importable
primary_resource_type: "google_bigquery_dataset_access"
primary_resource_id: "authorized_routine"
vars:
private_dataset: "private_dataset"
public_dataset: "public_dataset"
public_routine: "public_routine"
properties:
datasetId: !ruby/object:Overrides::Terraform::PropertyOverride
ignore_read: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
resource "google_bigquery_dataset" "public" {
dataset_id = "<%= ctx[:vars]['public_dataset'] %>"
description = "This dataset is public"
}

resource "google_bigquery_routine" "public" {
dataset_id = google_bigquery_dataset.public.dataset_id
routine_id = "<%= ctx[:vars]['public_routine'] %>"
routine_type = "TABLE_VALUED_FUNCTION"
language = "SQL"
definition_body = <<-EOS
SELECT 1 + value AS value
EOS
arguments {
name = "value"
argument_kind = "FIXED_TYPE"
data_type = jsonencode({ "typeKind" = "INT64" })
}
return_table_type = jsonencode({ "columns" = [
{ "name" = "value", "type" = { "typeKind" = "INT64" } },
] })
}

resource "google_bigquery_dataset" "private" {
dataset_id = "<%= ctx[:vars]['private_dataset'] %>"
description = "This dataset is private"
}

resource "google_bigquery_dataset_access" "authorized_routine" {
dataset_id = google_bigquery_dataset.private.dataset_id
routine {
project_id = google_bigquery_routine.public.project
dataset_id = google_bigquery_routine.public.dataset_id
routine_id = google_bigquery_routine.public.routine_id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
resource "google_bigquery_dataset" "public" {
dataset_id = "<%= ctx[:vars]['public_dataset'] %>"
description = "This dataset is public"
}

resource "google_bigquery_routine" "public" {
dataset_id = google_bigquery_dataset.public.dataset_id
routine_id = "<%= ctx[:vars]['public_routine'] %>"
routine_type = "TABLE_VALUED_FUNCTION"
language = "SQL"
definition_body = <<-EOS
SELECT 1 + value AS value
EOS
arguments {
name = "value"
argument_kind = "FIXED_TYPE"
data_type = jsonencode({ "typeKind" = "INT64" })
}
return_table_type = jsonencode({ "columns" = [
{ "name" = "value", "type" = { "typeKind" = "INT64" } },
] })
}

resource "google_bigquery_dataset" "private" {
dataset_id = "<%= ctx[:vars]['private_dataset'] %>"
description = "This dataset is private"
access {
role = "OWNER"
user_by_email = "<%= ctx[:test_env_vars]['service_account'] %>"
}
access {
routine {
project_id = google_bigquery_routine.public.project
dataset_id = google_bigquery_routine.public.dataset_id
routine_id = google_bigquery_routine.public.routine_id
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,42 @@ func TestAccBigQueryDatasetAccess_authorizedDataset(t *testing.T) {
})
}

func TestAccBigQueryDatasetAccess_authorizedRoutine(t *testing.T) {
// Multiple fine-grained resources
skipIfVcr(t)
t.Parallel()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these can't run in VCR based on the surrounding tests.

Suggested change
t.Parallel()
// Multiple fine-grained resources
skipIfVcr(t)
t.Parallel()


context := map[string]interface{}{
"public_dataset": fmt.Sprintf("tf_test_public_dataset_%s", randString(t, 10)),
"public_routine": fmt.Sprintf("tf_test_public_routine_%s", randString(t, 10)),
"private_dataset": fmt.Sprintf("tf_test_private_dataset_%s", randString(t, 10)),
}

expected := map[string]interface{}{
"routine": map[string]interface{}{
"projectId": getTestProjectFromEnv(),
"datasetId": context["public_dataset"],
"routineId": context["public_routine"],
},
}

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccBigQueryDatasetAccess_authorizedRoutine(context),
Check: testAccCheckBigQueryDatasetAccessPresent(t, "google_bigquery_dataset.private", expected),
},
{
// Destroy step instead of CheckDestroy so we can check the access is removed without deleting the dataset
Config: testAccBigQueryDatasetAccess_destroy(context["private_dataset"].(string), "private_dataset"),
Tei1988 marked this conversation as resolved.
Show resolved Hide resolved
Check: testAccCheckBigQueryDatasetAccessAbsent(t, "google_bigquery_dataset.private", expected),
},
},
})
}

func TestAccBigQueryDatasetAccess_multiple(t *testing.T) {
// Multiple fine-grained resources
skipIfVcr(t)
Expand Down Expand Up @@ -358,6 +394,47 @@ resource "google_bigquery_dataset" "public" {
`, datasetID, datasetID2)
}

func testAccBigQueryDatasetAccess_authorizedRoutine(context map[string]interface{}) string {
return Nprintf(`
resource "google_bigquery_dataset" "public" {
dataset_id = "%{public_dataset}"
description = "This dataset is public"
}

resource "google_bigquery_routine" "public" {
dataset_id = google_bigquery_dataset.public.dataset_id
routine_id = "%{public_routine}"
routine_type = "TABLE_VALUED_FUNCTION"
language = "SQL"
definition_body = <<-EOS
SELECT 1 + value AS value
EOS
arguments {
name = "value"
argument_kind = "FIXED_TYPE"
data_type = jsonencode({ "typeKind" = "INT64" })
}
return_table_type = jsonencode({ "columns" = [
{ "name" = "value", "type" = { "typeKind" = "INT64" } },
] })
}

resource "google_bigquery_dataset" "private" {
dataset_id = "%{private_dataset}"
description = "This dataset is private"
}

resource "google_bigquery_dataset_access" "authorized_routine" {
dataset_id = google_bigquery_dataset.private.dataset_id
routine {
project_id = google_bigquery_routine.public.project
dataset_id = google_bigquery_routine.public.dataset_id
routine_id = google_bigquery_routine.public.routine_id
}
}
`, context)
}

func testAccBigQueryDatasetAccess_multiple(datasetID string) string {
return fmt.Sprintf(`
resource "google_bigquery_dataset_access" "access" {
Expand Down
4 changes: 4 additions & 0 deletions mmv1/third_party/terraform/utils/iam_bigquery_dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ func accessToIamMember(access map[string]interface{}) (string, error) {
// dataset does not map to an IAM member, use access instead
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
}
if _, ok := access["routine"]; ok {
// dataset does not map to an IAM member, use access instead
return "", fmt.Errorf("Failed to convert BigQuery Dataset access to IAM member. To use views with a dataset, please use dataset_access")
}
if member, ok := access["userByEmail"]; ok {
// service accounts have "gservice" in their email. This is best guess due to lost information
if strings.Contains(member.(string), "gserviceaccount") {
Expand Down