From 3dddc8b356a3de7925f5bd9dd2814a7c191a4740 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Fri, 4 Dec 2015 02:08:35 -0800 Subject: [PATCH 1/7] Implement cloudfront sign --- awscli/customizations/cloudfront.py | 69 ++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/awscli/customizations/cloudfront.py b/awscli/customizations/cloudfront.py index 148294b8845d..d7a3145bbd3e 100644 --- a/awscli/customizations/cloudfront.py +++ b/awscli/customizations/cloudfront.py @@ -13,13 +13,19 @@ import time import random +import rsa +from botocore.utils import parse_to_aware_datetime +from botocore.signers import CloudFrontSigner + from awscli.arguments import CustomArgument from awscli.customizations.utils import validate_mutually_exclusive_handler +from awscli.customizations.commands import BasicCommand def register(event_handler): - """Provides a simpler --paths for ``aws cloudfront create-invalidation``""" + event_handler.register('building-command-table.cloudfront', _add_sign) + # Provides a simpler --paths for ``aws cloudfront create-invalidation`` event_handler.register( 'building-argument-table.cloudfront.create-invalidation', _add_paths) event_handler.register( @@ -49,3 +55,64 @@ def add_to_params(self, parameters, value): "CallerReference": caller_reference, "Paths": {"Quantity": len(value), "Items": value}, } + + +def _add_sign(command_table, session, **kwargs): + command_table['sign'] = SignCommand(session) + + +class SignCommand(BasicCommand): + NAME = 'sign' + DESCRIPTION = 'Sign a given url.' + DATE_HELP = """Supported formats include: + YYYY-MM-DD (which means 0AM UTC of that day), + YYYY-MM-DDThh:mm:ss (with default timezone as UTC), + YYYY-MM-DDThh:mm:ss+hh:mm or YYYY-MM-DDThh:mm:ss-hh:mm (with offset), + or EpochTime. + Do NOT use YYYYMMDD, because it will be treated as EpochTime.""" + ARG_TABLE = [ + { + 'name': 'url', + 'no_paramfile': True, # To disable the default paramfile behavior + 'required': True, + 'help_text': 'The URL to be signed', + }, + { + 'name': 'key-pair-id', + 'required': True, + 'help_text': 'The ID of your CloudFront key pairs.', + }, + { + 'name': 'private-key', + 'required': True, + 'help_text': 'file://path/to/your/private-key.pem', + }, + {'name': 'date-less-than', 'required': True, 'help_text': DATE_HELP}, + {'name': 'date-greater-than', 'help_text': DATE_HELP}, + {'name': 'ip-address', 'help_text': 'Format: x.x.x.x/x or x.x.x.x'}, + ] + + def _run_main(self, args, parsed_globals): + signer = CloudFrontSigner( + args.key_pair_id, RsaSigner(args.private_key).sign) + date_less_than = parse_to_aware_datetime(args.date_less_than) + if args.date_greater_than is not None: + date_greater_than = parse_to_aware_datetime(args.date_greater_than) + else: + date_greater_than = None + if date_greater_than is not None or args.ip_address is not None: + policy = signer.build_policy( + args.url, date_less_than, date_greater_than=date_greater_than, + ip_address=args.ip_address) + print(signer.generate_presigned_url(args.url, policy=policy)) + else: + print(signer.generate_presigned_url(args.url, date_less_than)) + return 0 + + +class RsaSigner(object): + def __init__(self, private_key): + self.priv_key = rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')) + + def sign(self, message): + return rsa.sign(message, self.priv_key, 'SHA-1') From 1d6b3180c68f819335664bd841e58734a5784ea1 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Fri, 4 Dec 2015 11:09:33 -0800 Subject: [PATCH 2/7] Test cases --- tests/functional/cloudfront/test_sign.py | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/functional/cloudfront/test_sign.py diff --git a/tests/functional/cloudfront/test_sign.py b/tests/functional/cloudfront/test_sign.py new file mode 100644 index 000000000000..34acf081cb95 --- /dev/null +++ b/tests/functional/cloudfront/test_sign.py @@ -0,0 +1,67 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from awscli.testutils import FileCreator +from awscli.testutils import BaseAWSPreviewCommandParamsTest as \ + BaseAWSCommandParamsTest + + +class TestSign(BaseAWSCommandParamsTest): + # A private key only for testing purpose. + private_key = ''' + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAu6o2+Jc8UINw2P/w2l7A1xXu3emQEZQ9diA3bmog8r9Dg+65 + fZgAqmuNWPqBivv7j3DGnLUdt8uCIr7PYUbK7wDa6n7U3ryOWtO2ZTc3StiJVcqT + sokZ0qxGFtDRafjBuydXtcxh52vVTcHqH33nubyyZIzuhTwfmrIOnUXnLwbMrBBP + bg/8mlgQooyo1XbrN1eO4XMs+UgQ9Mqc7KRJRinUJ+KYuCnM8f/nN4RjYdjTcghk + xCPEHCeSt2luywWyYmfguWCBS2Mu1q0250wKyNazlgiiTJtAuuSeweb4NKPOJL9X + hR6Ce6UuU4WYlli8gvQh3FAV3N3C1Rxo20k28QIDAQABAoIBAQCUEkP5dWrzpCJg + NeHWizjg/L9SfT1dgXfVQqo6BqckoeElsjDNdifgT6hhcpbQEO52SWeMsiNWp85w + l9mNSYxJdIVGzPgtHt27sJyT1DNebOg/tu0+y4qCfcd3rR/u24YQo4RDP5ZoQN82 + 0TBn1LIIDWk8iS6SFdRh/OgnE8bLhNbK9IfZQFEEJrFkArrn/le/ro2mfJkC/imo + QvqKmM0dGBXt5SCDSbUQAzKtEcR/4gf/qSjFe2YAwAvSA05WXMH6szdtx6/H/VbK + Uck/WwTHvGObQDFEWmICxPK9AWT0qaFNjlUsi3bjQRdIlYYrXe+6nVMB/Jp1awq7 + tGBqIcWBAoGBAPtXCNuoQhKXqkjJgteQpB+wFav12XRZgpOciYdeviJrgWydpOOu + O9wkiRUctUijRJbUuWCJF7SgYGoT2xTTp/COiOReqs7qXLMuuXCZcPKkMRJj5wmo + Uc2AwUV/o3+PNz1NFK+2RgciXplac7qugIyuxIvBKuVFTBlCg0+if/0pAoGBAL8k + 845wKqOeiawwle/o9lKLGPy1T11GrE6l1A5jRuE1WTVM77jRrb0Hmo0mdfHaf5A0 + EjXGIX/fjcmQzBrEd78eCUsvI2Bgn6xXwhd4TTyWHGZfoQjFqAGkixuLN1oo2h1g + bRreFKfAubFP8MC93z23vnH6tdY2VIA4h5ehUFyJAoGAJqxJrKLDJ+E2TmTTQR/8 + YPPTIdZ+UyzCrrvTXYTydJFeJLxM9suEYmcswJbePgMBNsQckgIGJ8DVlPzhJN88 + ZANKhPkcByKAiQGTfwPdITiqZE4C6rV/gMNi+bKeEa6TrVcC69Z8B/T94VLNo9fd + 58esbmSWmRiEkQ5u7f3u+6ECgYA8+6ANCLJB43nPCu07TpsP+LrvHTWF799XdEa0 + lG3vuiKNA8/TqmoAziU79VJZ6Dkcm9BXga/8aSmGboD/5UDDI+UZLJ/fxtQKmzEc + ZdBWjRnge5AYCV+xrnqHPiJZzIDSMIp+sO3sG2vjKzsHc0x/F1lWagOLpWfORLrV + 4KyP6QKBgAafeSrfK3LM7idiCBuxckLCgFoHa7uXLUNJRS5iIU+bbZLPj2ozu/tk + U0jp7sNk1CyMWI36lR3sujkSyH3lPIXVgrXMuGY3PJRGntN8WlWEsw4VUMGRj3h4 + 5rB+y/UOS+nlEwQ6eOS09GByJDEXOXpcwjFcTr/f7V8mi0jH+gY/ + -----END RSA PRIVATE KEY----- + ''' + prefix = 'cloudfront sign --key-pair-id my_id --url http://example.com/hi ' + + def setUp(self): + files = FileCreator() + self.private_key_file = files.create_file('foo.pem', self.private_key) + self.addCleanup(files.remove_all) + super(TestSign, self).setUp() + + def test_canned_policy(self): + cmdline = ( + self.prefix + '--private-key file://' + self.private_key_file + + ' --date-less-than 2016-1-1') + self.assertIn('Expires', self.run_cmd(cmdline)[0]) + + def test_custom_policy(self): + cmdline = ( + self.prefix + '--private-key file://' + self.private_key_file + + ' --date-less-than 2016-1-1 --ip-address 12.34.56.78') + self.assertIn('Policy', self.run_cmd(cmdline)[0]) From b6ed654f16af2407ffcb9210060b1797dd081451 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Fri, 4 Dec 2015 11:12:51 -0800 Subject: [PATCH 3/7] Add CHANGELOG --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f31167639497..7f4da8102884 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Next Release (TBD) (`issue 1664 `__) * feature:``aws cloudfront create-invalidation``: Add a new --paths option. (`issue 1662 `__) +* feature:``aws cloudfront sign``: Add a new command to sign url. 1.9.11 From fbcab9c8535a86dc7769202812ab1bc94f70b976 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 7 Dec 2015 18:17:13 -0800 Subject: [PATCH 4/7] Ensure a desired URL is returned --- tests/functional/cloudfront/test_sign.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/functional/cloudfront/test_sign.py b/tests/functional/cloudfront/test_sign.py index 34acf081cb95..c78238808750 100644 --- a/tests/functional/cloudfront/test_sign.py +++ b/tests/functional/cloudfront/test_sign.py @@ -10,6 +10,9 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import mock +from botocore.compat import urlparse, parse_qs + from awscli.testutils import FileCreator from awscli.testutils import BaseAWSPreviewCommandParamsTest as \ BaseAWSCommandParamsTest @@ -54,14 +57,28 @@ def setUp(self): self.addCleanup(files.remove_all) super(TestSign, self).setUp() + def assertDesiredUrl(self, url, base, params): + self.assertEqual(len(url.splitlines()), 1, "Expects only 1 line") + self.assertTrue(url.startswith(base), "URL mismatch") + url = url.strip() # Otherwise the last param contains a trailing CRLF + self.assertEqual(parse_qs(urlparse(url).query), params) + def test_canned_policy(self): cmdline = ( self.prefix + '--private-key file://' + self.private_key_file + ' --date-less-than 2016-1-1') - self.assertIn('Expires', self.run_cmd(cmdline)[0]) + expected_params = { + 'Key-Pair-Id': ['my_id'], + 'Expires': ['1451606400'], 'Signature': [mock.ANY]} + self.assertDesiredUrl( + self.run_cmd(cmdline)[0], 'http://example.com/hi', expected_params) def test_custom_policy(self): cmdline = ( self.prefix + '--private-key file://' + self.private_key_file + ' --date-less-than 2016-1-1 --ip-address 12.34.56.78') - self.assertIn('Policy', self.run_cmd(cmdline)[0]) + expected_params = { + 'Key-Pair-Id': ['my_id'], + 'Policy': [mock.ANY], 'Signature': [mock.ANY]} + self.assertDesiredUrl( + self.run_cmd(cmdline)[0], 'http://example.com/hi', expected_params) From a14dc954974067fb2c787ccc2d5baf4ea099aa8f Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 7 Dec 2015 18:17:43 -0800 Subject: [PATCH 5/7] Minor code style adjustments --- awscli/customizations/cloudfront.py | 9 +++++---- awscli/handlers.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/awscli/customizations/cloudfront.py b/awscli/customizations/cloudfront.py index d7a3145bbd3e..e74ff900e2b9 100644 --- a/awscli/customizations/cloudfront.py +++ b/awscli/customizations/cloudfront.py @@ -68,7 +68,7 @@ class SignCommand(BasicCommand): YYYY-MM-DD (which means 0AM UTC of that day), YYYY-MM-DDThh:mm:ss (with default timezone as UTC), YYYY-MM-DDThh:mm:ss+hh:mm or YYYY-MM-DDThh:mm:ss-hh:mm (with offset), - or EpochTime. + or EpochTime (which always means UTC). Do NOT use YYYYMMDD, because it will be treated as EpochTime.""" ARG_TABLE = [ { @@ -94,7 +94,7 @@ class SignCommand(BasicCommand): def _run_main(self, args, parsed_globals): signer = CloudFrontSigner( - args.key_pair_id, RsaSigner(args.private_key).sign) + args.key_pair_id, RSASigner(args.private_key).sign) date_less_than = parse_to_aware_datetime(args.date_less_than) if args.date_greater_than is not None: date_greater_than = parse_to_aware_datetime(args.date_greater_than) @@ -106,11 +106,12 @@ def _run_main(self, args, parsed_globals): ip_address=args.ip_address) print(signer.generate_presigned_url(args.url, policy=policy)) else: - print(signer.generate_presigned_url(args.url, date_less_than)) + print(signer.generate_presigned_url(args.url, + date_less_than=date_less_than)) return 0 -class RsaSigner(object): +class RSASigner(object): def __init__(self, private_key): self.priv_key = rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')) diff --git a/awscli/handlers.py b/awscli/handlers.py index c5f780bac58b..4c61e8ee785e 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -33,7 +33,7 @@ from awscli.customizations.rds import register_rds_modify_split from awscli.customizations.putmetricdata import register_put_metric_data from awscli.customizations.sessendemail import register_ses_send_email -from awscli.customizations.cloudfront import register as cloudfront_register +from awscli.customizations.cloudfront import register as register_cloudfront from awscli.customizations.iamvirtmfa import IAMVMFAWrapper from awscli.customizations.argrename import register_arg_renames from awscli.customizations.configure import register_configure_cmd @@ -109,7 +109,6 @@ def awscli_initialize(event_handlers): register_rds_modify_split(event_handlers) register_put_metric_data(event_handlers) register_ses_send_email(event_handlers) - cloudfront_register(event_handlers) IAMVMFAWrapper(event_handlers) register_arg_renames(event_handlers) register_configure_cmd(event_handlers) @@ -142,3 +141,4 @@ def awscli_initialize(event_handlers): event_handlers.register( 'building-argument-table.iot.create-certificate-from-csr', register_create_keys_from_csr_arguments) + register_cloudfront(event_handlers) From c0e370e6a60ca59b2a2846e49f089f8218d40cdf Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Tue, 8 Dec 2015 10:37:29 -0800 Subject: [PATCH 6/7] Provide more description to CLI options based on CloudFront docs --- awscli/customizations/cloudfront.py | 40 ++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/awscli/customizations/cloudfront.py b/awscli/customizations/cloudfront.py index e74ff900e2b9..2d6b051862b2 100644 --- a/awscli/customizations/cloudfront.py +++ b/awscli/customizations/cloudfront.py @@ -10,6 +10,7 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import sys import time import random @@ -64,7 +65,7 @@ def _add_sign(command_table, session, **kwargs): class SignCommand(BasicCommand): NAME = 'sign' DESCRIPTION = 'Sign a given url.' - DATE_HELP = """Supported formats include: + DATE_FORMAT = """Supported formats include: YYYY-MM-DD (which means 0AM UTC of that day), YYYY-MM-DDThh:mm:ss (with default timezone as UTC), YYYY-MM-DDThh:mm:ss+hh:mm or YYYY-MM-DDThh:mm:ss-hh:mm (with offset), @@ -80,34 +81,49 @@ class SignCommand(BasicCommand): { 'name': 'key-pair-id', 'required': True, - 'help_text': 'The ID of your CloudFront key pairs.', + 'help_text': ( + "The active CloudFront key pair Id for the key pair " + "that you're using to generate the signature."), }, { 'name': 'private-key', 'required': True, 'help_text': 'file://path/to/your/private-key.pem', }, - {'name': 'date-less-than', 'required': True, 'help_text': DATE_HELP}, - {'name': 'date-greater-than', 'help_text': DATE_HELP}, - {'name': 'ip-address', 'help_text': 'Format: x.x.x.x/x or x.x.x.x'}, + { + 'name': 'date-less-than', 'required': True, + 'help_text': + 'The expiration date and time for the URL. ' + DATE_FORMAT, + }, + { + 'name': 'date-greater-than', + 'help_text': + 'An optional start date and time for the URL. ' + DATE_FORMAT, + }, + { + 'name': 'ip-address', + 'help_text': ( + 'An optional IP address or IP address range to allow client ' + 'making the GET request from. Format: x.x.x.x/x or x.x.x.x'), + }, ] def _run_main(self, args, parsed_globals): signer = CloudFrontSigner( args.key_pair_id, RSASigner(args.private_key).sign) date_less_than = parse_to_aware_datetime(args.date_less_than) - if args.date_greater_than is not None: - date_greater_than = parse_to_aware_datetime(args.date_greater_than) - else: - date_greater_than = None + date_greater_than = args.date_greater_than + if date_greater_than is not None: + date_greater_than = parse_to_aware_datetime(date_greater_than) if date_greater_than is not None or args.ip_address is not None: policy = signer.build_policy( args.url, date_less_than, date_greater_than=date_greater_than, ip_address=args.ip_address) - print(signer.generate_presigned_url(args.url, policy=policy)) + sys.stdout.write(signer.generate_presigned_url( + args.url, policy=policy)) else: - print(signer.generate_presigned_url(args.url, - date_less_than=date_less_than)) + sys.stdout.write(signer.generate_presigned_url( + args.url, date_less_than=date_less_than)) return 0 From c56d85948ec502dd2d94bb2aa15adc7f1e27edff Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Tue, 8 Dec 2015 14:43:44 -0800 Subject: [PATCH 7/7] Refine CHANGELOG --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f4da8102884..0dff63dbc1cf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,7 +9,8 @@ Next Release (TBD) (`issue 1664 `__) * feature:``aws cloudfront create-invalidation``: Add a new --paths option. (`issue 1662 `__) -* feature:``aws cloudfront sign``: Add a new command to sign url. +* feature:``aws cloudfront sign``: Add a new command to create a signed url. + (`issue 1668 `__) 1.9.11