Remove ID API and fixup config
This commit is contained in:
parent
488288b0a0
commit
1635905855
@ -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')
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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 []
|
|
@ -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)
|
|
@ -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
|
|
@ -32,16 +32,6 @@ allowed_rpc_exception_modules = billingstack.exceptions, billingstack.openstack.
|
|||||||
# Port the bind the API server to
|
# Port the bind the API server to
|
||||||
#api_port = 9001
|
#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
|
# Central service
|
||||||
#################################################
|
#################################################
|
||||||
@ -60,6 +50,43 @@ admin_token = rand0m
|
|||||||
#retry_interval = 10
|
#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
|
# Rating service
|
||||||
#################################################
|
#################################################
|
||||||
@ -78,23 +105,3 @@ admin_token = rand0m
|
|||||||
#max_retries = 10
|
#max_retries = 10
|
||||||
#retry_interval = 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
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user