diff --git a/billingstack/identity/__init__.py b/billingstack/identity/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/billingstack/identity/api/__init__.py b/billingstack/identity/api/__init__.py deleted file mode 100644 index 8a54949..0000000 --- a/billingstack/identity/api/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright © 2013 Woorea Solutions, S.L -# -# Author: Luis Gervaso -# -# 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') diff --git a/billingstack/identity/api/app.py b/billingstack/identity/api/app.py deleted file mode 100644 index 837197f..0000000 --- a/billingstack/identity/api/app.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright © 2012 Woorea Solutions, S.L -# -# Author: Luis Gervaso -# -# 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 diff --git a/billingstack/identity/api/config.py b/billingstack/identity/api/config.py deleted file mode 100644 index c2b9e52..0000000 --- a/billingstack/identity/api/config.py +++ /dev/null @@ -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 diff --git a/billingstack/identity/api/hooks.py b/billingstack/identity/api/hooks.py deleted file mode 100644 index b9a0037..0000000 --- a/billingstack/identity/api/hooks.py +++ /dev/null @@ -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() diff --git a/billingstack/identity/api/v1.py b/billingstack/identity/api/v1.py deleted file mode 100644 index 2749bf9..0000000 --- a/billingstack/identity/api/v1.py +++ /dev/null @@ -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() diff --git a/billingstack/identity/base.py b/billingstack/identity/base.py deleted file mode 100644 index ddfa906..0000000 --- a/billingstack/identity/base.py +++ /dev/null @@ -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 diff --git a/billingstack/identity/cms.py b/billingstack/identity/cms.py deleted file mode 100644 index 071a902..0000000 --- a/billingstack/identity/cms.py +++ /dev/null @@ -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 diff --git a/billingstack/identity/impl_sqlalchemy.py b/billingstack/identity/impl_sqlalchemy.py deleted file mode 100644 index 8a98b15..0000000 --- a/billingstack/identity/impl_sqlalchemy.py +++ /dev/null @@ -1,216 +0,0 @@ -# Author: Endre Karlson -# -# 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) diff --git a/billingstack/identity/token_base.py b/billingstack/identity/token_base.py deleted file mode 100644 index c2c0ff1..0000000 --- a/billingstack/identity/token_base.py +++ /dev/null @@ -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 diff --git a/billingstack/identity/token_memcache.py b/billingstack/identity/token_memcache.py deleted file mode 100644 index e246b96..0000000 --- a/billingstack/identity/token_memcache.py +++ /dev/null @@ -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 [] diff --git a/billingstack/identity/utils.py b/billingstack/identity/utils.py deleted file mode 100644 index fd7edb8..0000000 --- a/billingstack/identity/utils.py +++ /dev/null @@ -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) diff --git a/billingstack/tests/identity/__init__.py b/billingstack/tests/identity/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/billingstack/tests/identity/test_api.py b/billingstack/tests/identity/test_api.py deleted file mode 100644 index 2805ff1..0000000 --- a/billingstack/tests/identity/test_api.py +++ /dev/null @@ -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 diff --git a/etc/billingstack/billingstack.conf.sample b/etc/billingstack/billingstack.conf.sample index 3de6c48..a7a3978 100644 --- a/etc/billingstack/billingstack.conf.sample +++ b/etc/billingstack/billingstack.conf.sample @@ -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 - -