Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(290)

Side by Side Diff: third_party/gsutil/gslib/commands/signurl.py

Issue 1377933002: [catapult] - Copy Telemetry's gsutilz over to third_party. (Closed) Base URL: https://github.com/catapult-project/catapult.git@master
Patch Set: Rename to gsutil. Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/gsutil/gslib/commands/setmeta.py ('k') | third_party/gsutil/gslib/commands/stat.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # -*- coding: utf-8 -*-
2 # Copyright 2014 Google Inc. All Rights Reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Implementation of Url Signing workflow.
16
17 see: https://developers.google.com/storage/docs/accesscontrol#Signed-URLs)
18 """
19
20 from __future__ import absolute_import
21
22 import base64
23 import calendar
24 from datetime import datetime
25 from datetime import timedelta
26 import getpass
27 import re
28 import time
29 import urllib
30
31 from apitools.base.py.exceptions import HttpError
32 from apitools.base.py.http_wrapper import MakeRequest
33 from apitools.base.py.http_wrapper import Request
34
35 from gslib.command import Command
36 from gslib.command_argument import CommandArgument
37 from gslib.cs_api_map import ApiSelector
38 from gslib.exception import CommandException
39 from gslib.storage_url import ContainsWildcard
40 from gslib.storage_url import StorageUrlFromString
41 from gslib.util import GetNewHttp
42 from gslib.util import NO_MAX
43 from gslib.util import UTF8
44
45 try:
46 # Check for openssl.
47 # pylint: disable=C6204
48 from OpenSSL.crypto import load_pkcs12
49 from OpenSSL.crypto import sign
50 HAVE_OPENSSL = True
51 except ImportError:
52 load_pkcs12 = None
53 sign = None
54 HAVE_OPENSSL = False
55
56
57 _SYNOPSIS = """
58 gsutil signurl [-c] [-d] [-m] [-p] pkcs12-file url...
59 """
60
61 _DETAILED_HELP_TEXT = ("""
62 <B>SYNOPSIS</B>
63 """ + _SYNOPSIS + """
64
65
66 <B>DESCRIPTION</B>
67 The signurl command will generate signed urls that can be used to access
68 the specified objects without authentication for a specific period of time.
69
70 Please see the `Signed URLs documentation
71 <https://developers.google.com/storage/docs/accesscontrol#Signed-URLs>`_ for
72 background about signed URLs.
73
74 Multiple gs:// urls may be provided and may contain wildcards. A signed url
75 will be produced for each provided url, authorized
76 for the specified HTTP method and valid for the given duration.
77
78 Note: Unlike the gsutil ls command, the signurl command does not support
79 operations on sub-directories. For example, if you run the command:
80
81 gsutil signurl <private-key-file> gs://some-bucket/some-object/
82
83 The signurl command uses the private key for a service account (the
84 '<private-key-file>' argument) to generate the cryptographic
85 signature for the generated URL. The private key file must be in PKCS12
86 format. The signurl command will prompt for the passphrase used to protect
87 the private key file (default 'notasecret'). For more information
88 regarding generating a private key for use with the signurl command please
89 see the `Authentication documentation.
90 <https://developers.google.com/storage/docs/authentication#generating-a-privat e-key>`_
91
92 gsutil will look up information about the object "some-object/" (with a
93 trailing slash) inside bucket "some-bucket", as opposed to operating on
94 objects nested under gs://some-bucket/some-object. Unless you actually
95 have an object with that name, the operation will fail.
96
97 <B>OPTIONS</B>
98 -m Specifies the HTTP method to be authorized for use
99 with the signed url, default is GET.
100
101 -d Specifies the duration that the signed url should be valid
102 for, default duration is 1 hour.
103
104 Times may be specified with no suffix (default hours), or
105 with s = seconds, m = minutes, h = hours, d = days.
106
107 This option may be specified multiple times, in which case
108 the duration the link remains valid is the sum of all the
109 duration options.
110
111 -c Specifies the content type for which the signed url is
112 valid for.
113
114 -p Specify the keystore password instead of prompting.
115
116 <B>USAGE</B>
117
118 Create a signed url for downloading an object valid for 10 minutes:
119
120 gsutil signurl -d 10m <private-key-file> gs://<bucket>/<object>
121
122 Create a signed url for uploading a plain text file via HTTP PUT:
123
124 gsutil signurl -m PUT -d 1h -c text/plain <private-key-file> \\
125 gs://<bucket>/<obj>
126
127 To construct a signed URL that allows anyone in possession of
128 the URL to PUT to the specified bucket for one day, creating
129 any object of Content-Type image/jpg, run:
130
131 gsutil signurl -m PUT -d 1d -c image/jpg <private-key-file> \\
132 gs://<bucket>/<obj>
133
134
135 """)
136
137
138 def _DurationToTimeDelta(duration):
139 r"""Parses the given duration and returns an equivalent timedelta."""
140
141 match = re.match(r'^(\d+)([dDhHmMsS])?$', duration)
142 if not match:
143 raise CommandException('Unable to parse duration string')
144
145 duration, modifier = match.groups('h')
146 duration = int(duration)
147 modifier = modifier.lower()
148
149 if modifier == 'd':
150 ret = timedelta(days=duration)
151 elif modifier == 'h':
152 ret = timedelta(hours=duration)
153 elif modifier == 'm':
154 ret = timedelta(minutes=duration)
155 elif modifier == 's':
156 ret = timedelta(seconds=duration)
157
158 return ret
159
160
161 def _GenSignedUrl(key, client_id, method, md5,
162 content_type, expiration, gcs_path):
163 """Construct a string to sign with the provided key and returns \
164 the complete url."""
165
166 tosign = ('{0}\n{1}\n{2}\n{3}\n/{4}'
167 .format(method, md5, content_type,
168 expiration, gcs_path))
169 signature = base64.b64encode(sign(key, tosign, 'RSA-SHA256'))
170
171 final_url = ('https://storage.googleapis.com/{0}?'
172 'GoogleAccessId={1}&Expires={2}&Signature={3}'
173 .format(gcs_path, client_id, expiration,
174 urllib.quote_plus(str(signature))))
175
176 return final_url
177
178
179 def _ReadKeystore(ks_contents, passwd):
180 ks = load_pkcs12(ks_contents, passwd)
181 client_id = (ks.get_certificate()
182 .get_subject()
183 .CN.replace('.apps.googleusercontent.com',
184 '@developer.gserviceaccount.com'))
185
186 return ks, client_id
187
188
189 class UrlSignCommand(Command):
190 """Implementation of gsutil url_sign command."""
191
192 # Command specification. See base class for documentation.
193 command_spec = Command.CreateCommandSpec(
194 'signurl',
195 command_name_aliases=['signedurl', 'queryauth'],
196 usage_synopsis=_SYNOPSIS,
197 min_args=2,
198 max_args=NO_MAX,
199 supported_sub_args='m:d:c:p:',
200 file_url_ok=False,
201 provider_url_ok=False,
202 urls_start_arg=1,
203 gs_api_support=[ApiSelector.XML, ApiSelector.JSON],
204 gs_default_api=ApiSelector.JSON,
205 argparse_arguments=[
206 CommandArgument.MakeNFileURLsArgument(1),
207 CommandArgument.MakeZeroOrMoreCloudURLsArgument()
208 ]
209 )
210 # Help specification. See help_provider.py for documentation.
211 help_spec = Command.HelpSpec(
212 help_name='signurl',
213 help_name_aliases=['signedurl', 'queryauth'],
214 help_type='command_help',
215 help_one_line_summary='Create a signed url',
216 help_text=_DETAILED_HELP_TEXT,
217 subcommand_help_text={},
218 )
219
220 def _ParseAndCheckSubOpts(self):
221 # Default argument values
222 delta = None
223 method = 'GET'
224 content_type = ''
225 passwd = None
226
227 for o, v in self.sub_opts:
228 if o == '-d':
229 if delta is not None:
230 delta += _DurationToTimeDelta(v)
231 else:
232 delta = _DurationToTimeDelta(v)
233 elif o == '-m':
234 method = v
235 elif o == '-c':
236 content_type = v
237 elif o == '-p':
238 passwd = v
239 else:
240 self.RaiseInvalidArgumentException()
241
242 if delta is None:
243 delta = timedelta(hours=1)
244
245 expiration = calendar.timegm((datetime.utcnow() + delta).utctimetuple())
246 if method not in ['GET', 'PUT', 'DELETE', 'HEAD']:
247 raise CommandException('HTTP method must be one of [GET|HEAD|PUT|DELETE]')
248
249 return method, expiration, content_type, passwd
250
251 def _ProbeObjectAccessWithClient(self, key, client_id, gcs_path):
252 """Performs a head request against a signed url to check for read access."""
253
254 signed_url = _GenSignedUrl(key, client_id, 'HEAD', '', '',
255 int(time.time()) + 10, gcs_path)
256
257 try:
258 h = GetNewHttp()
259 req = Request(signed_url, 'HEAD')
260 response = MakeRequest(h, req)
261
262 if response.status_code not in [200, 403, 404]:
263 raise HttpError(response)
264
265 return response.status_code
266 except HttpError as e:
267 raise CommandException('Unexpected response code while querying'
268 'object readability ({0})'.format(e.message))
269
270 def _EnumerateStorageUrls(self, in_urls):
271 ret = []
272
273 for url_str in in_urls:
274 if ContainsWildcard(url_str):
275 ret.extend([blr.storage_url for blr in self.WildcardIterator(url_str)])
276 else:
277 ret.append(StorageUrlFromString(url_str))
278
279 return ret
280
281 def RunCommand(self):
282 """Command entry point for signurl command."""
283 if not HAVE_OPENSSL:
284 raise CommandException(
285 'The signurl command requires the pyopenssl library (try pip '
286 'install pyopenssl or easy_install pyopenssl)')
287
288 method, expiration, content_type, passwd = self._ParseAndCheckSubOpts()
289 storage_urls = self._EnumerateStorageUrls(self.args[1:])
290
291 if not passwd:
292 passwd = getpass.getpass('Keystore password:')
293
294 ks, client_id = _ReadKeystore(open(self.args[0], 'rb').read(), passwd)
295
296 print 'URL\tHTTP Method\tExpiration\tSigned URL'
297 for url in storage_urls:
298 if url.scheme != 'gs':
299 raise CommandException('Can only create signed urls from gs:// urls')
300 if url.IsBucket():
301 gcs_path = url.bucket_name
302 else:
303 # Need to url encode the object name as Google Cloud Storage does when
304 # computing the string to sign when checking the signature.
305 gcs_path = '{0}/{1}'.format(url.bucket_name,
306 urllib.quote(url.object_name.encode(UTF8)))
307
308 final_url = _GenSignedUrl(ks.get_privatekey(), client_id,
309 method, '', content_type, expiration,
310 gcs_path)
311
312 expiration_dt = datetime.fromtimestamp(expiration)
313
314 print '{0}\t{1}\t{2}\t{3}'.format(url.url_string.encode(UTF8), method,
315 (expiration_dt
316 .strftime('%Y-%m-%d %H:%M:%S')),
317 final_url.encode(UTF8))
318
319 response_code = self._ProbeObjectAccessWithClient(ks.get_privatekey(),
320 client_id, gcs_path)
321
322 if response_code == 404 and method != 'PUT':
323 if url.IsBucket():
324 msg = ('Bucket {0} does not exist. Please create a bucket with '
325 'that name before a creating signed URL to access it.'
326 .format(url))
327 else:
328 msg = ('Object {0} does not exist. Please create/upload an object '
329 'with that name before a creating signed URL to access it.'
330 .format(url))
331
332 raise CommandException(msg)
333 elif response_code == 403:
334 self.logger.warn(
335 '%s does not have permissions on %s, using this link will likely '
336 'result in a 403 error until at least READ permissions are granted',
337 client_id, url)
338
339 return 0
OLDNEW
« no previous file with comments | « third_party/gsutil/gslib/commands/setmeta.py ('k') | third_party/gsutil/gslib/commands/stat.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698