10000 Merge pull request #318 from tseaver/151-163-use_acl_endpoints · googleapis/google-cloud-python@632e126 · GitHub
[go: up one dir, main page]

Skip to content

Commit 632e126

Browse files
committed
Merge pull request #318 from tseaver/151-163-use_acl_endpoints
Fix #151/#163: use ACL-specific endpoints where feasible for buckets and keys
2 parents b1e8bcf + dcf7d27 commit 632e126

File tree

6 files changed

+351
-259
lines changed

6 files changed

+351
-259
lines changed

gcloud/storage/acl.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,20 @@ def revoke_owner(self):
167167
class ACL(object):
168168
"""Container class representing a list of access controls."""
169169

170+
loaded = False
171+
170172
def __init__(self):
171173
self.entities = {}
172174

175+
def clear(self):
176+
"""Remove all entities from the ACL."""
177+
self.entities.clear()
178+
179+
def reset(self):
180+
"""Remove all entities from the ACL, and clear the ``loaded`` flag."""
181+
self.entities.clear()
182+
self.loaded = False
183+
173184
def __iter__(self):
174185
for entity in self.entities.itervalues():
175186
for role in entity.get_roles():
@@ -242,6 +253,7 @@ def add_entity(self, entity):
242253
:param entity: The entity to add to this ACL.
243254
"""
244255
self.entities[str(entity)] = entity
256+
self.loaded = True
245257

246258
def entity(self, entity_type, identifier=None):
247259
"""Factory method for creating an Entity.

gcloud/storage/bucket.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,27 @@ class Bucket(object):
1919
:type name: string
2020
:param name: The name of the bucket.
2121
"""
22+
# ACL rules are lazily retrieved.
23+
_acl = _default_object_acl = None
2224

2325
def __init__(self, connection=None, name=None, metadata=None):
2426
self.connection = connection
2527
self.name = name
2628
self.metadata = metadata
2729

28-
# ACL rules are lazily retrieved.
29-
self.acl = None
30-
self.default_object_acl = None
30+
@property
31+
def acl(self):
32+
"""Create our ACL on demand."""
33+
if self._acl is None:
34+
self._acl = BucketACL(self)
35+
return self._acl
36+
37+
@property
38+
def default_object_acl(self):
39+
"""Create our defaultObjectACL on demand."""
40+
if self._default_object_acl is None:
41+
self._default_object_acl = DefaultObjectACL(self)
42+
return self._default_object_acl
3143

3244
@classmethod
3345
def from_dict(cls, bucket_dict, connection=None):
@@ -313,17 +325,15 @@ def has_metadata(self, field=None):
313325
else:
314326
return True
315327

316-
def reload_metadata(self, full=False):
328+
def reload_metadata(self):
317329
"""Reload metadata from Cloud Storage.
318330
319-
:type full: bool
320-
:param full: If True, loads all data (include ACL data).
321-
322331
:rtype: :class:`Bucket`
323332
:returns: The bucket you just reloaded data for.
324333
"""
325-
projection = 'full' if full else 'noAcl'
326-
query_params = {'projection': projection}
334+
# Pass only '?projection=noAcl' here because 'acl'/'defaultObjectAcl'
335+
# are handled via 'get_acl()'/'get_default_object_acl()'
336+
query_params = {'projection': 'noAcl'}
327337
self.metadata = self.connection.api_request(
328338
method='GET', path=self.path, query_params=query_params)
329339
return self
@@ -344,9 +354,14 @@ def get_metadata(self, field=None, default=None):
344354
:rtype: dict or anything
345355
:returns: All metadata or the value of the specific field.
346356
"""
357+
if field == 'acl':
358+
raise KeyError("Use 'get_acl()'")
359+
360+
if field == 'defaultObjectAcl':
361+
raise KeyError("Use 'get_default_object_acl()'")
362+
347363
if not self.has_metadata(field=field):
348-
full = (field and field in ('acl', 'defaultObjectAcl'))
349-
self.reload_metadata(full=full)
364+
self.reload_metadata()
350365

