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

feat: add support for getting and setting table IAM policy #144

Merged
merged 16 commits into from
Jul 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions google/cloud/bigquery/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

import google.api_core.client_options
import google.api_core.exceptions
from google.api_core.iam import Policy
from google.api_core import page_iterator
import google.cloud._helpers
from google.cloud import exceptions
Expand Down Expand Up @@ -605,6 +606,63 @@ def get_dataset(self, dataset_ref, retry=DEFAULT_RETRY, timeout=None):
)
return Dataset.from_api_repr(api_response)

def get_iam_policy(
self, table, requested_policy_version=1, retry=DEFAULT_RETRY, timeout=None,
):
if not isinstance(table, (Table, TableReference)):
raise TypeError("table must be a Table or TableReference")

if requested_policy_version != 1:
raise ValueError("only IAM policy version 1 is supported")

body = {"options": {"requestedPolicyVersion": 1}}

path = "{}:getIamPolicy".format(table.path)

response = self._call_api(
retry, method="POST", path=path, data=body, timeout=timeout,
)

return Policy.from_api_repr(response)

def set_iam_policy(
self, table, policy, updateMask=None, retry=DEFAULT_RETRY, timeout=None,
):
if not isinstance(table, (Table, TableReference)):
raise TypeError("table must be a Table or TableReference")

if not isinstance(policy, (Policy)):
raise TypeError("policy must be a Policy")

body = {"policy": policy.to_api_repr()}

if updateMask is not None:
body["updateMask"] = updateMask

path = "{}:setIamPolicy".format(table.path)

response = self._call_api(
retry, method="POST", path=path, data=body, timeout=timeout,
)

return Policy.from_api_repr(response)

def test_iam_permissions(
self, table, permissions, retry=DEFAULT_RETRY, timeout=None,
):
if not isinstance(table, (Table, TableReference)):
raise TypeError("table must be a Table or TableReference")

body = {"permissions": permissions}

path = "{}:testIamPermissions".format(table.path)

response = self._call_api(
retry, method="POST", path=path, data=body, timeout=timeout,
)

return response

def get_model(self, model_ref, retry=DEFAULT_RETRY, timeout=None):
"""[Beta] Fetch the model referenced by ``model_ref``.

Expand Down
38 changes: 38 additions & 0 deletions google/cloud/bigquery/iam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""BigQuery API IAM policy definitions

For all allowed roles and permissions, see:

https://cloud.google.com/bigquery/docs/access-control
"""

# BigQuery-specific IAM roles available for tables and views

BIGQUERY_DATA_EDITOR_ROLE = "roles/bigquery.dataEditor"
"""When applied to a table or view, this role provides permissions to
read and update data and metadata for the table or view."""

BIGQUERY_DATA_OWNER_ROLE = "roles/bigquery.dataOwner"
"""When applied to a table or view, this role provides permissions to
read and update data and metadata for the table or view, share the
table/view, and delete the table/view."""

BIGQUERY_DATA_VIEWER_ROLE = "roles/bigquery.dataViewer"
"""When applied to a table or view, this role provides permissions to
read data and metadata from the table or view."""

BIGQUERY_METADATA_VIEWER_ROLE = "roles/bigquery.metadataViewer"
"""When applied to a table or view, this role provides persmissions to
read metadata from the table or view."""
49 changes: 49 additions & 0 deletions tests/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from google.api_core.exceptions import InternalServerError
from google.api_core.exceptions import ServiceUnavailable
from google.api_core.exceptions import TooManyRequests
from google.api_core.iam import Policy
from google.cloud import bigquery
from google.cloud import bigquery_v2
from google.cloud.bigquery.dataset import Dataset
Expand Down Expand Up @@ -1407,6 +1408,54 @@ def test_copy_table(self):
got_rows = self._fetch_single_page(dest_table)
self.assertTrue(len(got_rows) > 0)

def test_get_set_iam_policy(self):
from google.cloud.bigquery.iam import BIGQUERY_DATA_VIEWER_ROLE

dataset = self.temp_dataset(_make_dataset_id("create_table"))
table_id = "test_table"
table_ref = Table(dataset.table(table_id))
self.assertFalse(_table_exists(table_ref))

table = retry_403(Config.CLIENT.create_table)(table_ref)
self.to_delete.insert(0, table)

