Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudfront sign #1668

Merged
merged 7 commits into from
Dec 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Next Release (TBD)
(`issue 1664 <https://github.com/aws/aws-cli/pull/1664>`__)
* feature:``aws cloudfront create-invalidation``: Add a new --paths option.
(`issue 1662 <https://github.com/aws/aws-cli/pull/1662>`__)
* feature:``aws cloudfront sign``: Add a new command to create a signed url.
(`issue 1668 <https://github.com/aws/aws-cli/pull/1668>`__)


1.9.11
Expand Down
86 changes: 85 additions & 1 deletion awscli/customizations/cloudfront.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@
# 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

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(
Expand Down Expand Up @@ -49,3 +56,80 @@ 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_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),
or EpochTime (which always means UTC).
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 active CloudFront key pair Id for the key pair "
"that you're using to generate the signature."),
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the private-key represent? Its good that you put how to specify it but it tells the user nothing about the private key. For a few of these arguments it seems that we are making the assumption the user has read the cloudfront docs. I do not think this should be necessary to use the sign command.

'name': 'private-key',
'required': True,
'help_text': 'file://path/to/your/private-key.pem',
},
{
'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)
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)
sys.stdout.write(signer.generate_presigned_url(
args.url, policy=policy))
else:
sys.stdout.write(signer.generate_presigned_url(
args.url, date_less_than=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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blank line between methods.

return rsa.sign(message, self.priv_key, 'SHA-1')
4 changes: 2 additions & 2 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
84 changes: 84 additions & 0 deletions tests/functional/cloudfront/test_sign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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.
import mock
from botocore.compat import urlparse, parse_qs

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 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')
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')
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)