diff --git a/boto/__init__.py b/boto/__init__.py index 7170682a69..96cde8dcd7 100644 --- a/boto/__init__.py +++ b/boto/__init__.py @@ -407,6 +407,20 @@ def connect_ses(aws_access_key_id=None, aws_secret_access_key=None, **kwargs): from boto.ses import SESConnection return SESConnection(aws_access_key_id, aws_secret_access_key, **kwargs) +def connect_sts(aws_access_key_id=None, aws_secret_access_key=None, **kwargs): + """ + :type aws_access_key_id: string + :param aws_access_key_id: Your AWS Access Key ID + + :type aws_secret_access_key: string + :param aws_secret_access_key: Your AWS Secret Access Key + + :rtype: :class:`boto.sts.STSConnection` + :return: A connection to Amazon's STS + """ + from boto.sts import STSConnection + return STSConnection(aws_access_key_id, aws_secret_access_key, **kwargs) + def connect_ia(ia_access_key_id=None, ia_secret_access_key=None, is_secure=False, **kwargs): """ @@ -418,9 +432,10 @@ def connect_ia(ia_access_key_id=None, ia_secret_access_key=None, section called "ia_access_key_id" :type ia_secret_access_key: string - :param ia_secret_access_key: Your IA Secret Access Key. This will also look in your - boto config file for an entry in the Credentials - section called "ia_secret_access_key" + :param ia_secret_access_key: Your IA Secret Access Key. This will also + look in your boto config file for an entry + in the Credentials section called + "ia_secret_access_key" :rtype: :class:`boto.s3.connection.S3Connection` :return: A connection to the Internet Archive diff --git a/boto/auth.py b/boto/auth.py index 59b80daaed..89758848ee 100644 --- a/boto/auth.py +++ b/boto/auth.py @@ -77,7 +77,8 @@ def __init__(self, host, config, provider): self._provider = provider self._hmac = hmac.new(self._provider.secret_key, digestmod=sha) if sha256: - self._hmac_256 = hmac.new(self._provider.secret_key, digestmod=sha256) + self._hmac_256 = hmac.new(self._provider.secret_key, + digestmod=sha256) else: self._hmac_256 = None @@ -113,6 +114,9 @@ def add_auth(self, http_request, **kwargs): if not headers.has_key('Date'): headers['Date'] = formatdate(usegmt=True) + if self._provider.security_token: + key = self._provider.security_token_header + headers[key] = self._provider.security_token c_string = boto.utils.canonical_string(method, auth_path, headers, None, self._provider) b64_hmac = self.sign_string(c_string) @@ -187,7 +191,8 @@ def add_auth(self, http_request, **kwargs): # if this is a retried request, the qs from the previous try will # already be there, we need to get rid of that and rebuild it http_request.path = http_request.path.split('?')[0] - http_request.path = (http_request.path + '?' + qs + '&Signature=' + urllib.quote(signature)) + http_request.path = (http_request.path + '?' + qs + + '&Signature=' + urllib.quote(signature)) class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler): """Provides Signature V0 Signing""" diff --git a/boto/connection.py b/boto/connection.py index 9b527365e2..a93346a69f 100644 --- a/boto/connection.py +++ b/boto/connection.py @@ -364,7 +364,8 @@ class AWSAuthConnection(object): def __init__(self, host, aws_access_key_id=None, aws_secret_access_key=None, is_secure=True, port=None, proxy=None, proxy_port=None, proxy_user=None, proxy_pass=None, debug=0, - https_connection_factory=None, path='/', provider='aws'): + https_connection_factory=None, path='/', + provider='aws', security_token=None): """ :type host: str :param host: The host to make the connection to @@ -463,7 +464,8 @@ def __init__(self, host, aws_access_key_id=None, aws_secret_access_key=None, self.provider = Provider(provider, aws_access_key_id, - aws_secret_access_key) + aws_secret_access_key, + security_token) # allow config file to override default host if self.provider.host: diff --git a/boto/provider.py b/boto/provider.py index 44c6430927..5fb64c62f0 100644 --- a/boto/provider.py +++ b/boto/provider.py @@ -151,10 +151,12 @@ class Provider(object): } } - def __init__(self, name, access_key=None, secret_key=None): + def __init__(self, name, access_key=None, secret_key=None, + security_token=None): self.host = None self.access_key = access_key self.secret_key = secret_key + self.security_token = security_token self.name = name self.acl_class = self.AclClassMap[self.name] self.canned_acls = self.CannedAclsMap[self.name] diff --git a/boto/s3/connection.py b/boto/s3/connection.py index 44d3e5ee43..80209b7dbc 100644 --- a/boto/s3/connection.py +++ b/boto/s3/connection.py @@ -140,14 +140,14 @@ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, proxy_user=None, proxy_pass=None, host=DefaultHost, debug=0, https_connection_factory=None, calling_format=SubdomainCallingFormat(), path='/', - provider='aws', bucket_class=Bucket): + provider='aws', bucket_class=Bucket, security_token=None): self.calling_format = calling_format self.bucket_class = bucket_class AWSAuthConnection.__init__(self, host, aws_access_key_id, aws_secret_access_key, is_secure, port, proxy, proxy_port, proxy_user, proxy_pass, debug=debug, https_connection_factory=https_connection_factory, - path=path, provider=provider) + path=path, provider=provider, security_token=security_token) def _required_auth_capability(self): return ['s3'] diff --git a/boto/ses/__init__.py b/boto/ses/__init__.py index 167080b9b3..f893423d44 100644 --- a/boto/ses/__init__.py +++ b/boto/ses/__init__.py @@ -21,4 +21,49 @@ # IN THE SOFTWARE. from connection import SESConnection +from boto.regioninfo import RegionInfo +def regions(): + """ + Get all available regions for the SES service. + + :rtype: list + :return: A list of :class:`boto.regioninfo.RegionInfo` instances + """ + return [RegionInfo(name='us-east-1', + endpoint='email.us-east-1.amazonaws.com', + connection_cls=SESConnection)] + +def connect_to_region(region_name, **kw_params): + """ + Given a valid region name, return a + :class:`boto.sns.connection.SESConnection`. + + :type: str + :param region_name: The name of the region to connect to. + + :rtype: :class:`boto.sns.connection.SESConnection` or ``None`` + :return: A connection to the given region, or None if an invalid region + name is given + """ + for region in regions(): + if region.name == region_name: + return region.connect(**kw_params) + return None + +def get_region(region_name, **kw_params): + """ + Find and return a :class:`boto.regioninfo.RegionInfo` object + given a region name. + + :type: str + :param: The name of the region. + + :rtype: :class:`boto.regioninfo.RegionInfo` + :return: The RegionInfo object for the given region or None if + an invalid region name is provided. + """ + for region in regions(**kw_params): + if region.name == region_name: + return region + return None diff --git a/boto/ses/connection.py b/boto/ses/connection.py index 4bb98e0b7b..8cd80c3981 100644 --- a/boto/ses/connection.py +++ b/boto/ses/connection.py @@ -22,6 +22,7 @@ from boto.connection import AWSAuthConnection from boto.exception import BotoServerError +from boto.regioninfo import RegionInfo import boto import boto.jsonresponse @@ -32,15 +33,23 @@ class SESConnection(AWSAuthConnection): ResponseError = BotoServerError - DefaultHost = 'email.us-east-1.amazonaws.com' + DefaultRegionName = 'us-east-1' + DefaultRegionEndpoint = 'email.us-east-1.amazonaws.com' APIVersion = '2010-12-01' def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, - port=None, proxy=None, proxy_port=None, - host=DefaultHost, debug=0): - AWSAuthConnection.__init__(self, host, aws_access_key_id, - aws_secret_access_key, True, port, proxy, - proxy_port, debug=debug) + is_secure=True, port=None, proxy=None, proxy_port=None, + proxy_user=None, proxy_pass=None, debug=0, + https_connection_factory=None, region=None, path='/'): + if not region: + region = RegionInfo(self, self.DefaultRegionName, + self.DefaultRegionEndpoint) + self.region = region + AWSAuthConnection.__init__(self, self.region.endpoint, + aws_access_key_id, aws_secret_access_key, + is_secure, port, proxy, proxy_port, + proxy_user, proxy_pass, debug, + https_connection_factory, path) def _required_auth_capability(self): return ['ses'] diff --git a/boto/sts/__init__.py b/boto/sts/__init__.py new file mode 100644 index 0000000000..7ee10b4283 --- /dev/null +++ b/boto/sts/__init__.py @@ -0,0 +1,70 @@ +# Copyright (c) 2010-2011 Mitch Garnaat http://garnaat.org/ +# Copyright (c) 2010-2011, Eucalyptus Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from connection import STSConnection +from boto.regioninfo import RegionInfo + +def regions(): + """ + Get all available regions for the STS service. + + :rtype: list + :return: A list of :class:`boto.regioninfo.RegionInfo` instances + """ + return [RegionInfo(name='us-east-1', + endpoint='sts.amazonaws.com', + connection_cls=STSConnection) + ] + +def connect_to_region(region_name, **kw_params): + """ + Given a valid region name, return a + :class:`boto.sts.connection.STSConnection`. + + :type: str + :param region_name: The name of the region to connect to. + + :rtype: :class:`boto.sts.connection.STSConnection` or ``None`` + :return: A connection to the given region, or None if an invalid region + name is given + """ + for region in regions(): + if region.name == region_name: + return region.connect(**kw_params) + return None + +def get_region(region_name, **kw_params): + """ + Find and return a :class:`boto.regioninfo.RegionInfo` object + given a region name. + + :type: str + :param: The name of the region. + + :rtype: :class:`boto.regioninfo.RegionInfo` + :return: The RegionInfo object for the given region or None if + an invalid region name is provided. + """ + for region in regions(**kw_params): + if region.name == region_name: + return region + return None diff --git a/boto/sts/connection.py b/boto/sts/connection.py new file mode 100644 index 0000000000..6761327912 --- /dev/null +++ b/boto/sts/connection.py @@ -0,0 +1,90 @@ +# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/ +# Copyright (c) 2011, Eucalyptus Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from boto.connection import AWSQueryConnection +from boto.regioninfo import RegionInfo +from credentials import Credentials, FederationToken +import boto + +class STSConnection(AWSQueryConnection): + + DefaultRegionName = 'us-east-1' + DefaultRegionEndpoint = 'sts.amazonaws.com' + APIVersion = '2011-06-15' + + def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, + is_secure=True, port=None, proxy=None, proxy_port=None, + proxy_user=None, proxy_pass=None, debug=0, + https_connection_factory=None, region=None, path='/', + converter=None): + if not region: + region = RegionInfo(self, self.DefaultRegionName, + self.DefaultRegionEndpoint, + connection_cls=STSConnection) + self.region = region + AWSQueryConnection.__init__(self, aws_access_key_id, + aws_secret_access_key, + is_secure, port, proxy, proxy_port, + proxy_user, proxy_pass, + self.region.endpoint, debug, + https_connection_factory, path) + + def _required_auth_capability(self): + return ['sign-v2'] + + def get_session_token(self, duration=None): + """ + :type duration: int + :param duration: The number of seconds the credentials should + remain valid. + + """ + params = {} + if duration: + params['Duration'] = duration + return self.get_object('GetSessionToken', params, + Credentials, verb='POST') + + + def get_federation_token(self, name, duration=None, policy=None): + """ + :type name: str + :param name: The name of the Federated user associated with + the credentials. + + :type duration: int + :param duration: The number of seconds the credentials should + remain valid. + + :type policy: str + :param policy: A JSON policy to associate with these credentials. + + """ + params = {'Name' : name} + if duration: + params['Duration'] = duration + if policy: + params['Policy'] = policy + return self.get_object('GetFederationToken', params, + FederationToken, verb='POST') + + diff --git a/boto/sts/credentials.py b/boto/sts/credentials.py new file mode 100644 index 0000000000..68af3a8cf8 --- /dev/null +++ b/boto/sts/credentials.py @@ -0,0 +1,90 @@ +# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/ +# Copyright (c) 2011, Eucalyptus Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +class Credentials(object): + """ + ivar access_key: The AccessKeyID. + ivar secret_key: The SecretAccessKey. + ivar session_token: The session token that must be passed with + requests to use the temporary credentials + ivar expiration: The timestamp for when the credentials will expire + """ + + def __init__(self, parent=None): + self.parent = parent + self.access_key = None + self.secret_key = None + self.session_token = None + self.expiration = None + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'AccessKeyId': + self.access_key = value + elif name == 'SecretAccessKey': + self.secret_key = value + elif name == 'SessionToken': + self.session_token = value + elif name == 'Expiration': + self.expiration = value + elif name == 'RequestId': + self.request_id = value + else: + pass + +class FederationToken(object): + """ + ivar credentials: A Credentials object containing the credentials. + ivar federated_user_arn: ARN specifying federated user using credentials. + ivar federated_user_id: The ID of the federated user using credentials. + ivar packed_policy_size: A percentage value indicating the size of + the policy in packed form + """ + + def __init__(self, parent=None): + self.parent = parent + self.credentials = None + self.federated_user_arn = None + self.federated_user_id = None + self.packed_policy_size = None + + def startElement(self, name, attrs, connection): + if name == 'Credentials': + self.credentials = Credentials() + return self.credentials + else: + return None + + def endElement(self, name, value, connection): + if name == 'Arn': + self.federated_user_arn = value + elif name == 'FederatedUserId': + self.federated_user_id = value + elif name == 'PackedPolicySize': + self.packed_policy_size = int(value) + elif name == 'RequestId': + self.request_id = value + else: + pass + diff --git a/docs/source/ref/index.rst b/docs/source/ref/index.rst index 5ebfab3b11..37c7abbd25 100644 --- a/docs/source/ref/index.rst +++ b/docs/source/ref/index.rst @@ -29,5 +29,6 @@ API Reference ses sns sqs + sts vpc diff --git a/docs/source/ref/sts.rst b/docs/source/ref/sts.rst new file mode 100644 index 0000000000..fdfc185c19 --- /dev/null +++ b/docs/source/ref/sts.rst @@ -0,0 +1,25 @@ +.. ref-sts + +=== +STS +=== + +boto.sts +-------- + +.. automodule:: boto.sts + :members: + :undoc-members: + +.. autoclass:: boto.sts.SNSConnection + :members: + :undoc-members: + +boto.sts.credentials +---------------- + +.. automodule:: boto.sts.credentials + :members: + :undoc-members: + + diff --git a/setup.py b/setup.py index a802766055..7b4cb7d2fd 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ "boto.services", "boto.cloudfront", "boto.roboto", "boto.rds", "boto.vpc", "boto.fps", "boto.emr", "boto.sns", "boto.ecs", "boto.iam", "boto.route53", "boto.ses", - "boto.cloudformation"], + "boto.cloudformation", "boto.sts"], license = "MIT", platforms = "Posix; MacOS X; Windows", classifiers = ["Development Status :: 5 - Production/Stable",