Remove ID API and fixup config

This commit is contained in:
Endre Karlson 2013-04-23 23:33:57 -07:00
parent 488288b0a0
commit 1635905855
15 changed files with 37 additions and 1478 deletions

View File

@ -1,28 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Woorea Solutions, S.L
#
# Author: Luis Gervaso <luis@woorea.es>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo.config import cfg
API_SERVICE_OPTS = [
cfg.IntOpt('api_port', default=9092,
help='The port for the BS Identity API server'),
cfg.IntOpt('api_listen', default='0.0.0.0', help='Bind to address'),
cfg.StrOpt('storage_driver', default='sqlalchemy',
help='Storage driver to use'),
]
cfg.CONF.register_opts(API_SERVICE_OPTS, 'service:identity_api')

View File

@ -1,61 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 Woorea Solutions, S.L
#
# Author: Luis Gervaso <luis@woorea.es>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import configuration
from pecan import make_app
from billingstack.api.hooks import ConfigHook, NoAuthHook
from billingstack.identity.api import config as api_config
from billingstack.identity.api.hooks import DBHook
def get_pecan_config():
# Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py')
return configuration.conf_from_file(filename)
def setup_app(pecan_config=None, extra_hooks=None):
app_hooks = [ConfigHook(), DBHook()]
if extra_hooks:
app_hooks.extend(extra_hooks)
if not pecan_config:
pecan_config = get_pecan_config()
app_hooks.append(NoAuthHook())
configuration.set_config(dict(pecan_config), overwrite=True)
app = make_app(
pecan_config.app.root,
static_root=pecan_config.app.static_root,
template_path=pecan_config.app.template_path,
logging=getattr(pecan_config, 'logging', {}),
debug=getattr(pecan_config.app, 'debug', False),
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
hooks=app_hooks,
guess_content_type_from_ext=getattr(
pecan_config.app,
'guess_content_type_from_ext',
True),
)
return app

View File

