Skip to content

Commit

Permalink
Merge pull request #1046 from tseaver/bigquery-dataset_acls
Browse files Browse the repository at this point in the history
Add Dataset access support
  • Loading branch information
tseaver committed Aug 11, 2015
2 parents 3357530 + 6720b35 commit 2d017de
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 2 deletions.
86 changes: 85 additions & 1 deletion gcloud/bigquery/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@
from gcloud.bigquery.table import Table


class AccessGrant(object):
"""Represent grant of an access role to an entity.
:type role: string (one of 'OWNER', 'WRITER', 'READER').
:param role: role granted to the entity.
:type entity_type: string (one of 'specialGroup', 'groupByEmail', or
'userByEmail')
:param entity_type: type of entity being granted the role.
:type entity_id: string
:param entity_id: ID of entity being granted the role.
"""
def __init__(self, role, entity_type, entity_id):
self.role = role
self.entity_type = entity_type
self.entity_id = entity_id

def __repr__(self):
return '<AccessGrant: role=%s, %s=%s>' % (
self.role, self.entity_type, self.entity_id)


class Dataset(object):
"""Datasets are containers for tables.
Expand All @@ -32,12 +55,16 @@ class Dataset(object):
:type client: :class:`gcloud.bigquery.client.Client`
:param client: A client which holds credentials and project configuration
for the dataset (which requires a project).
:type access_grants: list of :class:`AccessGrant`
:param access_grants: roles granted to entities for this dataset
"""

def __init__(self, name, client):
def __init__(self, name, client, access_grants=()):
self.name = name
self._client = client
self._properties = {}
self.access_grants = access_grants

@property
def project(self):
Expand All @@ -57,6 +84,29 @@ def path(self):
"""
return '/projects/%s/datasets/%s' % (self.project, self.name)

@property
def access_grants(self):
"""Dataset's access grants.
:rtype: list of :class:`AccessGrant`
:returns: roles granted to entities for this dataset
"""
return list(self._access_grants)

@access_grants.setter
def access_grants(self, value):
"""Update dataset's access grants
:type value: list of :class:`AccessGrant`
:param value: roles granted to entities for this dataset
:raises: TypeError if 'value' is not a sequence, or ValueError if
any item in the sequence is not an AccessGrant
"""
if not all(isinstance(field, AccessGrant) for field in value):
raise ValueError('Values must be AccessGrant instances')
self._access_grants = tuple(value)

@property
def created(self):
"""Datetime at which the dataset was created.
Expand Down Expand Up @@ -227,6 +277,27 @@ def _require_client(self, client):
client = self._client
return client

def _parse_access_grants(self, access):
"""Parse a resource fragment into a set of access grants.
:type access: list of mappings
:param access: each mapping represents a single access grant
:rtype: list of :class:`AccessGrant`
:returns: a list of parsed grants
"""
result = []
for grant in access:
grant = grant.copy()
role = grant.pop('role')
# Hypothetical case: we don't know that the back-end will ever
# return such structures, but they are logical. See:
# https://github.com/GoogleCloudPlatform/gcloud-python/pull/1046#discussion_r36687769
for entity_type, entity_id in sorted(grant.items()):
result.append(
AccessGrant(role, entity_type, entity_id))
return result

def _set_properties(self, api_response):
"""Update properties from resource in body of ``api_response``
Expand All @@ -235,12 +306,22 @@ def _set_properties(self, api_response):
"""
self._properties.clear()
cleaned = api_response.copy()
access = cleaned.pop('access', ())
self.access_grants = self._parse_access_grants(access)
if 'creationTime' in cleaned:
cleaned['creationTime'] = float(cleaned['creationTime'])
if 'lastModifiedTime' in cleaned:
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
self._properties.update(cleaned)

def _build_access_resource(self):
"""Generate a resource fragment for dataset's access grants."""
result = []
for grant in self.access_grants:
info = {'role': grant.role, grant.entity_type: grant.entity_id}
result.append(info)
return result

