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
|
||||
#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
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user