add an ec2 extension

This commit is contained in:
termie 2012-01-16 16:31:44 -08:00
parent 2ed9759ff7
commit afd897f912
6 changed files with 264 additions and 9 deletions

View File

@ -35,6 +35,8 @@ driver = keystone.backends.kvs.KvsToken
[policy]
driver = keystone.backends.policy.SimpleMatch
[ec2]
driver = keystone.backends.kvs.KvsEc2
[filter:debug]
paste.filter_factory = keystone.wsgi:Debug.factory
@ -51,6 +53,8 @@ paste.filter_factory = keystone.middleware:JsonBodyMiddleware.factory
[filter:crud_extension]
paste.filter_factory = keystone.service:AdminCrudExtension.factory
[filter:ec2_extension]
paste.filter_factory = keystone.service:Ec2Extension.factory
[app:public_service]
paste.app_factory = keystone.service:public_app_factory
@ -59,7 +63,7 @@ paste.app_factory = keystone.service:public_app_factory
paste.app_factory = keystone.service:admin_app_factory
[pipeline:public_api]
pipeline = token_auth admin_token_auth json_body debug public_service
pipeline = token_auth admin_token_auth json_body debug ec2_extension public_service
[pipeline:admin_api]
pipeline = token_auth admin_token_auth json_body debug crud_extension admin_service

View File

@ -74,7 +74,7 @@ class KvsIdentity(object):
role_ids = self.db.get('role_list', [])
return [self.get_role(x) for x in role_ids]
# These should probably be part of the high-level API
# These should probably be part of the high-level API
def add_user_to_tenant(self, tenant_id, user_id):
user_ref = self.get_user(user_id)
tenants = set(user_ref.get('tenants', []))
@ -265,3 +265,37 @@ class KvsPolicy(object):
def can_haz(self, target, action, credentials):
pass
class KvsEc2(object):
def __init__(self, db=None):
if db is None:
db = INMEMDB
elif type(db) is type({}):
db = DictKvs(db)
self.db = db
# Public interface
def get_credential(self, credential_id):
credential_ref = self.db.get('credential-%s' % credential_id)
return credential_ref
def list_credentials(self):
credential_ids = self.db.get('credential_list', [])
return [self.get_credential(x) for x in credential_ids]
# CRUD
def create_credential(self, credential_id, credential):
self.db.set('credential-%s' % credential_id, credential)
credential_list = set(self.db.get('credential_list', []))
credential_list.add(credential_id)
self.db.set('credential_list', list(credential_list))
return credential
def delete_credential(self, credential_id):
old_credential = self.db.get('credential-%s' % credential_id)
self.db.delete('credential-%s' % credential_id)
credential_list = set(self.db.get('credential_list', []))
credential_list.remove(credential_id)
self.db.set('credential_list', list(credential_list))
return None

View File

@ -123,3 +123,4 @@ register_str('driver', group='catalog')
register_str('driver', group='identity')
register_str('driver', group='policy')
register_str('driver', group='token')
register_str('driver', group='ec2')

12
keystone/ec2.py Normal file
View File

@ -0,0 +1,12 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
from keystone import config
from keystone import manager
CONF = config.CONF
class Manager(manager.Manager):
def __init__(self):
super(Manager, self).__init__(CONF.ec2.driver)

View File

