Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
Wire up AWS login and add unit tests for login/auth. GUI-188
Browse files Browse the repository at this point in the history
  • Loading branch information
kamalgill committed Nov 12, 2013
1 parent d909f60 commit 75bb7e3
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 44 deletions.
114 changes: 88 additions & 26 deletions koala/models/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
"""
import base64
import time
import hashlib
import hmac
import logging
import urllib
import urllib2
import urlparse
import xml

from datetime import datetime

from boto.handler import XmlHandler as BotoXmlHandler
from boto.sts.credentials import Credentials
from pyramid.security import Authenticated, authenticated_userid
Expand Down Expand Up @@ -41,10 +43,67 @@ def groupfinder(user_id, request):
return [Authenticated]


class TokenAuthenticator(object):
class AWSQuery(object):
"""
Build a signed request to an Amazon AWS endpoint.
Credit: https://github.com/kesor/amazon-queries
:type endpoint: string
:param endpoint: from http://docs.amazonwebservices.com/general/latest/gr/rande.html
:type key_id: string
:param key_id: The Access Key ID for the request sender.
:type secret_key: string
:param secret_key: Secret Access Key used for request signature.
:type parameters: dict
:param parameters: Optional additional request parameters.
"""
def __init__(self, endpoint, key_id, secret_key, parameters=None):
parameters = parameters or dict()
parsed = urlparse.urlparse(endpoint)
self.host = parsed.hostname
self.path = parsed.path or '/'
self.endpoint = endpoint
self.secret_key = secret_key
self.parameters = dict({
'AWSAccessKeyId': key_id,
'SignatureVersion': 2,
'SignatureMethod': 'HmacSHA256',
}, **parameters)

@property
def signed_parameters(self):
self.parameters['Timestamp'] = datetime.utcnow().isoformat()
params = dict(self.parameters, **{'Signature': self.signature})
return urllib.urlencode(params)

@property
def signature(self):
params = urllib.urlencode(sorted(self.parameters.items()))
text = "\n".join(['POST', self.host, self.path, params])
auth = hmac.new(str(self.secret_key), msg=text, digestmod=hashlib.sha256)
return base64.b64encode(auth.digest())


class EucaAuthenticator(object):
"""Eucalyptus cloud token authenticator"""

def __init__(self, host, duration):
# make the call to STS service to authenticate with the CLC
self.auth_url = "https://{host}:8773/{service}?Action={action}&DurationSeconds={dur}&Version={ver}".format(
"""
Configure connection to Eucalyptus STS service to authenticate with the CLC (cloud controller)
:type host: string
:param host: IP address or FQDN of CLC host
:type duration: int
:param duration: Duration of the session token (in seconds)
"""
template = 'https://{host}:8773/{service}?Action={action}&DurationSeconds={dur}&Version={ver}'
self.auth_url = template.format(
host=host,
dur=duration,
service='services/Tokens',
Expand Down Expand Up @@ -79,34 +138,36 @@ def authenticate(self, account, user, passwd, new_passwd=None, timeout=15):
logging.info("Authenticated Eucalyptus user: " + account + "/" + user)
return creds

@staticmethod
def authenticate_aws(access_key, secret_key, duration):

class AWSAuthenticator(AWSQuery):

def __init__(self, key_id, secret_key, duration):
"""
Configure connection to AWS STS service
:type key_id: string
:param key_id: AWS access key
:type secret_key: string
:param secret_key: AWS secret key
:type duration: int
:param duration: Duration of AWS session token, in seconds
"""
self.endpoint = 'https://sts.amazonaws.com'
params = dict(
AWSAccessKeyId=access_key,
Action='GetSessionToken',
DurationSeconds=duration,
SignatureMethod='HmacSHA256',
SignatureVersion='2',
Timestamp=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
Version='2011-06-15'
)
encoded_params = urllib.urlencode(params)
string_to_sign = unicode("POST\nsts.amazonaws.com\n/\n{0}".format(encoded_params)).encode('utf-8')
secret_key = unicode(secret_key).encode('utf-8')

# Sign the request
signature = hmac.new(key=secret_key, msg=string_to_sign, digestmod=hashlib.sha256).digest()

# Base64 encode the signature
encoded_sig = base64.encodestring(signature).strip()
super(AWSAuthenticator, self).__init__(self.endpoint, key_id, secret_key, parameters=params)

# Make the signature URL safe and add to URL params
encoded = urllib.quote(encoded_sig)
params['Signature'] = encoded
package = base64.encodestring(urllib.urlencode(params))

req = urllib2.Request('https://sts.amazonaws.com', data=package)
response = urllib2.urlopen(req, timeout=20)
def authenticate(self, timeout=20):
""" Make authentication request to AWS STS service
Timeout defaults to 20 seconds"""
req = urllib2.Request(self.endpoint, data=self.signed_parameters)
response = urllib2.urlopen(req, timeout=timeout)
body = response.read()

# parse AccessKeyId, SecretAccessKey and SessionToken
Expand All @@ -115,3 +176,4 @@ def authenticate_aws(access_key, secret_key, duration):
xml.sax.parseString(body, h)
logging.info("Authenticated AWS user")
return creds

46 changes: 38 additions & 8 deletions koala/tests/auth/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"""
from pyramid.testing import DummyRequest

