Handle EC2 Credentials on /tokens

- EC2 credentials are just another type of credential
  that can be passed in to /tokens. This patch now
  handles those credentials correctly.
- POST /tokens {'auth': {'OS-EC2-ec2Credentials...}
  now works correctly.
- Multiple credential handling is improved. There is
  a detect_credentials call in utils now to detect
  the different types.

Addresses:
- bug 843058
- bug 904523
Prepares for:
- bp s3token
- bp keystone-client

Change-Id: I43931fdc7b8a9b76eac351e11394cfa507911578
This commit is contained in:
Ziad Sawalha 2012-01-20 04:12:10 -06:00
parent 9e1e113901
commit 6362857541
7 changed files with 294 additions and 27 deletions

View File

@ -47,26 +47,35 @@ class TokenController(wsgi.Controller):
@utils.wrap_error
def authenticate(self, req):
try:
auth_with_credentials = utils.get_normalized_request_content(
auth.AuthWithPasswordCredentials, req)
result = self.identity_service.authenticate(auth_with_credentials)
except fault.BadRequestFault as e1:
try:
unscoped = utils.get_normalized_request_content(
auth.AuthWithUnscopedToken, req)
result = self.identity_service.\
authenticate_with_unscoped_token(unscoped)
except fault.BadRequestFault as e2:
if e1.msg == e2.msg:
raise e1
else:
raise fault.BadRequestFault(e1.msg + ' or ' + e2.msg)
credential_type = utils.detect_credential_type(req)
return utils.send_result(200, req, result)
if credential_type == "passwordCredentials":
auth_with_credentials = utils.get_normalized_request_content(
auth.AuthWithPasswordCredentials, req)
result = self.identity_service.authenticate(
auth_with_credentials)
return utils.send_result(200, req, result)
elif credential_type == "token":
unscoped = utils.get_normalized_request_content(
auth.AuthWithUnscopedToken, req)
result = self.identity_service.\
authenticate_with_unscoped_token(unscoped)
return utils.send_result(200, req, result)
elif credential_type in ["ec2Credentials", "OS-KSEC2-ec2Credentials"]:
return self._authenticate_ec2(req)
else:
raise fault.BadRequestFault("Invalid credentials %s" %
credential_type)
@utils.wrap_error
def authenticate_ec2(self, req):
return self._authenticate_ec2(req)
def _authenticate_ec2(self, req):
"""Undecorated EC2 handler"""
creds = utils.get_normalized_request_content(auth.Ec2Credentials, req)
return utils.send_result(200, req,
self.identity_service.authenticate_ec2(creds))

View File

@ -202,10 +202,16 @@ class Ec2Credentials(object):
dom = etree.Element("root")
dom.append(etree.fromstring(xml_str))
root = dom.find("{http://docs.openstack.org/identity/api/v2.0}"
"ec2Credentials")
"auth")
xmlns = "http://docs.openstack.org/identity/api/ext/OS-KSEC2/v1.0"
if root is None:
root = dom.find("{%s}ec2Credentials" % xmlns)
else:
root = root.find("{%s}ec2Credentials" % xmlns)
if root is None:
raise fault.BadRequestFault("Expecting ec2Credentials")
access = root.get("access")
access = root.get("key")
utils.check_empty_string(access, "Expecting an access key.")
signature = root.get("signature")
utils.check_empty_string(signature, "Expecting a signature.")
@ -225,10 +231,19 @@ class Ec2Credentials(object):
@staticmethod
def from_json(json_str):
try:
obj = json.loads(json_str)
if not "ec2Credentials" in obj:
root = json.loads(json_str)
if "auth" in root:
obj = root['auth']
else:
obj = root
if "OS-KSEC2-ec2Credentials" in obj:
cred = obj["OS-KSEC2-ec2Credentials"]
elif "ec2Credentials" in obj:
cred = obj["ec2Credentials"]
else:
raise fault.BadRequestFault("Expecting ec2Credentials")
cred = obj["ec2Credentials"]
# Check that fields are valid
invalid = [key for key in cred if key not in\
['username', 'access', 'signature', 'params',

View File

@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) # pylint: disable=C0103
FLAGS = flags.FLAGS
flags.DEFINE_string('keystone_ec2_url',
'http://localhost:5000/v2.0/ec2tokens',
'http://localhost:5000/v2.0/tokens',
'URL to get token from ec2 request.')
@ -57,7 +57,7 @@ class EC2Token(wsgi.Middleware):
auth_params.pop('Signature')
# Authenticate the request.
creds = {'ec2Credentials': {'access': access,
creds = {'OS-KSEC2-ec2Credentials': {'access': access,
'signature': signature,
'host': req.host,
'verb': req.method,

View File

@ -41,6 +41,7 @@ class ServiceApi(wsgi.Router):
mapper.connect("/tokens", controller=auth_controller,
action="authenticate",
conditions=dict(method=["POST"]))
# TODO(zns): this should be deprecated
mapper.connect("/ec2tokens", controller=auth_controller,
action="authenticate_ec2",
conditions=dict(method=["POST"]))

View File

@ -28,10 +28,135 @@ LOGGER = logging.getLogger(__name__)
class EC2AuthnMethods(base.ServiceAPITest):
@jsonify
def test_valid_authn_ec2_success_json(self):
"""Tests correct syntax with {"auth":} wrapper and extension """
url = "/tokens"
access = "xpd285.access"
secret = "345fgi.secret"
kwargs = {
"user_name": self.auth_user['name'],
"tenant_id": self.auth_user['tenant_id'],
"type": "EC2",
"key": access,
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
req = self.get_request('POST', url)
params = {
"SignatureVersion": "2",
"one_param": "5",
"two_params": "happy",
}
credentials = {
"access": access,
"verb": "GET",
"params": params,
"host": "some.host.com:8773",
"path": "services/Cloud",
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.Ec2Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds)
body = {
"auth": {
"OS-KSEC2-ec2Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'access': {
u'token': {
u'id': self.auth_token_id,
u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f")},
u'user': {
u'id': unicode(self.auth_user['id']),
u'name': self.auth_user['name'],
u'roles': [{u'description': u'regular role',
u'id': u'0',
u'name': u'regular_role'}]}}}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.status_ok()
@jsonify
def test_authn_ec2_success_json(self):
"""Tests syntax with {"auth":} wrapper """
self._auth_to_url(url="/ec2tokens")
@jsonify
def test_authn_ec2_success_json_contract(self):
"""Tests syntax with {"auth":} wrapper """
self._auth_to_url(url="/tokens")
def _auth_to_url(self, url):
"""
Test that good ec2 credentials returns a 200 OK
Test that plain ec2 credentials returns a 200 OK
"""
access = "xpd285.access"
secret = "345fgi.secret"
kwargs = {
"user_name": self.auth_user['name'],
"tenant_id": self.auth_user['tenant_id'],
"type": "EC2",
"key": access,
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
req = self.get_request('POST', url)
params = {
"SignatureVersion": "2",
"one_param": "5",
"two_params": "happy",
}
credentials = {
"access": access,
"verb": "GET",
"params": params,
"host": "some.host.com:8773",
"path": "services/Cloud",
"signature": None,
}
sign = signer.Signer(secret)
obj_creds = auth.Ec2Credentials(**credentials)
credentials['signature'] = sign.generate(obj_creds)
body = {
"auth": {
"ec2Credentials": credentials,
}
}
req.body = json.dumps(body)
self.get_response()
expected = {
u'access': {
u'token': {
u'id': self.auth_token_id,
u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f")},
u'user': {
u'id': unicode(self.auth_user['id']),
u'name': self.auth_user['name'],
u'roles': [{u'description': u'regular role',
u'id': u'0',
u'name': u'regular_role'}]}}}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.status_ok()
@jsonify
def test_old_authn_ec2_success_json(self):
"""Tests old syntax without {"auth":} wrapper """
self._old_auth_to_url(url="/ec2tokens")
@jsonify
def test_old_authn_ec2_success_json_contract(self):
"""Tests old syntax without {"auth":} wrapper """
self._old_auth_to_url(url="/tokens")
def _old_auth_to_url(self, url):
"""
Test that old ec2 credentials returns a 200 OK
"""
access = "xpd285.access"
secret = "345fgi.secret"
@ -43,7 +168,6 @@ class EC2AuthnMethods(base.ServiceAPITest):
"secret": secret,
}
self.fixture_create_credentials(**kwargs)
url = "/ec2tokens"
req = self.get_request('POST', url)
params = {
"SignatureVersion": "2",
@ -78,7 +202,6 @@ class EC2AuthnMethods(base.ServiceAPITest):
u'roles': [{u'description': u'regular role',
u'id': u'0',
u'name': u'regular_role'}]}}}
self.assert_dict_equal(expected, json.loads(self.res.body))
self.status_ok()

View File

@ -1,10 +1,11 @@
import json
import unittest2 as unittest
from keystone import utils
class TestStringEmpty(unittest.TestCase):
'''Unit tests for string functions of utils.py.'''
"""Unit tests for string functions of utils.py."""
def test_is_empty_for_a_valid_string(self):
self.assertFalse(utils.is_empty_string('asdfgf'))
@ -17,3 +18,72 @@ class TestStringEmpty(unittest.TestCase):
def test_is_empty_for_a_number(self):
self.assertFalse(utils.is_empty_string(0))
class TestCredentialDetection(unittest.TestCase):
"""Unit tests for credential type detection"""
def test_detects_passwordCredentials(self):
self.content_type = "application/xml"
self.body = '<auth '\
'xmlns="http://docs.openstack.org/identity/api/v2.0">'\
'<passwordCredentials/></auth>'
self.assertEquals(utils.detect_credential_type(self),
"passwordCredentials")
def test_detects_passwordCredentials_unwrapped(self):
self.content_type = "application/xml"
self.body = '<passwordCredentials '\
'xmlns="http://docs.openstack.org/identity/api/v2.0"/>'
self.assertEquals(utils.detect_credential_type(self),
"passwordCredentials")
def test_detects_no_creds(self):
self.content_type = "application/xml"
self.body = '<auth '\
'xmlns="http://docs.openstack.org/identity/api/v2.0"/>'
self.assertRaises(Exception, utils.detect_credential_type, self)
def test_detects_blank_creds(self):
self.content_type = "application/xml"
self.body = ''
self.assertRaises(Exception, utils.detect_credential_type, self)
def test_detects_anyCredentials(self):
self.content_type = "application/xml"
self.body = '<auth '\
'xmlns="http://docs.openstack.org/identity/api/v2.0">'\
'<anyCredentials/></auth>'
self.assertEquals(utils.detect_credential_type(self),
"anyCredentials")
def test_detects_anyCredentials_json(self):
self.content_type = "application/json"
self.body = json.dumps({'auth': {'anyCredentials': {}}})
self.assertEquals(utils.detect_credential_type(self),
"anyCredentials")
def test_detects_anyUnwrappedCredentials_json(self):
self.content_type = "application/json"
self.body = json.dumps({'anyCredentials': {}})
self.assertEquals(utils.detect_credential_type(self),
"anyCredentials")
def test_detects_anyCredentials_with_tenant_json(self):
self.content_type = "application/json"
self.body = json.dumps({'auth': {'tenantId': '1000',
'anyCredentials': {}}})
self.assertEquals(utils.detect_credential_type(self),
"anyCredentials")
def test_detects_skips_tenant_json(self):
self.content_type = "application/json"
self.body = json.dumps({'auth': {'tenantId': '1000'}})
self.assertRaises(Exception, utils.detect_credential_type, self)
self.body = json.dumps({'auth': {'tenantName': '1000'}})
self.assertRaises(Exception, utils.detect_credential_type, self)
if __name__ == '__main__':
unittest.main()

View File

@ -16,7 +16,9 @@
import functools
import json
import logging
from lxml import etree
import os
import sys
from webob import Response
@ -93,6 +95,53 @@ def get_normalized_request_content(model, req):
code=415)
def detect_credential_type(req):
"""Return the credential type name by detecting them in json/xml body"""
if req.content_type == "application/xml":
dom = etree.Element("root")
dom.append(etree.fromstring(req.body))
root = dom.find("{http://docs.openstack.org/identity/api/v2.0}"
"auth")
if root is None:
# Try legacy without wrapper
creds = dom.find("*")
if creds:
logger.warning("Received old syntax credentials not wrapped in"
"'auth'")
else:
creds = root.find("*")
if creds is None:
raise fault.BadRequestFault("Request is missing credentials")
name = creds.tag
if "}" in name:
#trim away namespace if it is there
name = name[name.rfind("}") + 1:]
return name
elif req.content_type == "application/json":
obj = json.loads(req.body)
if len(obj) == 0:
raise fault.BadRequestFault("Expecting 'auth'")
tag = obj.keys()[0]
if tag == "auth":
if len(obj[tag]) == 0:
raise fault.BadRequestFault("Expecting Credentials")
for key, value in obj[tag].iteritems():
if key not in ['tenantId', 'tenantName']:
return key
raise fault.BadRequestFault("Credentials missing from request")
else:
credentials_type = tag
return credentials_type
else:
logging.debug("Unsupported content-type passed: %s" % req.content_type)
raise fault.IdentityFault("I don't understand the content type",
code=415)
def send_error(code, req, result):
content = None