Skip to content

Commit ff6a77c

Browse files
author
Jon Wayne Parrott
authored
Add generate_upload_policy (googleapis#2998)
1 parent 6ce3507 commit ff6a77c

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

docs/storage_snippets.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,39 @@ def list_buckets(client, to_delete):
220220
to_delete.append(bucket)
221221

222222

223+
@snippet
224+
def policy_document(client, to_delete):
225+
# pylint: disable=unused-argument
226+
# [START policy_document]
227+
bucket = client.bucket('my-bucket')
228+
conditions = [
229+
['starts-with', '$key', ''],
230+
{'acl': 'public-read'}]
231+
232+
policy = bucket.generate_upload_policy(conditions)
233+
234+
# Generate an upload form using the form fields.
235+
policy_fields = ''.join(
236+
'<input type="hidden" name="{key}" value="{value}">'.format(
237+
key=key, value=value)
238+
for key, value in policy.items()
239+
)
240+
241+
upload_form = (
242+
'<form action="http://{bucket_name}.storage.googleapis.com"'
243+
' method="post"enctype="multipart/form-data">'
244+
'<input type="text" name="key" value="">'
245+
'<input type="hidden" name="bucket" value="{bucket_name}">'
246+
'<input type="hidden" name="acl" value="public-read">'
247+
'<input name="file" type="file">'
248+
'<input type="submit" value="Upload">'
249+
'{policy_fields}'
250+
'<form>').format(bucket_name=bucket.name, policy_fields=policy_fields)
251+
252+
print(upload_form)
253+
# [END policy_document]
254+
255+
223256
def _line_no(func):
224257
code = getattr(func, '__code__', None) or getattr(func, 'func_code')
225258
return code.co_firstlineno

storage/google/cloud/storage/bucket.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414

1515
"""Create / interact with Google Cloud Storage buckets."""
1616

17+
import base64
1718
import copy
19+
import datetime
20+
import json
1821

22+
import google.auth.credentials
1923
import six
2024

25+
from google.cloud._helpers import _datetime_to_rfc3339
26+
from google.cloud._helpers import _NOW
2127
from google.cloud._helpers import _rfc3339_to_datetime
2228
from google.cloud.exceptions import NotFound
2329
from google.cloud.iterator import HTTPIterator
@@ -829,3 +835,76 @@ def make_public(self, recursive=False, future=False, client=None):
829835
for blob in blobs:
830836
blob.acl.all().grant_read()
831837
blob.acl.save(client=client)
838+
839+
def generate_upload_policy(
840+
self, conditions, expiration=None, client=None):
841+
"""Create a signed upload policy for uploading objects.
842+
843+
This method generates and signs a policy document. You can use
844+
`policy documents`_ to allow visitors to a website to upload files to
845+
Google Cloud Storage without giving them direct write access.
846+
847+
For example:
848+
849+
.. literalinclude:: storage_snippets.py
850+
:start-after: [START policy_document]
851+
:end-before: [END policy_document]
852+
853+
.. _policy documents:
854+
https://cloud.google.com/storage/docs/xml-api\
855+
/post-object#policydocument
856+
857+
:type expiration: datetime
858+
:param expiration: Optional expiration in UTC. If not specified, the
859+
policy will expire in 1 hour.
860+
861+
:type conditions: list
862+
:param conditions: A list of conditions as described in the
863+
`policy documents`_ documentation.
864+
865+
:type client: :class:`~google.cloud.storage.client.Client`
866+
:param client: Optional. The client to use. If not passed, falls back
867+
to the ``client`` stored on the current bucket.
868+
869+
:rtype: dict
870+
:returns: A dictionary of (form field name, form field value) of form
871+
fields that should be added to your HTML upload form in order
872+
to attach the signature.
873+
"""
874+
client = self._require_client(client)
875+
credentials = client._base_connection.credentials
876+
877+
if not isinstance(credentials, google.auth.credentials.Signing):
878+
auth_uri = ('http://google-cloud-python.readthedocs.io/en/latest/'
879+
'google-cloud-auth.html#setting-up-a-service-account')
880+
raise AttributeError(
881+
'you need a private key to sign credentials.'
882+
'the credentials you are currently using %s '
883+
'just contains a token. see %s for more '
884+
'details.' % (type(credentials), auth_uri))
885+
886+
if expiration is None:
887+
expiration = _NOW() + datetime.timedelta(hours=1)
888+
889+
conditions = conditions + [
890+
{'bucket': self.name},
891+
]
892+
893+
policy_document = {
894+
'expiration': _datetime_to_rfc3339(expiration),
895+
'conditions': conditions,
896+
}
897+
898+
encoded_policy_document = base64.b64encode(
899+
json.dumps(policy_document).encode('utf-8'))
900+
signature = base64.b64encode(
901+
credentials.sign_bytes(encoded_policy_document))
902+
903+
fields = {
904+
'bucket': self.name,
905+
'GoogleAccessId': credentials.signer_email,
906+
'policy': encoded_policy_document.decode('utf-8'),
907+
'signature': signature.decode('utf-8'),
908+
}
909+
910+
return fields

storage/unit_tests/test_bucket.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import unittest
1617

18+
import mock
19+
20+
21+
def _create_signing_credentials():
22+
import google.auth.credentials
23+
24+
class _SigningCredentials(
25+
google.auth.credentials.Credentials,
26+
google.auth.credentials.Signing):
27+
pass
28+
29+
credentials = mock.Mock(spec=_SigningCredentials)
30+
31+
return credentials
32+
1733

1834
class Test_Bucket(unittest.TestCase):
1935

@@ -782,7 +798,6 @@ def test_storage_class_setter_DURABLE_REDUCED_AVAILABILITY(self):
782798
self.assertTrue('storageClass' in bucket._changes)
783799

784800
def test_time_created(self):
785-
import datetime
786801
from google.cloud._helpers import _RFC3339_MICROS
787802
from google.cloud._helpers import UTC
788803

@@ -903,7 +918,6 @@ def test_make_public_w_future_reload_default(self):
903918
self._make_public_w_future_helper(default_object_acl_loaded=False)
904919

905920
def test_make_public_recursive(self):
906-
import mock
907921
from google.cloud.storage.acl import _ACLEntity
908922

909923
_saved = []
@@ -1068,6 +1082,82 @@ def dummy_response():
10681082
self.assertEqual(page2.num_items, 0)
10691083
self.assertEqual(iterator.prefixes, set(['foo', 'bar']))
10701084

1085+
def _test_generate_upload_policy_helper(self, **kwargs):
1086+
import base64
1087+
import json
1088+
1089+
credentials = _create_signing_credentials()
1090+
credentials.signer_email = mock.sentinel.signer_email
1091+
credentials.sign_bytes.return_value = b'DEADBEEF'
1092+
connection = _Connection()
1093+
connection.credentials = credentials
1094+
client = _Client(connection)
1095+
name = 'name'
1096+
bucket = self._make_one(client=client, name=name)
1097+
1098+
conditions = [
1099+
['starts-with', '$key', '']]
1100+
1101+
policy_fields = bucket.generate_upload_policy(conditions, **kwargs)
1102+
1103+
self.assertEqual(policy_fields['bucket'], bucket.name)
1104+
self.assertEqual(
1105+
policy_fields['GoogleAccessId'], mock.sentinel.signer_email)
1106+
self.assertEqual(
1107+
policy_fields['signature'],
1108+
base64.b64encode(b'DEADBEEF').decode('utf-8'))
1109+
1110+
policy = json.loads(
1111+
base64.b64decode(policy_fields['policy']).decode('utf-8'))
1112+
1113+
policy_conditions = policy['conditions']
1114+
expected_conditions = [{'bucket': bucket.name}] + conditions
1115+
for expected_condition in expected_conditions:
1116+
for condition in policy_conditions:
1117+
if condition == expected_condition:
1118+
break
1119+
else: # pragma: NO COVER
1120+
self.fail('Condition {} not found in {}'.format(
1121+
expected_condition, policy_conditions))
1122+
1123+
return policy_fields, policy
1124+
1125+
@mock.patch(
1126+
'google.cloud.storage.bucket._NOW',
1127+
return_value=datetime.datetime(1990, 1, 1))
1128+
def test_generate_upload_policy(self, now):
1129+
from google.cloud._helpers import _datetime_to_rfc3339
1130+
1131+
_, policy = self._test_generate_upload_policy_helper()
1132+
1133+
self.assertEqual(
1134+
policy['expiration'],
1135+
_datetime_to_rfc3339(
1136+
now() + datetime.timedelta(hours=1)))
1137+
1138+
def test_generate_upload_policy_args(self):
1139+
from google.cloud._helpers import _datetime_to_rfc3339
1140+
1141+
expiration = datetime.datetime(1990, 5, 29)
1142+
1143+
_, policy = self._test_generate_upload_policy_helper(
1144+
expiration=expiration)
1145+
1146+
self.assertEqual(
1147+
policy['expiration'],
1148+
_datetime_to_rfc3339(expiration))
1149+
1150+
def test_generate_upload_policy_bad_credentials(self):
1151+
credentials = object()
1152+
connection = _Connection()
1153+
connection.credentials = credentials
1154+
client = _Client(connection)
1155+
name = 'name'
1156+
bucket = self._make_one(client=client, name=name)
1157+
1158+
with self.assertRaises(AttributeError):
1159+
bucket.generate_upload_policy([])
1160+
10711161

10721162
class _Connection(object):
10731163
_delete_bucket = False
@@ -1076,6 +1166,7 @@ def __init__(self, *responses):
10761166
self._responses = responses
10771167
self._requested = []
10781168
self._deleted_buckets = []
1169+
self.credentials = None
10791170

10801171
@staticmethod
10811172
def _is_bucket_path(path):
@@ -1108,4 +1199,5 @@ class _Client(object):
11081199

11091200
def __init__(self, connection, project=None):
11101201
self._connection = connection
1202+
self._base_connection = connection
11111203
self.project = project

0 commit comments

Comments
 (0)