from koala.forms.login import EucaLoginForm
from koala.tests import BaseFormTestCase
from koala.forms.login import AWSLoginForm, EucaLoginForm
from koala.models.auth import AWSAuthenticator, EucaAuthenticator
from koala.tests import BaseTestCase, BaseFormTestCase


class EucaLoginFormTestCase(BaseFormTestCase):
Expand All @@ -17,9 +18,38 @@ def test_required_fields(self):
self.assert_required('username')
self.assert_required('password')

def test_valid_form(self):
form = EucaLoginForm(
request=self.request,
data=dict(account='foo', username='bar', password='baz')
)
self.assertEqual(form.errors, {})

class AWSLoginFormTestCase(BaseFormTestCase):
form_class = AWSLoginForm
request = DummyRequest()

def test_required_fields(self):
self.assert_required('access_key')
self.assert_required('secret_key')


class EucaAuthTestCase(BaseTestCase):
def test_euca_authenticator(self):
host = 'localhost'
duration = 3600
auth = EucaAuthenticator(host=host, duration=duration)
expected_url = ''.join([
'https://localhost:8773/services/Tokens?Action=GetSessionToken',
'&DurationSeconds=3600&Version=2011-06-15'
])
self.assertEqual(auth.auth_url, expected_url)


class AWSAuthTestCase(BaseTestCase):
def test_aws_authenticator(self):
access_key = 'foo_accesskey'
secret_key = 'super-seekrit-key'
endpoint = 'https://sts.amazonaws.com'
auth = AWSAuthenticator(key_id=access_key, secret_key=secret_key, duration=3600)
self.assertEqual(auth.endpoint, endpoint)
self.assertEqual(auth.host, 'sts.amazonaws.com')
self.assertEqual(auth.parameters.get('AWSAccessKeyId'), access_key)
self.assertEqual(auth.parameters.get('SignatureVersion'), 2)
self.assertEqual(auth.parameters.get('SignatureMethod'), 'HmacSHA256')
self.assertEqual(auth.parameters.get('Action'), 'GetSessionToken')
self.assertEqual(auth.parameters.get('Version'), '2011-06-15')
5 changes: 0 additions & 5 deletions koala/tests/instances/test_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@


class InstancesViewTests(BaseLiveTestCase):
def setUp(self):
self.config = testing.setUp()

def tearDown(self):
testing.tearDown()

def test_instances_view_defaults(self):
from koala.views.instances import InstancesView
Expand Down
9 changes: 4 additions & 5 deletions koala/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pyramid.view import view_config, forbidden_view_config

from ..forms.login import EucaLoginForm, AWSLoginForm
from ..models.auth import TokenAuthenticator
from ..models.auth import AWSAuthenticator, EucaAuthenticator


@forbidden_view_config()
Expand Down Expand Up @@ -55,10 +55,10 @@ def handle_login(self):
session = self.request.session
clchost = self.request.registry.settings.get('clchost')
duration = self.request.registry.settings.get('session.cookie_expires')
auth = TokenAuthenticator(host=clchost, duration=duration)
new_passwd = None

if login_type == 'Eucalyptus':
auth = EucaAuthenticator(host=clchost, duration=duration)
euca_login_form = EucaLoginForm(self.request, formdata=self.request.params)
if euca_login_form.validate():
account = self.request.params.get('account')
Expand Down Expand Up @@ -86,9 +86,8 @@ def handle_login(self):
aws_access_key = self.request.params.get('access_key')
aws_secret_key = self.request.params.get('secret_key')
try:
creds = auth.authenticate_aws(
access_key=aws_access_key, secret_key=aws_secret_key, duration=duration
)
auth = AWSAuthenticator(key_id=aws_access_key, secret_key=aws_secret_key, duration=duration)
creds = auth.authenticate(timeout=10)
session['cloud_type'] = 'aws'
session['session_token'] = creds.session_token
session['access_key'] = creds.access_key
Expand Down
27 changes: 27 additions & 0 deletions licenses/amazon-queries.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2013, Evgeny Zislis

All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of `aws-api-engine` nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

0 comments on commit 75bb7e3

Please sign in to comment.