Tokens and a Memcache plugin
This commit is contained in:
parent
2fb1356be9
commit
f474fe3ead
174
billingstack/identity/cms.py
Normal file
174
billingstack/identity/cms.py
Normal 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
|
84
billingstack/identity/token_base.py
Normal file
84
billingstack/identity/token_base.py
Normal 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
|
||||
|
||||
|
126
billingstack/identity/token_memcache.py
Normal file
126
billingstack/identity/token_memcache.py
Normal 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 []
|
@ -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
|
||||
|
@ -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())
|
Loading…
x
Reference in New Issue
Block a user