Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / tools / telemetry / third_party / gsutilz / gslib / commands / signurl.py
blob31e8e510f6c7ead265a366179f6812bc664cc5ed
1 # -*- coding: utf-8 -*-
2 # Copyright 2014 Google Inc. All Rights Reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 see: https://developers.google.com/storage/docs/accesscontrol#Signed-URLs)
18 """
20 from __future__ import absolute_import
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
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
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
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
57 _SYNOPSIS = """
58 gsutil signurl [-c] [-d] [-m] [-p] pkcs12-file url...
59 """
61 _DETAILED_HELP_TEXT = ("""
62 <B>SYNOPSIS</B>
63 """ + _SYNOPSIS + """
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.
70 Please see the `Signed URLs documentation
71 <https://developers.google.com/storage/docs/accesscontrol#Signed-URLs>`_ for
72 background about signed URLs.
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.
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:
81 gsutil signurl <private-key-file> gs://some-bucket/some-object/
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-private-key>`_
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.
97 <B>OPTIONS</B>
98 -m Specifies the HTTP method to be authorized for use
99 with the signed url, default is GET.
101 -d Specifies the duration that the signed url should be valid
102 for, default duration is 1 hour.
104 Times may be specified with no suffix (default hours), or
105 with s = seconds, m = minutes, h = hours, d = days.
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.
111 -c Specifies the content type for which the signed url is
112 valid for.
114 -p Specify the keystore password instead of prompting.
116 <B>USAGE</B>
118 Create a signed url for downloading an object valid for 10 minutes:
120 gsutil signurl -d 10m <private-key-file> gs://<bucket>/<object>
122 Create a signed url for uploading a plain text file via HTTP PUT:
124 gsutil signurl -m PUT -d 1h -c text/plain <private-key-file> \\
125 gs://<bucket>/<obj>
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:
131 gsutil signurl -m PUT -d 1d -c image/jpg <private-key-file> \\
132 gs://<bucket>/<obj>
135 """)
138 def _DurationToTimeDelta(duration):
139 r"""Parses the given duration and returns an equivalent timedelta."""
141 match = re.match(r'^(\d+)([dDhHmMsS])?$', duration)
142 if not match:
143 raise CommandException('Unable to parse duration string')
145 duration, modifier = match.groups('h')
146 duration = int(duration)
147 modifier = modifier.lower()
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)
158 return ret
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."""
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'))
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))))
176 return final_url
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'))
186 return ks, client_id
189 class UrlSignCommand(Command):
190 """Implementation of gsutil url_sign command."""
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()
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={},
220 def _ParseAndCheckSubOpts(self):
221 # Default argument values
222 delta = None
223 method = 'GET'
224 content_type = ''
225 passwd = None
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()
242 if delta is None:
243 delta = timedelta(hours=1)
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]')
249 return method, expiration, content_type, passwd
251 def _ProbeObjectAccessWithClient(self, key, client_id, gcs_path):
252 """Performs a head request against a signed url to check for read access."""
254 signed_url = _GenSignedUrl(key, client_id, 'HEAD', '', '',
255 int(time.time()) + 10, gcs_path)
257 try:
258 h = GetNewHttp()
259 req = Request(signed_url, 'HEAD')
260 response = MakeRequest(h, req)
262 if response.status_code not in [200, 403, 404]:
263 raise HttpError(response)
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))
270 def _EnumerateStorageUrls(self, in_urls):
271 ret = []
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))
279 return ret
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)')
288 method, expiration, content_type, passwd = self._ParseAndCheckSubOpts()
289 storage_urls = self._EnumerateStorageUrls(self.args[1:])
291 if not passwd:
292 passwd = getpass.getpass('Keystore password:')
294 ks, client_id = _ReadKeystore(open(self.args[0], 'rb').read(), passwd)
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)))
308 final_url = _GenSignedUrl(ks.get_privatekey(), client_id,
309 method, '', content_type, expiration,
310 gcs_path)
312 expiration_dt = datetime.fromtimestamp(expiration)
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))
319 response_code = self._ProbeObjectAccessWithClient(ks.get_privatekey(),
320 client_id, gcs_path)
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))
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)
339 return 0