def _build_resource(self):
"""Generate a resource for ``create`` or ``update``."""
resource = {
Expand All @@ -260,6 +341,9 @@ def _build_resource(self):
if self.location is not None:
resource['location'] = self.location

if len(self.access_grants) > 0:
resource['access'] = self._build_access_resource()

return resource

def create(self, client=None):
Expand Down
123 changes: 122 additions & 1 deletion gcloud/bigquery/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
import unittest2


class TestAccessGrant(unittest2.TestCase):

def _getTargetClass(self):
from gcloud.bigquery.dataset import AccessGrant
return AccessGrant

def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)

def test_ctor_defaults(self):
grant = self._makeOne('OWNER', 'userByEmail', 'phred@example.com')
self.assertEqual(grant.role, 'OWNER')
self.assertEqual(grant.entity_type, 'userByEmail')
self.assertEqual(grant.entity_id, 'phred@example.com')


class TestDataset(unittest2.TestCase):
PROJECT = 'project'
DS_NAME = 'dataset-name'
Expand All @@ -39,6 +55,8 @@ def _setUpConstants(self):

def _makeResource(self):
self._setUpConstants()
USER_EMAIL = 'phred@example.com'
GROUP_EMAIL = 'group-name@lists.example.com'
return {
'creationTime': self.WHEN_TS * 1000,
'datasetReference':
Expand All @@ -48,10 +66,32 @@ def _makeResource(self):
'lastModifiedTime': self.WHEN_TS * 1000,
'location': 'US',
'selfLink': self.RESOURCE_URL,
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'OWNER', 'groupByEmail': GROUP_EMAIL},
{'role': 'WRITER', 'specialGroup': 'projectWriters'},
{'role': 'READER', 'specialGroup': 'projectReaders'}],
}

def _verifyResourceProperties(self, dataset, resource):
def _verifyAccessGrants(self, access_grants, resource):
r_grants = []
for r_grant in resource['access']:
role = r_grant.pop('role')
for entity_type, entity_id in sorted(r_grant.items()):
r_grants.append({'role': role,
'entity_type': entity_type,
'entity_id': entity_id})

self.assertEqual(len(access_grants), len(r_grants))
for a_grant, r_grant in zip(access_grants, r_grants):
self.assertEqual(a_grant.role, r_grant['role'])
self.assertEqual(a_grant.entity_type, r_grant['entity_type'])
self.assertEqual(a_grant.entity_id, r_grant['entity_id'])

def _verifyReadonlyResourceProperties(self, dataset, resource):

self.assertEqual(dataset.dataset_id, self.DS_ID)

if 'creationTime' in resource:
self.assertEqual(dataset.created, self.WHEN)
else:
Expand All @@ -69,12 +109,21 @@ def _verifyResourceProperties(self, dataset, resource):
else:
self.assertEqual(dataset.self_link, None)

def _verifyResourceProperties(self, dataset, resource):

self._verifyReadonlyResourceProperties(dataset, resource)

self.assertEqual(dataset.default_table_expiration_ms,
resource.get('defaultTableExpirationMs'))
self.assertEqual(dataset.description, resource.get('description'))
self.assertEqual(dataset.friendly_name, resource.get('friendlyName'))
self.assertEqual(dataset.location, resource.get('location'))

if 'access' in resource:
self._verifyAccessGrants(dataset.access_grants, resource)
else:
self.assertEqual(dataset.access_grants, [])

