Tokens and a Memcache plugin

This commit is contained in:
Endre Karlson 2013-03-12 14:28:47 +00:00
parent 2fb1356be9
commit f474fe3ead
6 changed files with 403 additions and 3 deletions

View File

@ -0,0 +1,174 @@
import hashlib
from billingstack.openstack.common import log
subprocess = None
LOG = log.getLogger(__name__)
PKI_ANS1_PREFIX = 'MII'
def _ensure_subprocess():
# NOTE(vish): late loading subprocess so we can
# use the green version if we are in
# eventlet.
global subprocess
if not subprocess:
try:
from eventlet import patcher
if patcher.already_patched.get('os'):
from eventlet.green import subprocess
else:
import subprocess
except ImportError:
import subprocess
def cms_verify(formatted, signing_cert_file_name, ca_file_name):
"""
verifies the signature of the contents IAW CMS syntax
"""
_ensure_subprocess()
process = subprocess.Popen(["openssl", "cms", "-verify",
"-certfile", signing_cert_file_name,
"-CAfile", ca_file_name,
"-inform", "PEM",
"-nosmimecap", "-nodetach",
"-nocerts", "-noattr"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = process.communicate(formatted)
retcode = process.poll()
if retcode:
LOG.error(_('Verify error: %s') % err)
raise subprocess.CalledProcessError(retcode, "openssl", output=err)
return output
def token_to_cms(signed_text):
copy_of_text = signed_text.replace('-', '/')
formatted = "-----BEGIN CMS-----\n"
line_length = 64
while len(copy_of_text) > 0:
if (len(copy_of_text) > line_length):
formatted += copy_of_text[:line_length]
copy_of_text = copy_of_text[line_length:]
else:
formatted += copy_of_text
copy_of_text = ""
formatted += "\n"
formatted += "-----END CMS-----\n"
return formatted
def verify_token(token, signing_cert_file_name, ca_file_name):
return cms_verify(token_to_cms(token),
signing_cert_file_name,
ca_file_name)
def is_ans1_token(token):
'''
thx to ayoung for sorting this out.
base64 decoded hex representation of MII is 3082
In [3]: binascii.hexlify(base64.b64decode('MII='))
Out[3]: '3082'
re: http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
pg4: For tags from 0 to 30 the first octet is the identfier
pg10: Hex 30 means sequence, followed by the length of that sequence.
pg5: Second octet is the length octet
first bit indicates short or long form, next 7 bits encode the number
of subsequent octets that make up the content length octets as an
unsigned binary int
82 = 10000010 (first bit indicates long form)
0000010 = 2 octets of content length
so read the next 2 octets to get the length of the content.
In the case of a very large content length there could be a requirement to
have more than 2 octets to designate the content length, therefore
requiring us to check for MIM, MIQ, etc.
In [4]: base64.b64encode(binascii.a2b_hex('3083'))
Out[4]: 'MIM='
In [5]: base64.b64encode(binascii.a2b_hex('3084'))
Out[5]: 'MIQ='
Checking for MI would become invalid at 16 octets of content length
10010000 = 90
In [6]: base64.b64encode(binascii.a2b_hex('3090'))
Out[6]: 'MJA='
Checking for just M is insufficient
But we will only check for MII:
Max length of the content using 2 octets is 7FFF or 32767
It's not practical to support a token of this length or greater in http
therefore, we will check for MII only and ignore the case of larger tokens
'''
return token[:3] == PKI_ANS1_PREFIX
def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
""" Uses OpenSSL to sign a document
Produces a Base64 encoding of a DER formatted CMS Document
http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax
"""
_ensure_subprocess()
process = subprocess.Popen(["openssl", "cms", "-sign",
"-signer", signing_cert_file_name,
"-inkey", signing_key_file_name,
"-outform", "PEM",
"-nosmimecap", "-nodetach",
"-nocerts", "-noattr"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = process.communicate(text)
retcode = process.poll()
if retcode or "Error" in err:
if retcode == 3:
LOG.error(_("Signing error: Unable to load certificate - "
"ensure you've configured PKI with "
"'keystone-manage pki_setup'"))
else:
LOG.error(_('Signing error: %s') % err)
raise subprocess.CalledProcessError(retcode, "openssl")
return output
def cms_sign_token(text, signing_cert_file_name, signing_key_file_name):
output = cms_sign_text(text, signing_cert_file_name, signing_key_file_name)
return cms_to_token(output)
def cms_to_token(cms_text):
start_delim = "-----BEGIN CMS-----"
end_delim = "-----END CMS-----"
signed_text = cms_text
signed_text = signed_text.replace('/', '-')
signed_text = signed_text.replace(start_delim, '')
signed_text = signed_text.replace(end_delim, '')
signed_text = signed_text.replace('\n', '')
return signed_text
def cms_hash_token(token_id):
"""
return: for ans1_token, returns the hash of the passed in token
otherwise, returns what it was passed in.
"""
if token_id is None:
return None
if is_ans1_token(token_id):
hasher = hashlib.md5()
hasher.update(token_id)
return hasher.hexdigest()
else:
return token_id

View File

@ -0,0 +1,84 @@
import copy
import datetime
from oslo.config import cfg
from billingstack import utils
from billingstack.identity import cms
from billingstack.openstack.common import timeutils
from billingstack.plugin import Plugin
cfg.CONF.register_group(
cfg.OptGroup(name='identity:token', title="Token configuration"))
cfg.CONF.register_opts([
cfg.IntOpt('expiration', default=86400)],
group='identity:token')
def unique_id(token_id):
"""Return a unique ID for a token.
The returned value is useful as the primary key of a database table,
memcache store, or other lookup table.
:returns: Given a PKI token, returns it's hashed value. Otherwise, returns
the passed-in value (such as a UUID token ID or an existing
hash).
"""
return cms.cms_hash_token(token_id)
def default_expire_time():
"""Determine when a fresh token should expire.
Expiration time varies based on configuration (see ``[token] expiration``).
:returns: a naive UTC datetime.datetime object
"""
expiration = cfg.CONF['identity:token'].expiration
expire_delta = datetime.timedelta(seconds=expiration)
return timeutils.utcnow() + expire_delta
class TokenPlugin(Plugin):
__plugin_ns__ = 'billingstack.token'
__plugin_type__ = 'token'
"""
Base for Token providers like Memcache, SQL, Redis.....
Note: This is NOT responsable for user / password authentication. It's a
layer that manages tokens....
"""
def get_token(self, token_id):
"""
Get a Token
:param token_id: Token ID to get...
"""
raise NotImplementedError
def delete_token(self, token_id):
"""
Delete a Token
:param token_id: Token ID to delete.
"""
raise NotImplementedError
def list_tokens(self):
"""
List tokens
"""
def list_revoked(self):
"""
List out revoked Tokens.
"""
raise NotImplementedError

View File

@ -0,0 +1,126 @@
import copy
import memcache
from oslo.config import cfg
from billingstack.identity.token_base import TokenPlugin
from billingstack.identity.token_base import default_expire_time, unique_id
from billingstack.openstack.common import jsonutils
from billingstack import utils
cfg.CONF.register_group(
cfg.OptGroup(name='token:memcache', title="Memcache"))
cfg.CONF.register_opts([
cfg.StrOpt('memcache_servers', default='127.0.0.1:11211')],
group='token:memcache')
class MemcachePlugin(TokenPlugin):
__plugin_name__ = 'memcache'
def __init__(self, client=None):
super(MemcachePlugin, self).__init__()
self._memcache_client = client
@property
def client(self):
return self._memcache_client or self._get_memcache_client()
def _get_memcache_client(self):
servers = cfg.CONF[self.name].memcache_servers.split(';')
self._memcache_client = memcache.Client(servers, debug=0)
return self._memcache_client
def _prefix_token_id(self, token_id):
return 'token-%s' % token_id.encode('utf-8')
def _prefix_user_id(self, user_id):
return 'usertokens-%s' % user_id.encode('utf-8')
def get_token(self, token_id):
if token_id is None:
#FIXME(ekarlso): Better error here?
raise exceptions.NotFound
ptk = self._prefix_token_id(token_id)
token = self.client.get(ptk)
if token is None:
#FIXME(ekarlso): Better error here?
raise exceptions.NotFound
return token
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
ptk = self._prefix_token_id(unique_id(token_id))
if not data_copy.get('expires'):
data_copy['expires'] = default_expire_time()
kwargs = {}
if data_copy['expires'] is not None:
expires_ts = utils.unixtime(data_copy['expires'])
kwargs['time'] = expires_ts
self.client.set(ptk, data_copy, **kwargs)
if 'id' in data['user']:
token_data = jsonutils.dumps(token_id)
user_id = data['user']['id']
user_key = self._prefix_user_id(user_id)
if not self.client.append(user_key, ',%s' % token_data):
if not self.client.add(user_key, token_data):
if not self.client.append(user_key, ',%s' % token_data):
msg = _('Unable to add token user list.')
raise exceptions.UnexpectedError(msg)
return copy.deepcopy(data_copy)
def _add_to_revocation_list(self, data):
data_json = jsonutils.dumps(data)
if not self.client.append(self.revocation_key, ',%s' % data_json):
if not self.client.add(self.revocation_key, data_json):
if not self.client.append(self.revocation_key,
',%s' % data_json):
msg = _('Unable to add token to revocation list.')
raise exceptions.UnexpectedError(msg)
def delete_token(self, token_id):
# Test for existence
data = self.get_token(unique_id(token_id))
ptk = self._prefix_token_id(unique_id(token_id))
result = self.client.delete(ptk)
self._add_to_revocation_list(data)
return result
def list_tokens(self, user_id, account_id=None, trust_id=None):
tokens = []
user_key = self._prefix_user_id(user_id)
user_record = self.client.get(user_key) or ""
token_list = jsonutils.loads('[%s]' % user_record)
for token_id in token_list:
ptk = self._prefix_token_id(token_id)
token_ref = self.client.get(ptk)
if token_ref:
if account_id is not None:
account = token_ref.get('account')
if not account:
continue
if account.get('id') != account_id:
continue
tokens.append(token_id)
return tokens
def list_revoked_tokens(self):
list_json = self.client.get(self.revocation_key)
if list_json:
return jsonutils.loads('[%s]' % list_json)
return []

View File

@ -5,8 +5,10 @@ import string
from billingstack import exceptions
cfg.CONF.register_opt(
cfg.IntOpt('crypt_strength', default=40000))
cfg.CONF.register_opts([
cfg.IntOpt('crypt_strength', default=40000)],
group='service:identity_api')
MAX_PASSWORD_LENGTH = 4096

View File

@ -1,9 +1,11 @@
import os
import pycountry
import re
import time
from oslo.config import cfg
from billingstack import exceptions
from oslo.config import cfg
from billingstack.openstack.common import log
@ -121,3 +123,12 @@ def get_columns(data):
map(lambda item: map(_seen, item.keys()), data)
return list(columns)
def unixtime(dt_obj):
"""Format datetime object as unix timestamp
:param dt_obj: datetime.datetime object
:returns: float
"""
return time.mktime(dt_obj.utctimetuple())

View File

@ -67,6 +67,9 @@ setup(
[billingstack.identity_plugin]
sqlalchemy = billingstack.identity.impl_sqlalchemy:SQLAlchemyPlugin
[billingstack.token_plugin]
memcache = billingstack.identity.token_memcache:MemcachePlugin
"""),
classifiers=[
'Development Status :: 3 - Alpha',