351366
if field:
352367
return self.metadata.get(field, default)
@@ -431,11 +446,15 @@ def reload_acl(self):
431446
:rtype: :class:`Bucket`
432447
:returns: The current bucket.
433448
"""
434-
self.acl = BucketACL(bucket=self)
449+
self.acl.clear()
450+
451+
url_path = '%s/acl' % self.path
452+
found = self.connection.api_request(method='GET', path=url_path)
453+
for entry in found['items']:
454+
self.acl.add_entity(self.acl.entity_from_dict(entry))
435455

436-
for entry in self.get_metadata('acl', []):
437-
entity = self.acl.entity_from_dict(entry)
438-
self.acl.add_entity(entity)
456+
# Even if we fetch no entries, the ACL is still loaded.
457+
self.acl.loaded = True
439458

440459
return self
441460

@@ -445,7 +464,7 @@ def get_acl(self):
445464
:rtype: :class:`gcloud.storage.acl.BucketACL`
446465
:returns: An ACL object for the current bucket.
447466
"""
448-
if not self.acl:
467+
if not self.acl.loaded:
449468
self.reload_acl()
450469
return self.acl
451470

@@ -487,12 +506,19 @@ def save_acl(self, acl=None):
487506
# both evaluate to False, but mean very different things.
488507
if acl is None:
489508
acl = self.acl
509+
dirty = acl.loaded
510+
else:
511+
dirty = True
490512

491-
if acl is None:
492-
return self
513+
if dirty:
514+
result = self.connection.api_request(
515+
method='PATCH', path=self.path, data={'acl': list(acl)},
516+
query_params={'projection': 'full'})
517+
self.acl.clear()
518+
for entry in result['acl']:
519+
self.acl.entity(self.acl.entity_from_dict(entry))
520+
self.acl.loaded = True
493521

494-
self.patch_metadata({'acl': list(acl)})
495-
self.reload_acl()
496522
return self
497523

498524
def clear_acl(self):
@@ -522,19 +548,26 @@ def clear_acl(self):
522548
523549
At this point all the custom rules you created have been removed.
524550
"""
525-
return self.save_acl(acl=[])
551+
# NOTE: back-end makes some ACL entries sticky (they remain even
552+
# after the PATCH succeeds.
553+
return self.save_acl([])
526554

527555
def reload_default_object_acl(self):
528556
"""Reload the Default Object ACL rules for this bucket.
529557
530558
:rtype: :class:`Bucket`
531559
:returns: The current bucket.
532560
"""
533-
self.default_object_acl = DefaultObjectACL(bucket=self)
561+
doa = self.default_object_acl
562+
doa.clear()
534563

535-
for entry in self.get_metadata('defaultObjectAcl', []):
536-
entity = self.default_object_acl.entity_from_dict(entry)
537-
self.default_object_acl.add_entity(entity)
564+
url_path = '%s/defaultObjectAcl' % self.path
565+
found = self.connection.api_request(method='GET', path=url_path)
566+
for entry in found['items']:
567+
doa.add_entity(doa.entity_from_dict(entry))
568+
569+
# Even if we fetch no entries, the ACL is still loaded.
570+
doa.loaded = True
538571

539572
return self
540573

@@ -547,7 +580,7 @@ def get_default_object_acl(self):
547580
:rtype: :class:`gcloud.storage.acl.DefaultObjectACL`
548581
:returns: A DefaultObjectACL object for this bucket.
549582
"""
550-
if not self.default_object_acl:
583+
if not self.default_object_acl.loaded:
551584
self.reload_default_object_acl()
552585
return self.default_object_acl
553586

@@ -562,18 +595,26 @@ def save_default_object_acl(self, acl=None):
562595
"""
563596
if acl is None:
564597
acl = self.default_object_acl
598+
dirty = acl.loaded
599+
else:
600+
dirty = True
601+
602+
if dirty:
603+
result = self.connection.api_request(
604+
method='PATCH', path=self.path,
605+
data={'defaultObjectAcl': list(acl)},
606+
query_params={'projection': 'full'})
607+
doa = self.default_object_acl
608+
doa.clear()
609+
for entry in result['defaultObjectAcl']:
610+
doa.entity(doa.entity_from_dict(entry))
611+
doa.loaded = True
565612

566-
if acl is None:
567-
return self
568-
569-
self.patch_metadata({'defaultObjectAcl': list(acl)})
570-
self.reload_default_object_acl()
571613
return self
572614

573615
def clear_default_object_acl(self):
574616
"""Remove the Default Object ACL from this bucket."""
575-
576-
return self.save_default_object_acl(acl=[])
617+
return self.save_default_object_acl([])
577618

578619
def make_public(self, recursive=False, future=False):
579620
"""Make a bucket public.