def test_ctor(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
Expand All @@ -84,6 +133,7 @@ def test_ctor(self):
self.assertEqual(
dataset.path,
'/projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME))
self.assertEqual(dataset.access_grants, [])

self.assertEqual(dataset.created, None)
self.assertEqual(dataset.dataset_id, None)
Expand All @@ -96,6 +146,29 @@ def test_ctor(self):
self.assertEqual(dataset.friendly_name, None)
self.assertEqual(dataset.location, None)

def test_access_roles_setter_non_list(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
with self.assertRaises(TypeError):
dataset.access_grants = object()

def test_access_roles_setter_invalid_field(self):
from gcloud.bigquery.dataset import AccessGrant
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
phred = AccessGrant('OWNER', 'userByEmail', 'phred@example.com')
with self.assertRaises(ValueError):
dataset.access_grants = [phred, object()]

def test_access_roles_setter(self):
from gcloud.bigquery.dataset import AccessGrant
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
phred = AccessGrant('OWNER', 'userByEmail', 'phred@example.com')
bharney = AccessGrant('OWNER', 'userByEmail', 'bharney@example.com')
dataset.access_grants = [phred, bharney]
self.assertEqual(dataset.access_grants, [phred, bharney])

def test_default_table_expiration_ms_setter_bad_value(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
Expand Down Expand Up @@ -175,6 +248,41 @@ def test_from_api_repr_w_properties(self):
self.assertTrue(dataset._client is client)
self._verifyResourceProperties(dataset, RESOURCE)

def test__parse_access_grants_w_unknown_entity_type(self):
USER_EMAIL = 'phred@example.com'
GROUP_EMAIL = 'group-name@lists.example.com'
RESOURCE = {
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'WRITER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER', 'specialGroup': 'projectReaders'},
{'role': 'READER', 'unknown': 'UNKNOWN'}]
}
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client=client)
grants = dataset._parse_access_grants(RESOURCE['access'])
self._verifyAccessGrants(grants, RESOURCE)

def test__parse_access_grants_w_multiple_entity_types(self):
# Hypothetical case: we don't know that the back-end will ever
# return such structures, but they are logical. See:
# https://github.com/GoogleCloudPlatform/gcloud-python/pull/1046#discussion_r36687769
USER_EMAIL = 'phred@example.com'
OTHER_EMAIL = 'bharney@example.com'
GROUP_EMAIL = 'group-name@lists.example.com'
RESOURCE = {
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'WRITER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER',
'specialGroup': 'projectReaders',
'userByEmail': OTHER_EMAIL}]
}
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client=client)
grants = dataset._parse_access_grants(RESOURCE['access'])
self._verifyAccessGrants(grants, RESOURCE)

def test_create_w_bound_client(self):
PATH = 'projects/%s/datasets' % self.PROJECT
RESOURCE = self._makeResource()
Expand All @@ -196,7 +304,10 @@ def test_create_w_bound_client(self):
self._verifyResourceProperties(dataset, RESOURCE)

def test_create_w_alternate_client(self):
from gcloud.bigquery.dataset import AccessGrant
PATH = 'projects/%s/datasets' % self.PROJECT
USER_EMAIL = 'phred@example.com'
GROUP_EMAIL = 'group-name@lists.example.com'
DESCRIPTION = 'DESCRIPTION'
TITLE = 'TITLE'
RESOURCE = self._makeResource()
Expand All @@ -209,6 +320,11 @@ def test_create_w_alternate_client(self):
dataset = self._makeOne(self.DS_NAME, client=CLIENT1)
dataset.friendly_name = TITLE
dataset.description = DESCRIPTION
dataset.access_grants = [
AccessGrant('OWNER', 'userByEmail', USER_EMAIL),
AccessGrant('OWNER', 'groupByEmail', GROUP_EMAIL),
AccessGrant('READER', 'specialGroup', 'projectReaders'),
AccessGrant('WRITER', 'specialGroup', 'projectWriters')]

dataset.create(client=CLIENT2)

Expand All @@ -222,6 +338,11 @@ def test_create_w_alternate_client(self):
{'projectId': self.PROJECT, 'datasetId': self.DS_NAME},
'description': DESCRIPTION,
'friendlyName': TITLE,
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'OWNER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER', 'specialGroup': 'projectReaders'},
{'role': 'WRITER', 'specialGroup': 'projectWriters'}],
}
self.assertEqual(req['data'], SENT)
self._verifyResourceProperties(dataset, RESOURCE)
Expand Down

0 comments on commit 2d017de

Please sign in to comment.