self.assertTrue(_table_exists(table))

member = "serviceAccount:{}".format(Config.CLIENT.get_service_account_email())
BINDING = {
"role": BIGQUERY_DATA_VIEWER_ROLE,
"members": {member},
}

policy = Config.CLIENT.get_iam_policy(table)
self.assertIsInstance(policy, Policy)
self.assertEqual(policy.bindings, [])

policy.bindings.append(BINDING)
returned_policy = Config.CLIENT.set_iam_policy(table, policy)
self.assertEqual(returned_policy.bindings, policy.bindings)

def test_test_iam_permissions(self):
dataset = self.temp_dataset(_make_dataset_id("create_table"))
table_id = "test_table"
table_ref = Table(dataset.table(table_id))
self.assertFalse(_table_exists(table_ref))

table = retry_403(Config.CLIENT.create_table)(table_ref)
self.to_delete.insert(0, table)

self.assertTrue(_table_exists(table))

# Test some default permissions.
permissions = [
"bigquery.tables.get",
"bigquery.tables.getData",
"bigquery.tables.update",
]

response = Config.CLIENT.test_iam_permissions(table, [permissions])
self.assertEqual(set(response["permissions"]), set(permissions))

def test_job_cancel(self):
DATASET_ID = _make_dataset_id("job_cancel")
JOB_ID_PREFIX = "fetch_" + DATASET_ID
Expand Down
210 changes: 210 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,216 @@ def test_get_table_sets_user_agent(self):
)
self.assertIn("my-application/1.2.3", expected_user_agent)

def test_get_iam_policy(self):
from google.cloud.bigquery.iam import BIGQUERY_DATA_OWNER_ROLE
from google.cloud.bigquery.iam import BIGQUERY_DATA_EDITOR_ROLE
from google.cloud.bigquery.iam import BIGQUERY_DATA_VIEWER_ROLE
from google.api_core.iam import Policy

PATH = "/projects/{}/datasets/{}/tables/{}:getIamPolicy".format(
self.PROJECT, self.DS_ID, self.TABLE_ID,
)
steffnay marked this conversation as resolved.
Show resolved Hide resolved
BODY = {"options": {"requestedPolicyVersion": 1}}
ETAG = "CARDI"
VERSION = 1
OWNER1 = "user:phred@example.com"
OWNER2 = "group:cloud-logs@google.com"
EDITOR1 = "domain:google.com"
EDITOR2 = "user:phred@example.com"
VIEWER1 = "serviceAccount:1234-abcdef@service.example.com"
VIEWER2 = "user:phred@example.com"
RETURNED = {
"resourceId": PATH,
"etag": ETAG,
"version": VERSION,
"bindings": [
{"role": BIGQUERY_DATA_OWNER_ROLE, "members": [OWNER1, OWNER2]},
{"role": BIGQUERY_DATA_EDITOR_ROLE, "members": [EDITOR1, EDITOR2]},
{"role": BIGQUERY_DATA_VIEWER_ROLE, "members": [VIEWER1, VIEWER2]},
],
}
EXPECTED = {
binding["role"]: set(binding["members"]) for binding in RETURNED["bindings"]
}

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
conn = client._connection = make_connection(RETURNED)

policy = client.get_iam_policy(self.TABLE_REF, timeout=7.5)

conn.api_request.assert_called_once_with(
method="POST", path=PATH, data=BODY, timeout=7.5
)

self.assertIsInstance(policy, Policy)
self.assertEqual(policy.etag, RETURNED["etag"])
self.assertEqual(policy.version, RETURNED["version"])
self.assertEqual(dict(policy), EXPECTED)

def test_get_iam_policy_w_invalid_table(self):
creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)

table_resource_string = "projects/{}/datasets/{}/tables/{}".format(
self.PROJECT, self.DS_ID, self.TABLE_ID,
)

with self.assertRaises(TypeError):
client.get_iam_policy(table_resource_string)

def test_get_iam_policy_w_invalid_version(self):
creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)

with self.assertRaises(ValueError):
client.get_iam_policy(self.TABLE_REF, requested_policy_version=2)

def test_set_iam_policy(self):
from google.cloud.bigquery.iam import BIGQUERY_DATA_OWNER_ROLE
from google.cloud.bigquery.iam import BIGQUERY_DATA_EDITOR_ROLE
from google.cloud.bigquery.iam import BIGQUERY_DATA_VIEWER_ROLE
from google.api_core.iam import Policy