gcloud/storage/key.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class Key(object):
1717
1818
This must be a multiple of 256 KB per the API specification.
1919
"""
20+
# ACL rules are lazily retrieved.
21+
_acl = None
2022

2123
def __init__(self, bucket=None, name=None, metadata=None):
2224
"""Key constructor.
@@ -35,8 +37,12 @@ def __init__(self, bucket=None, name=None, metadata=None):
3537
self.name = name
3638
self.metadata = metadata or {}
3739

38-
# Lazily get the ACL information.
39-
self.acl = None
40+
@property
41+
def acl(self):
42+
"""Create our ACL on demand."""
43+
if self._acl is None:
44+
self._acl = ObjectACL(self)
45+
return self._acl
4046

4147
@classmethod
4248
def from_dict(cls, key_dict, bucket=None):
@@ -316,17 +322,15 @@ def has_metadata(self, field=None):
316322
else:
317323
return True
318324

319-
def reload_metadata(self, full=False):
325+
def reload_metadata(self):
320326
"""Reload metadata from Cloud Storage.
321327
322-
:type full: bool
323-
:param full: If True, loads all data (include ACL data).
324-
325328
:rtype: :class:`Key`
326329
:returns: The key you just reloaded data for.
327330
"""
328-
projection = 'full' if full else 'noAcl'
329-
query_params = {'projection': projection}
331+
# Pass only '?projection=noAcl' here because 'acl' is handled via
332+
# 'get_acl().
333+
query_params = {'projection': 'noAcl'}
330334
self.metadata = self.connection.api_request(
331335
method='GET', path=self.path, query_params=query_params)
332336
return self
@@ -347,9 +351,12 @@ def get_metadata(self, field=None, default=None):
347351
:rtype: dict or anything
348352
:returns: All metadata or the value of the specific field.
349353
"""
354+
# We ignore 'acl' because it is meant to be handled via 'get_acl()'.
355+
if field == 'acl':
356+
raise KeyError("Use 'get_acl()'")
357+
350358
if not self.has_metadata(field=field):
351-
full = (field and field == 'acl')
352-
self.reload_metadata(full=full)
359+
self.reload_metadata()
353360

354361
if field:
355362
return self.metadata.get(field, default)
@@ -382,11 +389,15 @@ def reload_acl(self):
382389
:rtype: :class:`Key`
383390
:returns: The current key.
384391
"""
385-
self.acl = ObjectACL(key=self)
392+
self.acl.clear()
386393

387-
for entry in self.get_metadata('acl', []):
388-
entity = self.acl.entity_from_dict(entry)
389-
self.acl.add_entity(entity)
394+
url_path = '%s/acl' % self.path
395+
found = self.connection.api_request(method='GET', path=url_path)
396+
for entry in found['items']:
397+
self.acl.add_entity(self.acl.entity_from_dict(entry))
398+
399+
# Even if we fetch no entries, the ACL is still loaded.
400+
self.acl.loaded = True
390401

391402
return self
392403

@@ -396,7 +407,7 @@ def get_acl(self):
396407
:rtype: :class:`gcloud.storage.acl.ObjectACL`
397408
:returns: An ACL object for the current key.
398409
"""
399-
if not self.acl:
410+
if not self.acl.loaded:
400411
self.reload_acl()
401412
return self.acl
402413

@@ -407,16 +418,21 @@ def save_acl(self, acl=None):
407418
:param acl: The ACL object to save. If left blank, this will
408419
save the ACL set locally on the key.
409420
"""
410-
# We do things in this weird way because [] and None
411-
# both evaluate to False, but mean very different things.
412421
if acl is None:
413422
acl = self.acl
423+
dirty = acl.loaded
424+
else:
425+
dirty = True
414426

415-
if acl is None:
416-
return self
427+
if dirty:
428+
result = self.connection.api_request(
429+
method='PATCH', path=self.path, data={'acl': list(acl)},
430+
query_params={'projection': 'full'})
431+
self.acl.clear()
432+
for entry in result['acl']:
433+
self.acl.entity(self.acl.entity_from_dict(entry))
434+
self.acl.loaded = True
417435

418-
self.patch_metadata({'acl': list(acl)})
419-
self.reload_acl()
420436
return self
421437

422438
def clear_acl(self):
@@ -427,7 +443,7 @@ def clear_acl(self):
427443
have access to a key that you created even after you clear ACL
428444
rules with this method.
429445
"""
430-
return self.save_acl(acl=[])
446+
return self.save_acl([])
431447

432448
def make_public(self):
433449
"""Make this key public giving all users read access.

0 commit comments

Comments
 (0)
0