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: support authorized dataset entity #1075

Merged
merged 10 commits into from
Dec 14, 2021
61 changes: 27 additions & 34 deletions google/cloud/bigquery/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,29 @@ def _get_routine_reference(self, routine_id):
class AccessEntry(object):
tswast marked this conversation as resolved.
Show resolved Hide resolved
"""Represents grant of an access role to an entity.

An entry must have exactly one of the allowed :attr:`ENTITY_TYPES`. If
anything but ``view`` or ``routine`` are set, a ``role`` is also required.
``role`` is omitted for ``view`` and ``routine``, because they are always
read-only.
An entry must have exactly one of the allowed
:class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``,
or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``,
``routine``, ``dataset``, because they are always read-only.

See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets.

Args:
role (str):
Role granted to the entity. The following string values are
supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be
:data:`None` if the ``entity_type`` is ``view`` or ``routine``.
:data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``.

entity_type (str):
Type of entity being granted the role. One of :attr:`ENTITY_TYPES`.
Type of entity being granted the role. See
:class:`google.cloud.bigquery.enums.EntityTypes` for supported types.

entity_id (Union[str, Dict[str, str]]):
If the ``entity_type`` is not 'view' or 'routine', the ``entity_id``
is the ``str`` ID of the entity being granted the role. If the
``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
representing the view or routine from a different dataset to grant
access to in the following format for views::
If the ``entity_type`` is not 'view', 'routine', or 'dataset', the
``entity_id`` is the ``str`` ID of the entity being granted the role. If
the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
representing the view or routine from a different dataset to grant access
to in the following format for views::

{
'projectId': string,
Expand All @@ -114,11 +115,22 @@ class AccessEntry(object):
'routineId': string
}

If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes
a 'dataset' field with a ``dict`` representing the dataset and a 'target_types'
field with a ``str`` value of the dataset's resource type::

{
'dataset': {
'projectId': string,
'datasetId': string,
},
'target_types: 'VIEWS'
}

Raises:
ValueError:
If the ``entity_type`` is not among :attr:`ENTITY_TYPES`, or if a
``view`` or a ``routine`` has ``role`` set, or a non ``view`` and
non ``routine`` **does not** have a ``role`` set.
If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``,
non ``routine``, and non ``dataset`` **does not** have a ``role`` set.

Examples:
>>> entry = AccessEntry('OWNER', 'userByEmail', '[email protected]')
Expand All @@ -131,27 +143,8 @@ class AccessEntry(object):
>>> entry = AccessEntry(None, 'view', view)
"""

ENTITY_TYPES = frozenset(
[
"userByEmail",
"groupByEmail",
"domain",
"specialGroup",
"view",
"iamMember",
"routine",
]
)
"""Allowed entity types."""

def __init__(self, role, entity_type, entity_id):
if entity_type not in self.ENTITY_TYPES:
message = "Entity type %r not among: %s" % (
entity_type,
", ".join(self.ENTITY_TYPES),
)
raise ValueError(message)
if entity_type in ("view", "routine"):
if entity_type in ("view", "routine", "dataset"):
if role is not None:
raise ValueError(
"Role must be None for a %r. Received "
Expand Down
13 changes: 13 additions & 0 deletions google/cloud/bigquery/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,19 @@ def _make_sql_scalars_enum():
StandardSqlDataTypes = _make_sql_scalars_enum()


class EntityTypes:
steffnay marked this conversation as resolved.
Show resolved Hide resolved
"""Enum of allowed entity type names in AccessEntry"""

USER_BY_EMAIL = "userByEmail"
GROUP_BY_EMAIL = "groupByEmail"
DOMAIN = "domain"
DATASET = "dataset"
SPECIAL_GROUP = "specialGroup"
VIEW = "view"
IAM_MEMBER = "iamMember"
ROUTINE = "routine"


# See also: https://cloud.google.com/bigquery/data-types#legacy_sql_data_types
# and https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
class SqlTypeNames(str, enum.Enum):
Expand Down
27 changes: 22 additions & 5 deletions tests/unit/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,35 @@ def test_to_api_repr_routine(self):
exp_resource = {"routine": routine}
self.assertEqual(resource, exp_resource)

def test_to_api_repr_dataset(self):
dataset = {
"dataset": {"projectId": "my-project", "datasetId": "my_dataset"},
"target_types": "VIEWS",
}
entry = self._make_one(None, "dataset", dataset)
resource = entry.to_api_repr()
exp_resource = {"dataset": dataset}
self.assertEqual(resource, exp_resource)

def test_to_api_w_incorrect_role(self):
dataset = {
"dataset": {
"projectId": "my-project",
"datasetId": "my_dataset",
"tableId": "my_table",
},
"target_type": "VIEW",
}
with self.assertRaises(ValueError):
self._make_one("READER", "dataset", dataset)

def test_from_api_repr(self):
resource = {"role": "OWNER", "userByEmail": "[email protected]"}
entry = self._get_target_class().from_api_repr(resource)
self.assertEqual(entry.role, "OWNER")
self.assertEqual(entry.entity_type, "userByEmail")
self.assertEqual(entry.entity_id, "[email protected]")

def test_from_api_repr_w_unknown_entity_type(self):
steffnay marked this conversation as resolved.
Show resolved Hide resolved
resource = {"role": "READER", "unknown": "UNKNOWN"}
with self.assertRaises(ValueError):
self._get_target_class().from_api_repr(resource)

def test_from_api_repr_entries_w_extra_keys(self):
resource = {
"role": "READER",
Expand Down