@ -1,43 +0,0 @@
# Server Specific Configurations
server = {
'port': '9001',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': 'billingstack.identity.api.v1.RootController',
'modules': ['billingstack.identity.api'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/templates',
'debug': False,
'enable_acl': True,
}
logging = {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'billingstack': {'level': 'DEBUG', 'handlers': ['console']},
'wsme': {'level': 'DEBUG', 'handlers': ['console']}
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
},
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@ -1,11 +0,0 @@
from pecan import hooks
from oslo.config import cfg
from billingstack.identity.base import IdentityPlugin
class DBHook(hooks.PecanHook):
def before(self, state):
plugin = IdentityPlugin.get_plugin(
cfg.CONF['service:identity_api'].storage_driver)
state.request.storage_conn = plugin()

View File

@ -1,216 +0,0 @@
from pecan import request, expose, rest
import wsmeext.pecan as wsme_pecan
from wsme.types import text, wsattr
from billingstack.api.base import ModelBase, RestBase
class LoginCredentials(ModelBase):
name = wsattr(text, mandatory=True)
password = text
merchant = text
class LoginResponse(ModelBase):
"""
The response of the login
"""
token = text
class User(ModelBase):
def __init__(self, **kw):
#kw['contact_info'] = ContactInfo(**kw.get('contact_info', {}))
super(User, self).__init__(**kw)
id = text
name = text
password = text
@classmethod
def from_db(cls, values):
"""
Remove the password and anything else that's private.
"""
del values['password']
return cls(**values)
class Account(ModelBase):
id = text
name = text
type = text
class Role(ModelBase):
id = text
name = text
type = text
class UserController(RestBase):
"""User controller"""
__id__ = 'user'
@wsme_pecan.wsexpose(User)
def get_all(self):
row = request.storage_conn.get_user(request.ctxt, self.id_)
return User.from_db(row)
@wsme_pecan.wsexpose(User, body=User)
def put(self, body):
row = request.storage_conn.update_user(
request.ctxt,
self.id_,
body.to_db())
return User.from_db(row)
@wsme_pecan.wsexpose()
def delete(self):
request.storage_conn.delete_user(request.ctxt, self.id_)
class UsersController(RestBase):
"""Users controller"""
__resource__ = UserController
@wsme_pecan.wsexpose([User])
def get_all(self):
criterion = {}
rows = request.storage_conn.list_users(
request.ctxt,
criterion=criterion)
return [User.from_db(r) for r in rows]
@wsme_pecan.wsexpose(User, body=User)
def post(self, body):
row = request.storage_conn.create_user(
request.ctxt,
body.to_db())
return User.from_db(row)
class AccountRolesController(rest.RestController):
def __init__(self, account_id, user_id, role_id):
self.account_id = account_id
self.user_id = user_id
self.role_id = role_id
@wsme_pecan.wsexpose()
def put(self):
return request.storage_conn.create_grant(request.ctxt, self.user_id,
self.account_id, self.role_id)
@wsme_pecan.wsexpose()
def delete(self):
request.storage_conn.revoke_grant(request.ctxt, self.user_id,
self.account_id, self.role_id)
class AccountController(RestBase):
@expose()
def _lookup(self, *remainder):
if remainder[0] == 'users' and remainder[2] == 'roles':
return AccountRolesController(self.id_, remainder[1],
remainder[3]), ()
return super(AccountController, self)._lookup(remainder)
@wsme_pecan.wsexpose(Account)
def get_all(self):
row = request.storage_conn.get_account(request.ctxt, self.id_)
return Account.from_db(row)
@wsme_pecan.wsexpose(Account, body=Account)
def put(self, body):
row = request.storage_conn.update_account(
request.ctxt,
self.id_,
body.to_db())
return Account.from_db(row)
@wsme_pecan.wsexpose()
def delete(self):
request.storage_conn.delete_account(request.ctxt, self.id_)
class AccountsController(RestBase):
__resource__ = AccountController
@wsme_pecan.wsexpose([Account])
def get_all(self):
rows = request.storage_conn.list_accounts(request.ctxt)
return [Account.from_db(r) for r in rows]
@wsme_pecan.wsexpose(Account, body=Account)
def post(self, body):
row = request.storage_conn.create_account(
request.ctxt,
body.to_db())
return Account.from_db(row)
class RoleController(RestBase):
@wsme_pecan.wsexpose(Role, unicode)
def get_all(self):
row = request.storage_conn.get_role(request.ctxt, self.id_)
return Role.from_db(row)
@wsme_pecan.wsexpose(Role, body=Role)
def put(self, body):
row = request.storage_conn.update_role(
request.ctxt,
self.id_,
body.to_db())
return Role.from_db(row)
@wsme_pecan.wsexpose()
def delete(self):
request.storage_conn.delete_role(request.ctxt, self.id_)
class RolesController(RestBase):
__resource__ = RoleController
@wsme_pecan.wsexpose([Role])
def get_all(self):
rows = request.storage_conn.list_roles(request.ctxt,)
return [Role.from_db(r) for r in rows]
@wsme_pecan.wsexpose(Role, body=Role)
def post(self, body):
row = request.storage_conn.create_role(
request.ctxt,
body.to_db())
return Role.from_db(row)
class TokensController(RestBase):
"""
controller that authenticates a user...
"""
@wsme_pecan.wsexpose(LoginResponse, body=LoginCredentials)
def post(self, body):
data = {
'user_id': body.name,
'password': body.password}
auth_response = request.storage_conn.authenticate(request.ctxt, **data)
return LoginResponse(**auth_response)
class V1Controller(RestBase):
accounts = AccountsController()
roles = RolesController()
users = UsersController()
tokens = TokensController()
class RootController(RestBase):
v1 = V1Controller()

View File

@ -1,173 +0,0 @@
from oslo.config import cfg
from billingstack.plugin import Plugin
cfg.CONF.import_opt('storage_driver', 'billingstack.identity.api',
group='service:identity_api')
class IdentityPlugin(Plugin):
"""
A base IdentityPlugin
"""
__plugin_ns__ = 'billingstack.identity_plugin'
__plugin_type__ = 'identity'
@classmethod
def get_plugin(self, name=cfg.CONF['service:identity_api'].storage_driver,
**kw):
return super(IdentityPlugin, self).get_plugin(name, **kw)
def authenticate(self, context, user_id=None, password=None,
account_id=None):
"""
Authenticate a User
:param user_id: User ID
:param password: User Password
:param account_id: User ID
"""
raise NotImplementedError
def create_user(self, context, values):
"""
Create a User.
:param values: The values to create the User from.
"""
raise NotImplementedError
def list_users(self, context, criterion=None):
"""
List users.
:param criterion: Criterion to filter on.
"""
raise NotImplementedError
def get_user(self, context, id_):
"""
Get a User by ID.
:param id_: User id.
"""
raise NotImplementedError
def update_user(self, context, id, values):
"""
Update a User.
:param id_: User ID.
:param values: Values to update the User with.
"""
raise NotImplementedError
def delete_user(self, context, id_):
"""
Delete User.
:param id_: User ID to delete.
"""
raise NotImplementedError
def create_account(self, context, values):
"""
Create an Account.
:param values: Values to create Account from.
"""
raise NotImplementedError
def list_accounts(self, context, criterion=None):
"""
List Accounts.
:param criterion: Criterion to filter on.
"""
raise NotImplementedError
def get_account(self, context, id_):
"""
Get Account
:param id_: Account ID.
"""
raise NotImplementedError
def update_account(self, context, id_, values):
"""
Update Account.
:param id_: Account ID.
:param values: Account values.
"""
raise NotImplementedError
def delete_account(self, context, id_):
"""
Delete Account.
:param id_: Account ID
"""
raise NotImplementedError
def create_role(self, context, values):
"""
Create an Role.
:param values: Values to create Role from.
"""
raise NotImplementedError
def list_roles(self, context, criterion=None):
"""
List Accounts.
:param criterion: Criterion to filter on.
"""
raise NotImplementedError
def get_role(self, context, id_):
"""
Get Role.
:param id_: Role ID.
"""
raise NotImplementedError
def update_role(self, context, id_, values):
"""
Update Role.
:param id_: Role ID.
:param values: Role values.
"""
raise NotImplementedError
def delete_role(self, context, id_):
"""
Delete Role.
:param id_: Role ID
"""
raise NotImplementedError
def create_grant(self, context, user_id, account_id, role_id):
"""
Create a Grant
:param user_id: User ID.
:param account_id: Account ID.
:param role_id: Role ID.
"""
raise NotImplementedError
def remove_grant(self, context, user_id, account_id, role_id):
"""
Remove a Users Role grant on a Account
:param user_id: User ID.
:param account_id: Account ID.
:param role_id: Role ID.
"""
raise NotImplementedError

View File

@ -1,174 +0,0 @@
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

@ -1,216 +0,0 @@
# Author: Endre Karlson <endre.karlson@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A Identity plugin...
"""
from oslo.config import cfg
from sqlalchemy import Column, ForeignKey
from sqlalchemy import Unicode
from sqlalchemy.orm import exc
from sqlalchemy.ext.declarative import declarative_base
from billingstack import exceptions
from billingstack.openstack.common import log as logging
from billingstack.sqlalchemy.types import JSON, UUID
from billingstack.sqlalchemy import api, model_base, session
from billingstack.identity.base import IdentityPlugin
from billingstack.identity import utils as identity_utils
LOG = logging.getLogger(__name__)
# DB SCHEMA
BASE = declarative_base(cls=model_base.ModelBase)
cfg.CONF.register_group(cfg.OptGroup(
name='identity:sqlalchemy', title='Config for internal identity plugin'))
cfg.CONF.register_opts(session.SQLOPTS, group='identity:sqlalchemy')
class Role(BASE, model_base.BaseMixin):
name = Column(Unicode(64), unique=True, nullable=False)
extra = Column(JSON)
class UserAccountGrant(BASE):
user_id = Column(UUID, ForeignKey('user.id', ondelete='CASCADE',
onupdate='CASCADE'), primary_key=True)
account_id = Column(UUID, ForeignKey('account.id', ondelete='CASCADE',
onupdate='CASCADE'), primary_key=True)
data = Column(JSON)
class Account(BASE, model_base.BaseMixin):
type = Column(Unicode(10), nullable=False)
name = Column(Unicode(60), nullable=False)
title = Column(Unicode(100))
class User(BASE, model_base.BaseMixin):
"""
A User that can login.
"""
name = Column(Unicode(20), nullable=False)
password = Column(Unicode(255), nullable=False)
class SQLAlchemyPlugin(IdentityPlugin, api.HelpersMixin):
"""
A Internal IdentityPlugin that currently relies on SQLAlchemy as
the "Backend"
"""
def __init__(self):
self.setup('identity:sqlalchemy')
def base(self):
return BASE
def authenticate(self, context, user_id=None, password=None,
account_id=None):
#self._get_by_name(models.
pass
def create_user(self, context, values):
row = User(**values)
row.password = identity_utils.hash_password(row.password)
self._save(row)
return dict(row)
def list_users(self, context, criterion=None):
rows = self._list(User, criterion=criterion)
return map(dict, rows)
def get_user(self, context, id_):
row = self._get(User, id_)
return dict(row)
def update_user(self, context, id_, values):
row = self._update(User, id_, values)
return dict(row)
def delete_user(self, context, id_):
self._delete(User, id_)
def create_account(self, context, values):
row = Account(**values)
self._save(row)
return dict(row)
def list_accounts(self, context, criterion=None):
rows = self._list(Account, criterion=criterion)
return map(dict, rows)
def get_account(self, context, id_):
row = self._get(Account, id_)
return dict(row)
def update_account(self, context, id_, values):
row = self._update(Account, id_, values)
return dict(row)
def delete_account(self, context, id_):
self._delete(Account, id_)
def create_role(self, context, values):
row = Role(**values)
self._save(row)
return dict(row)
def list_roles(self, context, criterion=None):
rows = self._list(Role, criterion=criterion)
return map(dict, rows)
def get_role(self, context, id_):
row = self._get(Role, id_)
return dict(row)
def update_role(self, context, id_, values):
row = self._update(Role, id_, values)
return dict(row)
def delete_role(self, context, id_):
self._delete(Role, id_)
def get_metadata(self, user_id=None, account_id=None):
q = self.session.query(UserAccountGrant)\
.filter_by(user_id=user_id, account_id=account_id)
try:
return q.one().data
except exc.NoResultFound:
raise exceptions.NotFound
def create_metadata(self, user_id, account_id, metadata):
ref = UserAccountGrant(
account_id=account_id,
user_id=user_id,
data=metadata)
ref.save(self.session)
return metadata
def update_metadata(self, user_id, account_id, metadata):
q = self.session.query(UserAccountGrant)\
.filter_by(user_id=user_id, account_id=account_id)
ref = q.first()
data = ref.data.copy()
data.update(metadata)
ref.data = data
ref.save(self.session)
return ref
def create_grant(self, context, user_id, account_id, role_id):
self._get(Role, role_id)
try:
ref = self.get_metadata(user_id=user_id, account_id=account_id)
is_new = False
except exceptions.NotFound:
ref = {}
is_new = True
roles = set(ref.get('roles', []))
roles.add(role_id)
ref['roles'] = list(roles)
if is_new:
self.create_metadata(user_id, account_id, ref)
else:
self.update_metadata(user_id, account_id, ref)
def revoke_grant(self, context, user_id, account_id, role_id):
self._get(Role, role_id)
try:
ref = self.get_metadata(user_id=user_id, account_id=account_id)
is_new = False
except exceptions.NotFound:
ref = {}
is_new = True
roles = set(ref.get('roles', []))
try:
roles.remove(role_id)
except KeyError:
raise exceptions.NotFound(role_id=role_id)
ref['roles'] = list(roles)
if is_new:
self.create_metadata(user_id, account_id, ref)
else:
self.update_metadata(user_id, account_id, ref)

View File

@ -1,80 +0,0 @@
import datetime
from oslo.config import cfg
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

@ -1,128 +0,0 @@
import copy
import memcache
from oslo.config import cfg
from billingstack import exceptions
from billingstack.openstack.common.gettextutils import _
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

@ -1,53 +0,0 @@
import passlib.hash
from oslo.config import cfg
import random
import string
from billingstack import exceptions
cfg.CONF.register_opts([
cfg.IntOpt('crypt_strength', default=40000)],
group='service:identity_api')
MAX_PASSWORD_LENGTH = 4096
def generate_random_string(chars=7):
return u''.join(random.sample(string.ascii_letters * 2 + string.digits,
chars))
def trunc_password(password):
"""Truncate passwords to the MAX_PASSWORD_LENGTH."""
try:
if len(password) > MAX_PASSWORD_LENGTH:
return password[:MAX_PASSWORD_LENGTH]
else:
return password
except TypeError:
raise exceptions.ValidationError(attribute='string', target='password')
def hash_password(password):
"""Hash a password. Hard."""
password_utf8 = trunc_password(password).encode('utf-8')
if passlib.hash.sha512_crypt.identify(password_utf8):
return password_utf8
h = passlib.hash.sha512_crypt.encrypt(password_utf8,
rounds=cfg.CONF.crypt_strength)
return h
def check_password(password, hashed):
"""Check that a plaintext password matches hashed.
hashpw returns the salt value concatenated with the actual hash value.
It extracts the actual salt if this value is then passed as the salt.
"""
if password is None:
return False
password_utf8 = trunc_password(password).encode('utf-8')
return passlib.hash.sha512_crypt.verify(password_utf8, hashed)

View File

@ -1,265 +0,0 @@
import os
from pecan import set_config
from oslo.config import cfg
from billingstack.samples import get_samples
from billingstack.identity.base import IdentityPlugin
from billingstack.tests.base import BaseTestCase
cfg.CONF.import_opt(
'database_connection',
'billingstack.identity.impl_sqlalchemy',
group='identity:sqlalchemy')
ROLE = {
'name': 'Member'
}
# FIXME: Remove or keep
class IdentityAPITest(BaseTestCase):
"""
billingstack.api base test
"""
__test__ = False
PATH_PREFIX = '/v1'
def setUp(self):
super(IdentityAPITest, self).setUp()
self.samples = get_samples()
self.config(
storage_driver='sqlalchemy',
group='service:identity_api'
)
self.config(
database_connection='sqlite://',
group='identity:sqlalchemy')
self.plugin = IdentityPlugin.get_plugin(invoke_on_load=True)
self.plugin.setup_schema()
self.app = self.make_app()
def tearDown(self):
self.plugin.teardown_schema()
super(IdentityAPITest, self).tearDown()
set_config({}, overwrite=True)
def make_config(self, enable_acl=True):
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..',
'..',
)
)
return {
'app': {
'root': 'billingstack.identity.api.v1.RootController',
'modules': ['billingstack.identity.api'],
'static_root': '%s/public' % root_dir,
'template_path': '%s/api/templates' % root_dir,
'enable_acl': enable_acl,
},
'logging': {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'wsme': {'level': 'INFO', 'handlers': ['console']},
'billingstack': {'level': 'DEBUG',
'handlers': ['console'],
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
},
},
}
# Accounts
def test_create_account(self):
values = self.get_fixture('merchant')
values['type'] = 'merchant'
self.post('accounts', values)
def test_list_accounts(self):
resp = self.get('accounts')
self.assertLen(0, resp.json)
def test_get_account(self):
values = self.get_fixture('merchant')
values['type'] = 'merchant'
resp = self.post('accounts', values)
resp_actual = self.get('accounts/%s' % resp.json['id'])
self.assertData(resp.json, resp_actual.json)
def test_update_account(self):
values = self.get_fixture('merchant')
values['type'] = 'merchant'
resp = self.post('accounts', values)
expected = dict(resp.json, name='Merchant')
resp = self.put('accounts/%s' % expected['id'], expected)
self.assertData(expected, resp.json)
def test_delete_account(self):
values = self.get_fixture('merchant')
values['type'] = 'merchant'
resp = self.post('accounts', values)
self.delete('accounts/%s' % resp.json['id'])
resp = self.get('accounts')
self.assertLen(0, resp.json)
# Roles
def test_create_role(self):
values = ROLE.copy()
resp = self.post('roles', values)
assert resp.json['name'] == values['name']
assert resp.json['id'] is not None
def test_list_roles(self):
resp = self.get('roles')
self.assertLen(0, resp.json)
def test_get_role(self):
values = ROLE.copy()
resp = self.post('roles', values)
resp_actual = self.get('roles/%s' % resp.json['id'])
self.assertData(resp.json, resp_actual.json)
def test_update_role(self):
values = ROLE.copy()
resp = self.post('roles', values)
expected = dict(resp.json, name='SuperMember')
resp = self.put('roles/%s' % expected['id'], expected)
self.assertData(expected, resp.json)
def test_delete_role(self):
values = ROLE.copy()
resp = self.post('roles', values)
self.delete('roles/%s' % resp.json['id'])
resp = self.get('roles')
self.assertLen(0, resp.json)
def test_create_user(self):
values = self.get_fixture('user')
self.post('users', values)
def test_list_users(self):
resp = self.get('users')
self.assertLen(0, resp.json)
def test_get_user(self):
values = self.get_fixture('user')
resp = self.post('users', values)
resp_actual = self.get('users/%s' % resp.json['id'])
self.assertData(resp.json, resp_actual.json)
def test_update_user(self):
values = self.get_fixture('user')
resp = self.post('users', values)
expected = dict(resp.json, name='test')
resp = self.put('users/%s' % expected['id'], expected)
self.assertData(expected, resp.json)
def test_delete_user(self):
values = self.get_fixture('user')
resp = self.post('users', values)
self.delete('users/%s' % resp.json['id'])
resp = self.get('users')
self.assertLen(0, resp.json)
# Grants
def test_create_grant(self):
account_data = self.get_fixture('merchant')
account_data['type'] = 'merchant'
account = self.post('accounts', account_data).json
user_data = self.get_fixture('user')
user = self.post('users', user_data).json
role_data = ROLE.copy()
role = self.post('roles', role_data).json
url = 'accounts/%s/users/%s/roles/%s' % (
account['id'], user['id'], role['id'])
self.put(url, {})
def test_revoke_grant(self):
account_data = self.get_fixture('merchant')
account_data['type'] = 'merchant'
account = self.post('accounts', account_data).json
user_data = self.get_fixture('user')
user = self.post('users', user_data).json
role_data = ROLE.copy()
role = self.post('roles', role_data).json
url = 'accounts/%s/users/%s/roles/%s' % (
account['id'], user['id'], role['id'])
self.put(url, {})
self.delete(url)
def test_login(self):
user_data = self.get_fixture('user')
self.post('users', user_data).json
resp = self.post('tokens', user_data)
assert 'token' in resp.json

View File

@ -32,16 +32,6 @@ allowed_rpc_exception_modules = billingstack.exceptions, billingstack.openstack.
# Port the bind the API server to
#api_port = 9001
[service:identity_api]
# Address to bind the API server
# api_host = 0.0.0.0
# Port the bind the API server to
api_port = 9092
admin_token = rand0m
#################################################
# Central service
#################################################
@ -60,6 +50,43 @@ admin_token = rand0m
#retry_interval = 10
#################################################
# Biller service
#################################################
#-----------------------
# SQLAlchemy Storage
#-----------------------
[biller:sqlalchemy]
# Database connection string - to configure options for a given implementation
# like sqlalchemy or other see below
#database_connection = mysql://billingstack:billingstack@localhost:3306/billingstack
#connection_debug = 100
#connection_trace = False
#sqlite_synchronous = True
#idle_timeout = 3600
#max_retries = 10
#retry_interval = 10
#################################################
# Collector service
#################################################
#-----------------------
# SQLAlchemy Storage
#-----------------------
[collector:sqlalchemy]
# Database connection string - to configure options for a given implementation
# like sqlalchemy or other see below
#database_connection = mysql://billingstack:billingstack@localhost:3306/billingstack
#connection_debug = 100
#connection_trace = False
#sqlite_synchronous = True
#idle_timeout = 3600
#max_retries = 10
#retry_interval = 10
#################################################
# Rating service
#################################################
@ -78,23 +105,3 @@ admin_token = rand0m
#max_retries = 10
#retry_interval = 10
#################################################
# Identity service
#################################################
#-----------------------
# SQLAlchemy Storage
#-----------------------
[identity:sqlalchemy]
# Database connection string - to configure options for a given implementation
# like sqlalchemy or other see below
#database_connection = mysql://billingstack:billingstack@localhost:3306/billingstack
#connection_debug = 100
#connection_trace = False
#sqlite_synchronous = True
#idle_timeout = 3600
#max_retries = 10
#retry_interval = 10