PATH = "/projects/%s/datasets/%s/tables/%s:setIamPolicy" % (
self.PROJECT,
self.DS_ID,
self.TABLE_ID,
)
ETAG = "foo"
VERSION = 1
OWNER1 = "user:phred@example.com"
OWNER2 = "group:cloud-logs@google.com"
EDITOR1 = "domain:google.com"
EDITOR2 = "user:phred@example.com"
VIEWER1 = "serviceAccount:1234-abcdef@service.example.com"
VIEWER2 = "user:phred@example.com"
BINDINGS = [
{"role": BIGQUERY_DATA_OWNER_ROLE, "members": [OWNER1, OWNER2]},
{"role": BIGQUERY_DATA_EDITOR_ROLE, "members": [EDITOR1, EDITOR2]},
{"role": BIGQUERY_DATA_VIEWER_ROLE, "members": [VIEWER1, VIEWER2]},
]
MASK = "bindings,etag"
RETURNED = {"etag": ETAG, "version": VERSION, "bindings": BINDINGS}

policy = Policy()
for binding in BINDINGS:
policy[binding["role"]] = binding["members"]

BODY = {"policy": policy.to_api_repr(), "updateMask": MASK}

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
conn = client._connection = make_connection(RETURNED)

returned_policy = client.set_iam_policy(
self.TABLE_REF, policy, updateMask=MASK, timeout=7.5
)

conn.api_request.assert_called_once_with(
method="POST", path=PATH, data=BODY, timeout=7.5
)
self.assertEqual(returned_policy.etag, ETAG)
self.assertEqual(returned_policy.version, VERSION)
self.assertEqual(dict(returned_policy), dict(policy))

def test_set_iam_policy_no_mask(self):
from google.api_core.iam import Policy

PATH = "/projects/%s/datasets/%s/tables/%s:setIamPolicy" % (
self.PROJECT,
self.DS_ID,
self.TABLE_ID,
)
RETURNED = {"etag": "foo", "version": 1, "bindings": []}

policy = Policy()
BODY = {"policy": policy.to_api_repr()}

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
conn = client._connection = make_connection(RETURNED)

client.set_iam_policy(self.TABLE_REF, policy, timeout=7.5)

conn.api_request.assert_called_once_with(
method="POST", path=PATH, data=BODY, timeout=7.5
)

def test_set_iam_policy_invalid_policy(self):
from google.api_core.iam import Policy

policy = Policy()
invalid_policy_repr = policy.to_api_repr()

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)

with self.assertRaises(TypeError):
client.set_iam_policy(self.TABLE_REF, invalid_policy_repr)

def test_set_iam_policy_w_invalid_table(self):
from google.api_core.iam import Policy

policy = Policy()

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)

table_resource_string = "projects/%s/datasets/%s/tables/%s" % (
self.PROJECT,
self.DS_ID,
self.TABLE_ID,
)

with self.assertRaises(TypeError):
client.set_iam_policy(table_resource_string, policy)

def test_test_iam_permissions(self):
PATH = "/projects/%s/datasets/%s/tables/%s:testIamPermissions" % (
self.PROJECT,
self.DS_ID,
self.TABLE_ID,
)

PERMISSIONS = ["bigquery.tables.get", "bigquery.tables.update"]
BODY = {"permissions": PERMISSIONS}
RETURNED = {"permissions": PERMISSIONS}

creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
conn = client._connection = make_connection(RETURNED)

client.test_iam_permissions(self.TABLE_REF, PERMISSIONS, timeout=7.5)

conn.api_request.assert_called_once_with(
method="POST", path=PATH, data=BODY, timeout=7.5
)

def test_test_iam_permissions_w_invalid_table(self):
creds = _make_credentials()
http = object()
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)

table_resource_string = "projects/%s/datasets/%s/tables/%s" % (
self.PROJECT,
self.DS_ID,
self.TABLE_ID,
)

PERMISSIONS = ["bigquery.tables.get", "bigquery.tables.update"]

with self.assertRaises(TypeError):
client.test_iam_permissions(table_resource_string, PERMISSIONS)

def test_update_dataset_w_invalid_field(self):
from google.cloud.bigquery.dataset import Dataset

Expand Down