@ -10,6 +10,7 @@ import webob.dec
import webob.exc
from keystone import catalog
from keystone import ec2
from keystone import identity
from keystone import logging
from keystone import policy
@ -151,10 +152,6 @@ class PublicRouter(wsgi.Router):
controller=auth_controller,
action='authenticate',
conditions=dict(method=['POST']))
mapper.connect('/ec2tokens',
controller=auth_controller,
action='authenticate_ec2',
conditions=dict(methods=['POST']))
# Tenant Operations
tenant_controller = TenantController()
@ -329,6 +326,135 @@ class AdminCrudExtension(wsgi.ExtensionRouter):
application, mapper)
class Ec2Extension(wsgi.ExtensionRouter):
def __init__(self, application):
mapper = routes.Mapper()
ec2_controller = Ec2Controller()
# validation
mapper.connect('/ec2tokens',
controller=ec2_controller,
action='authenticate_ec2',
conditions=dict(methods=['POST']))
# crud
mapper.connect('/users/{user_id}/credentials/OS-EC2',
controller=ec2_controller,
action='create_credential',
conditions=dict(methods=['POST']))
mapper.connect('/users/{user_id}/credentials/OS-EC2',
controller=ec2_controller,
action='get_credentials',
conditions=dict(methods=['GET']))
mapper.connect('/users/{user_id}/credentials/OS-EC2/{credential_id}',
controller=ec2_controller,
action='get_credential',
conditions=dict(methods=['GET']))
mapper.connect('/users/{user_id}/credentials/OS-EC2/{credential_id}',
controller=ec2_controller,
action='delete_credential',
conditions=dict(methods=['DELETE']))
super(Ec2Extension, self).__init__(application, mapper)
class Ec2Controller(Application):
def __init__(self):
self.catalog_api = catalog.Manager()
self.identity_api = identity.Manager()
self.token_api = token.Manager()
self.policy_api = policy.Manager()
self.ec2_api = ec2.Manager()
super(Ec2Controller, self).__init__()
def authenticate_ec2(self, context, credentials=None,
ec2Credentials=None):
"""Validate a signed EC2 request and provide a token."""
# NOTE(termie): backwards compat hack
if not ecredentials and ec2Credentials:
credentials = ec2Credentials
creds_ref = self.ec2_api.get_credential(context,
credentials['access'])
signer = utils.Signer(creds_ref['secret'])
signature = signer.generate(credentials)
if signature == credentials['signature']:
pass
# NOTE(vish): Some libraries don't use the port when signing
# requests, so try again without port.
elif ':' in credentials['signature']:
hostname, _port = credentials['host'].split(":")
credentials['host'] = hostname
signature = signer.generate(credentials)
if signature != credentials.signature:
# TODO(termie): proper exception
raise Exception("Not Authorized")
else:
raise Exception("Not Authorized")
# TODO(termie): don't create new tokens every time
# TODO(termie): this is copied from TokenController.authenticate
token_id = uuid.uuid4().hex
tenant_ref = self.identity_api.get_tenant(creds_ref['tenant_id'])
user_ref = self.identity_api.get_user(creds_ref['user_id'])
metadata_ref = self.identity_api.get_metadata(
context=context,
user_id=user_ref['id'],
tenant_id=tenant_ref['id'])
catalog_ref = self.catalog_api.get_catalog(
context=context,
user_id=user_ref['id'],
tenant_id=tenant_ref['id'],
metadata=metadata_ref)
token_ref = self.token_api.create_token(
context, token_id, dict(expires='',
id=token_id,
user=user_ref,
tenant=tenant_ref,
metadata=metadata_ref))
# TODO(termie): optimize this call at some point and put it into the
# the return for metadata
# fill out the roles in the metadata
roles_ref = []
for role_id in metadata_ref.get('roles', []):
roles_ref.append(self.identity_api.get_role(context, role_id))
# TODO(termie): make this a util function or something
# TODO(termie): i don't think the ec2 middleware currently expects a
# full return, but it contains a note saying that it
# would be better to expect a full return
return TokenController._format_authenticate(
self, token_ref, roles_ref, catalog_ref)
def create_credential(self, context, user_id, tenant_id):
# TODO(termie): validate that this request is valid for given user
# tenant
cred_ref = {'user_id': user_id,
'tenant_id': tenant_id,
'id': uuid.uuid4().hex,
'secret': uuid.uuid4().hex}
self.ec2_api.create_credential(context, cred_ref['id'], cred_ref)
return cred_ref
def get_credentials(self, context, user_id):
"""List credentials for the given user_id."""
# TODO(termie): validate that this request is valid for given user
# tenant
return self.ec2_api.list_credentials(user_id)
def get_credential(self, context, user_id, credential_id):
# TODO(termie): validate that this request is valid for given user
# tenant
return self.ec2_api.get_credential(credential_id)
def delete_credential(self, context, user_id, credential_id):
# TODO(termie): validate that this request is valid for given user
# tenant
return self.ec2_api.delete_credential(credential_id)
class NoopController(Application):
def __init__(self):
super(NoopController, self).__init__()
@ -462,9 +588,6 @@ class TokenController(Application):
logging.debug('TOKEN_REF %s', token_ref)
return self._format_authenticate(token_ref, roles_ref, catalog_ref)
def authenticate_ec2(self, context):
raise NotImplemented()
# admin only
def validate_token(self, context, token_id, belongs_to=None):
"""Check that a token is valid.

View File

@ -17,9 +17,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import base64
import hashlib
import hmac
import json
import subprocess
import sys
import urllib
from keystone import logging
@ -53,6 +57,83 @@ class SmarterEncoder(json.JSONEncoder):
return super(SmarterEncoder, self).default(obj)
class Ec2Signer(object):
"""Hacked up code from boto/connection.py"""
def __init__(self, secret_key):
secret_key = secret_key.encode()
self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1)
if hashlib.sha256:
self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256)
def generate(self, credentials):
"""Generate auth string according to what SignatureVersion is given."""
if credentials.params['SignatureVersion'] == '0':
return self._calc_signature_0(credentials.params)
if credentials.params['SignatureVersion'] == '1':
return self._calc_signature_1(credentials.params)
if credentials.params['SignatureVersion'] == '2':
return self._calc_signature_2(credentials.params,
credentials.verb,
credentials.host,
credentials.path)
raise Exception('Unknown Signature Version: %s' %
credentials.params['SignatureVersion'])
@staticmethod
def _get_utf8_value(value):
"""Get the UTF8-encoded version of a value."""
if not isinstance(value, str) and not isinstance(value, unicode):
value = str(value)
if isinstance(value, unicode):
return value.encode('utf-8')
else:
return value
def _calc_signature_0(self, params):
"""Generate AWS signature version 0 string."""
s = params['Action'] + params['Timestamp']
self.hmac.update(s)
return base64.b64encode(self.hmac.digest())
def _calc_signature_1(self, params):
"""Generate AWS signature version 1 string."""
keys = params.keys()
keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
for key in keys:
self.hmac.update(key)
val = self._get_utf8_value(params[key])
self.hmac.update(val)
return base64.b64encode(self.hmac.digest())
def _calc_signature_2(self, params, verb, server_string, path):
"""Generate AWS signature version 2 string."""
LOG.debug('using _calc_signature_2')
string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path)
if self.hmac_256:
current_hmac = self.hmac_256
params['SignatureMethod'] = 'HmacSHA256'
else:
current_hmac = self.hmac
params['SignatureMethod'] = 'HmacSHA1'
keys = params.keys()
keys.sort()
pairs = []
for key in keys:
val = self._get_utf8_value(params[key])
val = urllib.quote(val, safe='-_~')
pairs.append(urllib.quote(key, safe='') + '=' + val)
qs = '&'.join(pairs)
LOG.debug('query string: %s', qs)
string_to_sign += qs
LOG.debug('string_to_sign: %s', string_to_sign)
current_hmac.update(string_to_sign)
b64 = base64.b64encode(current_hmac.digest())
LOG.debug('len(b64)=%d', len(b64))
LOG.debug('base64 encoded digest: %s', b64)
return b64
# From python 2.7
def check_output(*popenargs, **kwargs):
r"""Run command with arguments and return its output as a byte string.