Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0a311f7

Browse filesBrowse files
author
Jon Wayne Parrott
authored
Add generate_upload_policy (#2998)
1 parent 635f439 commit 0a311f7
Copy full SHA for 0a311f7

File tree

Expand file treeCollapse file tree

3 files changed

+206
-2
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+206
-2
lines changed
Open diff view settings
Collapse file

‎docs/storage_snippets.py‎

Copy file name to clipboardExpand all lines: docs/storage_snippets.py
+33Lines 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
Collapse file

‎storage/google/cloud/storage/bucket.py‎

Copy file name to clipboardExpand all lines: storage/google/cloud/storage/bucket.py
+79Lines 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
Collapse file

‎storage/unit_tests/test_bucket.py‎

Copy file name to clipboardExpand all lines: storage/unit_tests/test_bucket.py
+94-2Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.