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:
parent
9e1e113901
commit
6362857541
|
@ -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))
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]))
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue