diff options
author | tsv <tsv@hp.com> | 2014-03-12 14:37:02 -0700 |
---|---|---|
committer | tsv <tsv@hp.com> | 2014-06-09 13:09:47 -0600 |
commit | 01d23227b0fa2b17038dfb3710e700b553778e15 (patch) | |
tree | 78c6f47e59873976ff8453147b6ec993cc0f947a | |
parent | f392e55383c001617995713a344c364d16a66e29 (diff) | |
download | python-barbicanclient-01d23227b0fa2b17038dfb3710e700b553778e15.tar.gz |
Add Keystone V3 compliant session/auth plugin support
Barbican client uses Keystone V2 client via a Barbican auth
plugin. It also uses a regular requests.Session(). This commit
adds support for keystone session and replaces the heavy
keystone client with a lighter V2/V3 Password auth plugin. This
change is backwards compatible; it supports existing callers
and keystone session/plugin support.
On the testing front, this commit also introduces httpretty along
with a keystoneclient fixture. The keystoneclient fixture could
eventually be called directly from keystoneclient module instead
of copying it over here. Testing the keystone session/plugin
support is done mainly using httpretty
Patches:
7: Refactored Client.__init__ for better readability
8: Fixing pep8/py33 errors w.r.t new tox.ini changes
DocImpact
bp/barbican-client-has-to-be-keystone-v3.0-complaint
Change-Id: I8ef178b0338fe430a64c30bfe193406aabf3caf1
-rw-r--r-- | barbicanclient/barbican.py | 62 | ||||
-rw-r--r-- | barbicanclient/client.py | 131 | ||||
-rw-r--r-- | barbicanclient/common/auth.py | 96 | ||||
-rw-r--r-- | barbicanclient/openstack/common/timeutils.py | 2 | ||||
-rw-r--r-- | barbicanclient/test/common/test_auth.py | 3 | ||||
-rw-r--r-- | barbicanclient/test/keystone_client_fixtures.py | 189 | ||||
-rw-r--r-- | barbicanclient/test/test_barbican.py | 170 | ||||
-rw-r--r-- | barbicanclient/test/test_client.py | 293 | ||||
-rw-r--r-- | requirements.txt | 3 | ||||
-rw-r--r-- | test-requirements.txt | 1 |
10 files changed, 869 insertions, 81 deletions
diff --git a/barbicanclient/barbican.py b/barbicanclient/barbican.py index ea2297f..8561d2a 100644 --- a/barbicanclient/barbican.py +++ b/barbicanclient/barbican.py @@ -25,6 +25,7 @@ from barbicanclient import client class Barbican: + def __init__(self): self.parser = self._get_main_parser() self.subparsers = self.parser.add_subparsers( @@ -59,10 +60,22 @@ class Barbican: metavar='<auth-user-name>', default=client.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME].') + parser.add_argument('--os-user-id', + metavar='<auth-user-id>', + default=client.env('OS_USER_ID'), + help='Defaults to env[OS_USER_ID].') parser.add_argument('--os-password', '-P', metavar='<auth-password>', default=client.env('OS_PASSWORD'), help='Defaults to env[OS_PASSWORD].') + parser.add_argument('--os-user-domain-id', + metavar='<auth-user-domain-id>', + default=client.env('OS_USER_DOMAIN_ID'), + help='Defaults to env[OS_USER_DOMAIN_ID].') + parser.add_argument('--os-user-domain-name', + metavar='<auth-user-domain-name>', + default=client.env('OS_USER_DOMAIN_NAME'), + help='Defaults to env[OS_USER_DOMAIN_NAME].') parser.add_argument('--os-tenant-name', '-T', metavar='<auth-tenant-name>', default=client.env('OS_TENANT_NAME'), @@ -71,6 +84,28 @@ class Barbican: metavar='<tenant-id>', default=client.env('OS_TENANT_ID'), help='Defaults to env[OS_TENANT_ID].') + parser.add_argument('--os-project-id', + metavar='<auth-project-id>', + default=client.env('OS_PROJECT__ID'), + help='Another way to specify tenant ID. ' + 'This option is mutually exclusive with ' + ' --os-tenant-id. ' + 'Defaults to env[OS_PROJECT_ID].') + parser.add_argument('--os-project-name', + metavar='<auth-project-name>', + default=client.env('OS_PROJECT_NAME'), + help='Another way to specify tenant name. ' + 'This option is mutually exclusive with ' + ' --os-tenant-name. ' + 'Defaults to env[OS_PROJECT_NAME].') + parser.add_argument('--os-project-domain-id', + metavar='<auth-project-domain-id>', + default=client.env('OS_PROJECT_DOMAIN_ID'), + help='Defaults to env[OS_PROJECT_DOMAIN_ID].') + parser.add_argument('--os-project-domain-name', + metavar='<auth-project-domain-name>', + default=client.env('OS_PROJECT_DOMAIN_NAME'), + help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') parser.add_argument('--endpoint', '-E', metavar='<barbican-url>', default=client.env('BARBICAN_ENDPOINT'), @@ -315,21 +350,24 @@ class Barbican: def execute(self, **kwargs): args = self.parser.parse_args(kwargs.get('argv')) if args.no_auth: + if not all([args.endpoint, args.os_tenant_id or + args.os_project_id]): + self.parser.exit( + status=1, + message='ERROR: please specify --endpoint and ' + '--os-project-id(or --os-tenant-id)\n') self.client = client.Client(endpoint=args.endpoint, - tenant_id=args.os_tenant_id, + tenant_id=args.os_tenant_id or + args.os_project_id, insecure=args.insecure) - elif all([args.os_auth_url, args.os_username, args.os_password, - args.os_tenant_name]): - self._keystone = auth.KeystoneAuthV2( - auth_url=args.os_auth_url, - username=args.os_username, - password=args.os_password, - tenant_name=args.os_tenant_name, - insecure=args.insecure - ) - self.client = client.Client(auth_plugin=self._keystone, + elif all([args.os_auth_url, args.os_user_id or args.os_username, + args.os_password, args.os_tenant_name or args.os_tenant_id or + args.os_project_name or args.os_project_id]): + ks_session = auth.create_keystone_auth_session(args) + self.client = client.Client(session=ks_session, endpoint=args.endpoint, - tenant_id=args.os_tenant_id, + tenant_id=args.os_tenant_id or + args.os_project_id, insecure=args.insecure) else: self.parser.exit( diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 2d7bc52..1115ed4 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -16,8 +16,11 @@ import json import logging import os -import requests +from keystoneclient.auth.base import BaseAuthPlugin +from keystoneclient import exceptions +from keystoneclient import session as ks_session +from barbicanclient.common.auth import KeystoneAuthPluginWrapper from barbicanclient.openstack.common.gettextutils import _ from barbicanclient import orders from barbicanclient import secrets @@ -28,22 +31,27 @@ LOG = logging.getLogger(__name__) class HTTPError(Exception): + """Base exception for HTTP errors.""" + def __init__(self, message): super(HTTPError, self).__init__(message) class HTTPServerError(HTTPError): + """Raised for 5xx responses from the server.""" pass class HTTPClientError(HTTPError): + """Raised for 4xx responses from the server.""" pass class HTTPAuthError(HTTPError): + """Raised for 401 Unauthorized responses from the server.""" pass @@ -51,49 +59,43 @@ class HTTPAuthError(HTTPError): class Client(object): def __init__(self, session=None, auth_plugin=None, endpoint=None, - tenant_id=None, insecure=False): + tenant_id=None, insecure=False, service_type='keystore', + interface='public'): """ Barbican client object used to interact with barbican service. + :param session: This can be either requests.Session or + keystoneclient.session.Session :param auth_plugin: Authentication backend plugin - defaults to None + defaults to None. This can also be a keystoneclient authentication + plugin. :param endpoint: Barbican endpoint url. Required when not using an auth_plugin. When not provided, the client will try to fetch this from the auth service catalog :param tenant_id: The tenant ID used for context in barbican. Required when not using auth_plugin. When not provided, the client will try to get this from the auth_plugin. + :param insecure: Explicitly allow barbicanclient to perform + "insecure" TLS (https) requests. The server's certificate + will not be verified against any certificate authorities. + This option should be used with caution. + :param service_type: Used as an endpoint filter when using a + keystone auth plugin. Defaults to 'keystore' + :param interface: Another endpoint filter. Defaults to 'public' """ - LOG.debug("Creating Client object") - - self._session = session or requests.Session() - self.verify = not insecure - self.auth_plugin = auth_plugin - - if self.auth_plugin is not None: - try: - self._barbican_url = self.auth_plugin.barbican_url - except: - if endpoint: - self._barbican_url = endpoint - else: - raise - - self._tenant_id = self.auth_plugin.tenant_id - self._session.headers.update( - {'X-Auth-Token': self.auth_plugin.auth_token} - ) + LOG.debug(_("Creating Client object")) + self._wrap_session_with_keystone_if_required(session, insecure) + auth_plugin = self._update_session_auth_plugin(auth_plugin) + + if auth_plugin: + self._barbican_url = self._session.get_endpoint( + service_type=service_type, interface=interface) + self._tenant_id = self._get_tenant_id(self._session, auth_plugin) else: - if endpoint is None: - raise ValueError('Barbican endpoint url must be provided, or ' - 'must be available from auth_plugin') - if tenant_id is None: - raise ValueError('Tenant ID must be provided, or must be' - ' available from the auth_plugin') - if endpoint.endswith('/'): - self._barbican_url = endpoint[:-1] - else: - self._barbican_url = endpoint + # neither auth_plugin is provided nor it is available from session + # fallback to passed in parameters + self._validate_endpoint_and_tenant_id(endpoint, tenant_id) + self._barbican_url = self._get_normalized_endpoint(endpoint) self._tenant_id = tenant_id self.base_url = '{0}/{1}'.format(self._barbican_url, self._tenant_id) @@ -101,27 +103,80 @@ class Client(object): self.orders = orders.OrderManager(self) self.verifications = verifications.VerificationManager(self) + def _wrap_session_with_keystone_if_required(self, session, insecure): + # if session is not a keystone session, wrap it + if not isinstance(session, ks_session.Session): + self._session = ks_session.Session( + session=session, verify=not insecure) + else: + self._session = session + + def _update_session_auth_plugin(self, auth_plugin): + # if auth_plugin is not provided and the session + # has one, use it + using_auth_from_session = False + if auth_plugin is None and self._session.auth is not None: + auth_plugin = self._session.auth + using_auth_from_session = True + + ks_auth_plugin = auth_plugin + # if auth_plugin is not a keystone plugin, wrap it + if auth_plugin and not isinstance(auth_plugin, BaseAuthPlugin): + ks_auth_plugin = KeystoneAuthPluginWrapper(auth_plugin) + + # if auth_plugin is provided, override the session's auth with it + if not using_auth_from_session: + self._session.auth = ks_auth_plugin + + return auth_plugin + + def _validate_endpoint_and_tenant_id(self, endpoint, tenant_id): + if endpoint is None: + raise ValueError('Barbican endpoint url must be provided, or ' + 'must be available from auth_plugin or ' + 'keystone_client') + if tenant_id is None: + raise ValueError('Tenant ID must be provided, or must be ' + 'available from the auth_plugin or ' + 'keystone-client') + + def _get_normalized_endpoint(self, endpoint): + if endpoint.endswith('/'): + endpoint = endpoint[:-1] + return endpoint + + def _get_tenant_id(self, session, auth_plugin): + if isinstance(auth_plugin, BaseAuthPlugin): + # this is a keystoneclient auth plugin + if hasattr(auth_plugin, 'get_access'): + return auth_plugin.get_access(session).project_id + else: + # not an identity auth plugin and we don't know how to lookup + # the tenant_id + raise ValueError('Unable to obtain tenant_id from auth plugin') + else: + # this is a Barbican auth plugin + return auth_plugin.tenant_id + def get(self, href, params=None): headers = {'Accept': 'application/json'} - resp = self._session.get(href, params=params, headers=headers, - verify=self.verify) + resp = self._session.get(href, params=params, headers=headers) self._check_status_code(resp) return resp.json() def get_raw(self, href, headers): - resp = self._session.get(href, headers=headers, verify=self.verify) + resp = self._session.get(href, headers=headers) self._check_status_code(resp) return resp.content def delete(self, href): - resp = self._session.delete(href, verify=self.verify) + resp = self._session.delete(href) self._check_status_code(resp) def post(self, path, data): url = '{0}/{1}/'.format(self.base_url, path) headers = {'content-type': 'application/json'} - resp = self._session.post(url, data=json.dumps(data), headers=headers, - verify=self.verify) + resp = self._session.post(url, data=json.dumps(data), headers=headers) self._check_status_code(resp) return resp.json() diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index d858101..2944f28 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -16,22 +16,112 @@ import abc import json import logging +from keystoneclient.auth.base import BaseAuthPlugin from keystoneclient.v2_0 import client as ksclient from keystoneclient import exceptions +from keystoneclient import session as ks_session +from keystoneclient import discover import requests import six LOG = logging.getLogger(__name__) +""" +This class is for backward compatibility only and is an +adapter for using barbican style auth_plugin in place of +the recommended keystone auth_plugin. +""" + + +class KeystoneAuthPluginWrapper(BaseAuthPlugin): + + def __init__(self, barbican_auth_plugin): + self.barbican_auth_plugin = barbican_auth_plugin + + def get_token(self, session, **kwargs): + return self.barbican_auth_plugin.auth_token + + def get_endpoint(self, session, **kwargs): + # NOTE(gyee): this is really a hack as Barbican auth plugin only + # cares about Barbican endpoint. + return self.barbican_auth_plugin.barbican_url + + +def _discover_keystone_info(auth_url): + # From the auth_url, figure the keystone client version to use + try: + disco = discover.Discover(auth_url=auth_url) + versions = disco.available_versions() + except: + error_msg = 'Error: failed to discover keystone version '\ + 'using auth_url: %s' % auth_url + raise ValueError(error_msg) + else: + # use the first one in the list + if len(versions) > 0: + version = versions[0]['id'] + else: + error_msg = 'Error: Unable to discover a keystone plugin '\ + 'for the specified --os-auth-url.\n'\ + 'Please provide a valid auth url' + raise ValueError(error_msg) + try: + # the input auth_url may not have the version info in the + # url. get the correct auth_url from the versions + auth_url = versions[0]['links'][0]['href'] + except: + raise ValueError('Error: Unable to discover the correct auth url') + return version, auth_url + + +def create_keystone_auth_session(args): + """ + Creates an authenticated keystone session using + the supplied arguments. + """ + version, auth_url = _discover_keystone_info(args.os_auth_url) + project_name = args.os_project_name or args.os_tenant_name + project_id = args.os_project_id or args.os_tenant_id + + # FIXME(tsv): we are depending on the keystone version interface here. + # If keystone changes it, this code will need to be changed accordingly + if version == 'v2.0': + # create a V2 Password plugin + from keystoneclient.auth.identity import v2 + auth_plugin = v2.Password(auth_url=auth_url, + username=args.os_username, + password=args.os_password, + tenant_name=project_name, + tenant_id=project_id) + elif version == 'v3.0': + # create a V3 Password plugin + from keystoneclient.auth.identity import v3 + auth_plugin = v3.Password(auth_url=auth_url, + username=args.os_username, + user_id=args.os_user_id, + user_domain_name=args.os_user_domain_name, + user_domain_id=args.os_user_domain_id, + password=args.os_password, + project_id=project_id, + project_name=project_name, + project_domain_id=args.os_project_domain_id, + project_domain_name=args. + os_project_domain_name) + else: + raise ValueError('Error: unsupported keystone version!') + return ks_session.Session(auth=auth_plugin, verify=not args.insecure) + class AuthException(Exception): + """Raised when authorization fails.""" pass @six.add_metaclass(abc.ABCMeta) class AuthPluginBase(object): + """Base class for Auth plugins.""" @abc.abstractproperty @@ -49,6 +139,7 @@ class AuthPluginBase(object): class KeystoneAuthV2(AuthPluginBase): + def __init__(self, auth_url='', username='', password='', tenant_name='', tenant_id='', insecure=False, keystone=None): if not keystone: @@ -58,7 +149,7 @@ class KeystoneAuthV2(AuthPluginBase): ' and tenant_id or tenant_name.') self._barbican_url = None - #TODO(dmend): make these configurable + # TODO(dmend): make these configurable self._service_type = 'keystore' self._endpoint_type = 'publicURL' @@ -95,6 +186,7 @@ class KeystoneAuthV2(AuthPluginBase): class RackspaceAuthV2(AuthPluginBase): + def __init__(self, auth_url='', username='', api_key='', password=''): if not all([auth_url, username, api_key or password]): raise ValueError('Please provide auth_url, username, api_key or ' @@ -141,7 +233,7 @@ class RackspaceAuthV2(AuthPluginBase): LOG.error(msg) raise AuthException(msg) else: - #TODO(dmend): get barbican_url from catalog + # TODO(dmend): get barbican_url from catalog self._auth_token = data['access']['token']['id'] self.tenant_id = data['access']['token']['tenant']['id'] diff --git a/barbicanclient/openstack/common/timeutils.py b/barbicanclient/openstack/common/timeutils.py index 52688a0..e4b8f1a 100644 --- a/barbicanclient/openstack/common/timeutils.py +++ b/barbicanclient/openstack/common/timeutils.py @@ -134,7 +134,7 @@ def set_time_override(override_time=None): def advance_time_delta(timedelta): """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) + assert(utcnow.override_time is not None) try: for dt in utcnow.override_time: dt += timedelta diff --git a/barbicanclient/test/common/test_auth.py b/barbicanclient/test/common/test_auth.py index d728aef..0a8890b 100644 --- a/barbicanclient/test/common/test_auth.py +++ b/barbicanclient/test/common/test_auth.py @@ -38,8 +38,7 @@ class WhenTestingKeystoneAuthentication(testtools.TestCase): password=self.password, tenant_name=self.tenant_name, tenant_id=self.tenant_id, - keystone= - self.keystone_client) + keystone=self.keystone_client) def test_endpoint_username_password_tenant_are_required(self): with testtools.ExpectedException(ValueError): diff --git a/barbicanclient/test/keystone_client_fixtures.py b/barbicanclient/test/keystone_client_fixtures.py new file mode 100644 index 0000000..328ca69 --- /dev/null +++ b/barbicanclient/test/keystone_client_fixtures.py @@ -0,0 +1,189 @@ +# 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. + +import copy +import uuid + +from barbicanclient.openstack.common import jsonutils + + +# these are copied from python-keystoneclient tests +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +UPDATED = '2013-03-06T00:00:00Z' + +V2_URL = "%sv2.0" % BASE_URL +V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' + 'openstack-identity-service/2.0/content/', + 'rel': 'describedby', + 'type': 'text/html'} +V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' + 'ity-service/2.0/identity-dev-guide-2.0.pdf', + 'rel': 'describedby', + 'type': 'application/pdf'} + +V2_VERSION = {'id': 'v2.0', + 'links': [{'href': V2_URL, 'rel': 'self'}, + V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], + 'status': 'stable', + 'updated': UPDATED} + +V3_URL = "%sv3" % BASE_URL +V3_MEDIA_TYPES = [{'base': 'application/json', + 'type': 'application/vnd.openstack.identity-v3+json'}, + {'base': 'application/xml', + 'type': 'application/vnd.openstack.identity-v3+xml'}] + +V3_VERSION = {'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED} + + +def _create_version_list(versions): + return jsonutils.dumps({'versions': {'values': versions}}) + + +def _create_single_version(version): + return jsonutils.dumps({'version': version}) + + +V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) +V2_VERSION_LIST = _create_version_list([V2_VERSION]) + +V3_VERSION_ENTRY = _create_single_version(V3_VERSION) +V2_VERSION_ENTRY = _create_single_version(V2_VERSION) + +BARBICAN_ENDPOINT = 'http://www.barbican.com/v1' + + +def _get_normalized_token_data(**kwargs): + ref = copy.deepcopy(kwargs) + # normalized token data + ref['user_id'] = ref.get('user_id', uuid.uuid4().hex) + ref['username'] = ref.get('username', uuid.uuid4().hex) + ref['project_id'] = ref.get('project_id', + ref.get('tenant_id', uuid.uuid4().hex)) + ref['project_name'] = ref.get('tenant_name', + ref.get('tenant_name', uuid.uuid4().hex)) + ref['user_domain_id'] = ref.get('user_domain_id', uuid.uuid4().hex) + ref['user_domain_name'] = ref.get('user_domain_name', uuid.uuid4().hex) + ref['project_domain_id'] = ref.get('project_domain_id', uuid.uuid4().hex) + ref['project_domain_name'] = ref.get('project_domain_name', + uuid.uuid4().hex) + ref['roles'] = ref.get('roles', [{'name': uuid.uuid4().hex, + 'id': uuid.uuid4().hex}]) + ref['roles_link'] = ref.get('roles_link', []) + ref['barbican_url'] = ref.get('barbican_url', BARBICAN_ENDPOINT) + + return ref + + +def generate_v2_project_scoped_token(**kwargs): + """Generate a Keystone V2 token based on auth request.""" + ref = _get_normalized_token_data(**kwargs) + + o = {'access': {'token': {'id': uuid.uuid4().hex, + 'expires': '2099-05-22T00:02:43.941430Z', + 'issued_at': '2013-05-21T00:02:43.941473Z', + 'tenant': {'enabled': True, + 'id': ref.get('project_id'), + 'name': ref.get('project_id') + } + }, + 'user': {'id': ref.get('user_id'), + 'name': uuid.uuid4().hex, + 'username': ref.get('username'), + 'roles': ref.get('roles'), + 'roles_links': ref.get('roles_links') + } + }} + + # we only care about Barbican and Keystone endpoints + o['access']['serviceCatalog'] = [ + {'endpoints': [ + {'publicURL': ref.get('barbican_url'), + 'id': uuid.uuid4().hex, + 'region': 'RegionOne' + }], + 'endpoints_links': [], + 'name': 'Barbican', + 'type': 'keystore'}, + {'endpoints': [ + {'publicURL': ref.get('auth_url'), + 'adminURL': ref.get('auth_url'), + 'id': uuid.uuid4().hex, + 'region': 'RegionOne' + }], + 'endpoint_links': [], + 'name': 'keystone', + 'type': 'identity'}] + + return o + + +def generate_v3_project_scoped_token(**kwargs): + """Generate a Keystone V3 token based on auth request.""" + ref = _get_normalized_token_data(**kwargs) + + o = {'token': {'expires_at': '2099-05-22T00:02:43.941430Z', + 'issued_at': '2013-05-21T00:02:43.941473Z', + 'methods': ['password'], + 'project': {'id': ref.get('project_id'), + 'name': ref.get('project_name'), + 'domain': {'id': ref.get('project_domain_id'), + 'name': ref.get( + 'project_domain_name') + } + }, + 'user': {'id': ref.get('user_id'), + 'name': ref.get('username'), + 'domain': {'id': ref.get('user_domain_id'), + 'name': ref.get('user_domain_name') + } + }, + 'roles': ref.get('roles') + }} + + # we only care about Barbican and Keystone endpoints + o['token']['catalog'] = [ + {'endpoints': [ + { + 'id': uuid.uuid4().hex, + 'interface': 'public', + 'region': 'RegionTwo', + 'url': ref.get('barbican_url') + }], + 'id': uuid.uuid4().hex, + 'type': 'keystore'}, + {'endpoints': [ + { + 'id': uuid.uuid4().hex, + 'interface': 'public', + 'region': 'RegionTwo', + 'url': ref.get('auth_url') + }, + { + 'id': uuid.uuid4().hex, + 'interface': 'admin', + 'region': 'RegionTwo', + 'url': ref.get('auth_url') + }], + 'id': uuid.uuid4().hex, + 'type': 'identity'}] + + # token ID is conveyed via the X-Subject-Token header so we are generating + # one to stash there + token_id = uuid.uuid4().hex + + return token_id, o diff --git a/barbicanclient/test/test_barbican.py b/barbicanclient/test/test_barbican.py index cdf2bc8..995ce1a 100644 --- a/barbicanclient/test/test_barbican.py +++ b/barbicanclient/test/test_barbican.py @@ -18,30 +18,188 @@ import sys import six import testtools +import httpretty +import uuid +import json +from barbicanclient.test import keystone_client_fixtures +from barbicanclient.test import test_client import barbicanclient.barbican -class TestBarbican(testtools.TestCase): +class WhenTestingBarbicanCLI(test_client.BaseEntityResource): + + def setUp(self): + self._setUp('barbican') + def barbican(self, argstr): """Source: Keystone client's shell method in test_shell.py""" orig = sys.stdout + orig_err = sys.stderr clean_env = {} _old_env, os.environ = os.environ, clean_env.copy() + exit_code = 0 try: sys.stdout = six.StringIO() + sys.stderr = sys.stdout _barbican = barbicanclient.barbican.Barbican() _barbican.execute(argv=argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(exc_value.code, 0) + exit_code = exc_value.code finally: - out = sys.stdout.getvalue() + if exit_code == 0: + out = sys.stdout.getvalue() + else: + out = sys.stderr.getvalue() sys.stdout.close() sys.stdout = orig + sys.stderr = orig_err os.environ = _old_env - return out + return exit_code, out + + def test_should_show_usage_error_with_no_args(self): + args = "" + exit_code, out = self.barbican(args) + self.assertEqual(2, exit_code) + self.assertIn('error: the following', out) - def test_help(self): + def test_should_show_usage_with_help_flag(self): args = "-h" - self.assertIn('usage: ', self.barbican(args)) + exit_code, out = self.barbican(args) + self.assertEqual(0, exit_code) + self.assertIn('usage: ', out) + + def test_should_error_if_noauth_and_authurl_both_specified(self): + args = "--no-auth --os-auth-url http://localhost:5000/v3" + exit_code, out = self.barbican(args) + self.assertEqual(2, exit_code) + self.assertIn( + 'error: argument --os-auth-url/-A: not allowed with ' + 'argument --no-auth/-N', out) + + def _expect_error_with_invalid_noauth_args(self, args): + exit_code, out = self.barbican(args) + self.assertEqual(1, exit_code) + expected_err_msg = 'ERROR: please specify --endpoint '\ + 'and --os-project-id(or --os-tenant-id)\n' + self.assertIn(expected_err_msg, out) + + def test_should_error_if_noauth_and_missing_endpoint_tenantid_args(self): + self._expect_error_with_invalid_noauth_args("--no-auth secret list") + self._expect_error_with_invalid_noauth_args( + "--no-auth --endpoint http://xyz secret list") + self._expect_error_with_invalid_noauth_args( + "--no-auth --os-tenant-id 123 secret list") + self._expect_error_with_invalid_noauth_args( + "--no-auth --os-project-id 123 secret list") + + def _expect_success_code(self, args): + exit_code, out = self.barbican(args) + self.assertEqual(0, exit_code) + + def _expect_failure_code(self, args, code=1): + exit_code, out = self.barbican(args) + self.assertEqual(code, exit_code) + + def _assert_status_code_and_msg(self, args, expected_msg, code=1): + exit_code, out = self.barbican(args) + self.assertEqual(code, exit_code) + self.assertIn(expected_msg, out) + + @httpretty.activate + def test_should_succeed_if_noauth_with_valid_args_specified(self): + list_secrets_content = '{"secrets": [], "total": 0}' + list_secrets_url = '%s%s/secrets' % ( + self.endpoint, self.tenant_id) + httpretty.register_uri( + httpretty.GET, list_secrets_url, + body=list_secrets_content) + self._expect_success_code( + "--no-auth --endpoint %s --os-tenant-id %s secret list" + % (self.endpoint, self.tenant_id)) + + def test_should_error_if_required_keystone_auth_arguments_are_missing( + self): + expected_error_msg = 'ERROR: please specify authentication credentials' + self._assert_status_code_and_msg( + '--os-auth-url http://localhost:35357/v2.0 secret list', + expected_error_msg) + self._assert_status_code_and_msg('--os-auth-url ' + 'http://localhost:35357/v2.0 ' + '--os-username barbican ' + '--os-password barbican ' + 'secret list', expected_error_msg) + + +class TestBarbicanWithKeystoneClient(testtools.TestCase): + + def setUp(self): + super(TestBarbicanWithKeystoneClient, self).setUp() + self.kwargs = {'auth_url': keystone_client_fixtures.V3_URL} + for arg in ['username', 'password', 'project_name', + 'user_domain_name', 'project_domain_name']: + self.kwargs[arg] = uuid.uuid4().hex + self.barbican = barbicanclient.barbican.Barbican() + + def _to_argv(self, **kwargs): + """Format Keystone client arguments into command line argv.""" + argv = [] + for k, v in six.iteritems(kwargs): + argv.append('--os-' + k.replace('_', '-')) + argv.append(v) + return argv + + @httpretty.activate + def test_v2_auth(self): + self.kwargs['auth_url'] = keystone_client_fixtures.V2_URL + argv = self._to_argv(**self.kwargs) + argv.append('secret') + argv.append('list') + argv.append('-h') + argv.append('mysecretid') + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + self.kwargs['auth_url'], + body=keystone_client_fixtures.V2_VERSION_ENTRY) + # emulate Keystone v2 token request + v2_token = keystone_client_fixtures.generate_v2_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (self.kwargs['auth_url']), + body=json.dumps(v2_token)) + # emulate get secrets + barbican_url = keystone_client_fixtures.BARBICAN_ENDPOINT + httpretty.register_uri( + httpretty.DELETE, + '%s/%s/secrets/mysecretid' % ( + barbican_url, + v2_token['access']['token']['tenant']['id']), + status=200) + self.barbican.execute(argv=argv) + + @httpretty.activate + def test_v3_auth(self): + argv = self._to_argv(**self.kwargs) + argv.append('secret') + argv.append('list') + argv.append('-h') + argv.append('mysecretid') + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + self.kwargs['auth_url'], + body=keystone_client_fixtures.V3_VERSION_ENTRY) + # emulate Keystone v3 token request + id, v3_token = \ + keystone_client_fixtures.generate_v3_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % (self.kwargs['auth_url']), + body=json.dumps(v3_token)) + # emulate delete secret + barbican_url = keystone_client_fixtures.BARBICAN_ENDPOINT + httpretty.register_uri( + httpretty.DELETE, + '%s/%s/secrets/mysecretid' % ( + barbican_url, + v3_token['token']['project']['id']), + status=200) + self.barbican.execute(argv=argv) diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index 5e33633..74fc150 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -14,15 +14,24 @@ # limitations under the License. import mock +import httpretty import requests import testtools +import json +import uuid from barbicanclient import client +from barbicanclient.test import keystone_client_fixtures from barbicanclient.openstack.common import timeutils from barbicanclient.openstack.common import jsonutils +from keystoneclient import session as ks_session +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 + class FakeAuth(object): + def __init__(self, auth_token, barbican_url, tenant_name, tenant_id): self.auth_token = auth_token self.barbican_url = barbican_url @@ -31,6 +40,7 @@ class FakeAuth(object): class FakeResp(object): + def __init__(self, status_code, response_dict=None, content=None): self.status_code = status_code self.response_dict = response_dict @@ -47,7 +57,34 @@ class FakeResp(object): return self.content +class KeystonePasswordPlugins(object): + v2_auth_url = keystone_client_fixtures.V2_URL + v3_auth_url = keystone_client_fixtures.V3_URL + username = 'username' + password = 'password' + project_name = tenant_name = 'tenantname' + tenant_id = project_id = 'tenantid' + user_domain_name = 'udomain_name' + user_domain_id = 'udomain_id' + project_domain_name = 'pdomain_name' + project_domain_id = 'pdomain_id' + + @classmethod + def get_v2_plugin(cls): + return v2.Password(auth_url=cls.v2_auth_url, username=cls.username, + password=cls.password, tenant_name=cls.tenant_name) + + @classmethod + def get_v3_plugin(cls): + return v3.Password(auth_url=cls.v3_auth_url, username=cls.username, + password=cls.password, + project_name=cls.project_name, + user_domain_name=cls.user_domain_name, + project_domain_name=cls.project_domain_name) + + class WhenTestingClientInit(testtools.TestCase): + def setUp(self): super(WhenTestingClientInit, self).setUp() self.auth_endpoint = 'https://localhost:5000/v2.0/' @@ -71,12 +108,12 @@ class WhenTestingClientInit(testtools.TestCase): def test_can_be_used_without_auth_plugin(self): c = client.Client(auth_plugin=None, endpoint=self.endpoint, tenant_id=self.tenant_id) - self.assertNotIn('X-Auth-Token', c._session.headers) + expected = '%s%s' % (self.endpoint, self.tenant_id) + self.assertEqual(expected, c.base_url) def test_auth_token_header_is_set_when_using_auth_plugin(self): c = client.Client(auth_plugin=self.fake_auth) - self.assertIn('X-Auth-Token', c._session.headers) - self.assertEqual(c._session.headers.get('X-Auth-Token'), + self.assertEqual(c._session.get_token(), self.auth_token) def test_error_thrown_when_no_auth_and_no_endpoint(self): @@ -91,6 +128,10 @@ class WhenTestingClientInit(testtools.TestCase): c = client.Client(endpoint=self.endpoint, tenant_id=self.tenant_id) self.assertEqual(c._barbican_url, self.endpoint.strip('/')) + def test_base_url_starts_with_endpoint_url(self): + c = client.Client(auth_plugin=self.fake_auth) + self.assertTrue(c.base_url.startswith(self.endpoint)) + def test_base_url_ends_with_tenant_id(self): c = client.Client(auth_plugin=self.fake_auth) self.assertTrue(c.base_url.endswith(self.tenant_id)) @@ -112,6 +153,7 @@ class WhenTestingClientInit(testtools.TestCase): class WhenTestingClientWithSession(testtools.TestCase): + def setUp(self): super(WhenTestingClientWithSession, self).setUp() self.endpoint = 'https://localhost:9311/v1/' @@ -133,16 +175,17 @@ class WhenTestingClientWithSession(testtools.TestCase): tenant_id=self.tenant_id) def test_should_post(self): - self.session.post.return_value = FakeResp(200, {'entity_ref': - self.entity_href}) + self.session.request.return_value = mock.MagicMock(status_code=200) + self.session.request.return_value.json.return_value = { + 'entity_ref': self.entity_href} resp_dict = self.client.post(self.entity, self.entity_dict) self.assertEqual(self.entity_href, resp_dict['entity_ref']) # Verify the correct URL was used to make the call. - args, kwargs = self.session.post.call_args - url = args[0] + args, kwargs = self.session.request.call_args + url = args[1] self.assertEqual(self.entity_base, url) # Verify that correct information was sent in the call. @@ -150,16 +193,16 @@ class WhenTestingClientWithSession(testtools.TestCase): self.assertEqual(self.entity_name, data['name']) def test_should_get(self): - self.session.get.return_value = FakeResp(200, {'name': - self.entity_name}) - + self.session.request.return_value = mock.MagicMock(status_code=200) + self.session.request.return_value.json.return_value = { + 'name': self.entity_name} resp_dict = self.client.get(self.entity_href) self.assertEqual(self.entity_name, resp_dict['name']) # Verify the correct URL was used to make the call. - args, kwargs = self.session.get.call_args - url = args[0] + args, kwargs = self.session.request.call_args + url = args[1] self.assertEqual(self.entity_href, url) # Verify that correct information was sent in the call. @@ -167,7 +210,8 @@ class WhenTestingClientWithSession(testtools.TestCase): self.assertEqual('application/json', headers['Accept']) def test_should_get_raw(self): - self.session.get.return_value = FakeResp(200, content='content') + self.session.request.return_value = mock.MagicMock(status_code=200, + content='content') headers = {'Accept': 'application/octet-stream'} content = self.client.get_raw(self.entity_href, headers) @@ -175,8 +219,8 @@ class WhenTestingClientWithSession(testtools.TestCase): self.assertEqual('content', content) # Verify the correct URL was used to make the call. - args, kwargs = self.session.get.call_args - url = args[0] + args, kwargs = self.session.request.call_args + url = args[1] self.assertEqual(self.entity_href, url) # Verify that correct information was sent in the call. @@ -184,17 +228,230 @@ class WhenTestingClientWithSession(testtools.TestCase): self.assertEqual('application/octet-stream', headers['Accept']) def test_should_delete(self): - self.session.delete.return_value = FakeResp(200) + self.session.request.return_value = mock.MagicMock(status_code=200) self.client.delete(self.entity_href) # Verify the correct URL was used to make the call. - args, kwargs = self.session.delete.call_args - url = args[0] + args, kwargs = self.session.request.call_args + url = args[1] self.assertEqual(self.entity_href, url) +class WhenTestingClientWithKeystoneV2(WhenTestingClientWithSession): + + def setUp(self): + super(WhenTestingClientWithKeystoneV2, self).setUp() + + @httpretty.activate + def test_should_get(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V2_URL, + body=keystone_client_fixtures.V2_VERSION_ENTRY) + # emulate Keystone v2 token request + v2_token = keystone_client_fixtures.generate_v2_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (keystone_client_fixtures.V2_URL), + body=json.dumps(v2_token)) + auth_plugin = KeystonePasswordPlugins.get_v2_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + list_secrets_url = '%s/secrets' % (c.base_url) + httpretty.register_uri( + httpretty.GET, + list_secrets_url, + status=200, + body='{"name": "%s", "secret_ref": "%s"}' % + (self.entity_name, self.entity_href)) + resp = c.get(list_secrets_url) + self.assertEqual(self.entity_name, resp['name']) + self.assertEqual(self.entity_href, resp['secret_ref']) + + @httpretty.activate + def test_should_post(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V2_URL, + body=keystone_client_fixtures.V2_VERSION_ENTRY) + # emulate Keystone v2 token request + v2_token = keystone_client_fixtures.generate_v2_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (keystone_client_fixtures.V2_URL), + body=json.dumps(v2_token)) + auth_plugin = KeystonePasswordPlugins.get_v2_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + post_secret_url = '%s/secrets/' % (c.base_url) + httpretty.register_uri( + httpretty.POST, + post_secret_url, + status=200, + body='{"name": "%s", "secret_ref": "%s"}' + % (self.entity_name, self.entity_href)) + resp = c.post('secrets', '{"name":"test"}') + self.assertEqual(self.entity_name, resp['name']) + self.assertEqual(self.entity_href, resp['secret_ref']) + + @httpretty.activate + def test_should_get_raw(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V2_URL, + body=keystone_client_fixtures.V2_VERSION_ENTRY) + # emulate Keystone v2 token request + v2_token = keystone_client_fixtures.generate_v2_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (keystone_client_fixtures.V2_URL), + body=json.dumps(v2_token)) + auth_plugin = KeystonePasswordPlugins.get_v2_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + get_secret_url = '%s/secrets/s1' % (c.base_url) + httpretty.register_uri( + httpretty.GET, + get_secret_url, + status=200, body='content') + headers = {"Content-Type": "application/json"} + resp = c.get_raw(get_secret_url, headers) + self.assertEqual(b'content', resp) + + @httpretty.activate + def test_should_delete(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V2_URL, + body=keystone_client_fixtures.V2_VERSION_ENTRY) + # emulate Keystone v2 token request + v2_token = keystone_client_fixtures.generate_v2_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (keystone_client_fixtures.V2_URL), + body=json.dumps(v2_token)) + auth_plugin = KeystonePasswordPlugins.get_v2_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + delete_secret_url = '%s/secrets/s1' % (c.base_url) + httpretty.register_uri( + httpretty.DELETE, + delete_secret_url, + status=201) + c.delete(delete_secret_url) + + +class WhenTestingClientWithKeystoneV3(WhenTestingClientWithSession): + + def setUp(self): + super(WhenTestingClientWithKeystoneV3, self).setUp() + + @httpretty.activate + def test_should_get(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V3_URL, + body=keystone_client_fixtures.V3_VERSION_ENTRY) + # emulate Keystone v3 token request + id, v3_token = keystone_client_fixtures.\ + generate_v3_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % ( + keystone_client_fixtures.V3_URL), + body=json.dumps(v3_token), x_subject_token=id) + auth_plugin = KeystonePasswordPlugins.get_v3_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + list_secrets_url = '%s/secrets' % (c.base_url) + httpretty.register_uri( + httpretty.GET, + list_secrets_url, + status=200, + body='{"name": "%s", "secret_ref": "%s"}' + % (self.entity_name, self.entity_href)) + resp = c.get(list_secrets_url) + self.assertEqual(self.entity_name, resp['name']) + self.assertEqual(self.entity_href, resp['secret_ref']) + + @httpretty.activate + def test_should_post(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V3_URL, + body=keystone_client_fixtures.V3_VERSION_ENTRY) + # emulate Keystone v3 token request + id, v3_token = keystone_client_fixtures.\ + generate_v3_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % ( + keystone_client_fixtures.V3_URL), + body=json.dumps(v3_token), + x_subject_token=id) + auth_plugin = KeystonePasswordPlugins.get_v3_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + post_secret_url = '%s/secrets/' % (c.base_url) + httpretty.register_uri( + httpretty.POST, + post_secret_url, + status=200, + x_subject_token=id, + body='{"name": "%s", "secret_ref": "%s"}' + % (self.entity_name, self.entity_href)) + resp = c.post('secrets', '{"name":"test"}') + self.assertEqual(self.entity_name, resp['name']) + self.assertEqual(self.entity_href, resp['secret_ref']) + + @httpretty.activate + def test_should_get_raw(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V3_URL, + body=keystone_client_fixtures.V3_VERSION_ENTRY) + # emulate Keystone v3 token request + id, v3_token = keystone_client_fixtures.\ + generate_v3_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % ( + keystone_client_fixtures.V3_URL), + body=json.dumps(v3_token), + x_subject_token=id) + auth_plugin = KeystonePasswordPlugins.get_v3_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + get_secret_url = '%s/secrets/s1' % (c.base_url) + httpretty.register_uri( + httpretty.GET, + get_secret_url, + status=200, body='content') + headers = {"Content-Type": "application/json"} + resp = c.get_raw(get_secret_url, headers) + self.assertEqual(b'content', resp) + + @httpretty.activate + def test_should_delete(self): + # emulate Keystone version discovery + httpretty.register_uri(httpretty.GET, + keystone_client_fixtures.V3_URL, + body=keystone_client_fixtures.V3_VERSION_ENTRY) + # emulate Keystone v3 token request + id, v3_token = keystone_client_fixtures.\ + generate_v3_project_scoped_token() + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % ( + keystone_client_fixtures.V3_URL), + body=json.dumps(v3_token), + x_subject_token=id) + auth_plugin = KeystonePasswordPlugins.get_v3_plugin() + c = client.Client(auth_plugin=auth_plugin) + # emulate list secrets + delete_secret_url = '%s/secrets/s1' % (c.base_url) + httpretty.register_uri( + httpretty.DELETE, + delete_secret_url, + status=201) + c.delete(delete_secret_url) + + class BaseEntityResource(testtools.TestCase): + def _setUp(self, entity): super(BaseEntityResource, self).setUp() self.endpoint = 'https://localhost:9311/v1/' diff --git a/requirements.txt b/requirements.txt index 86ce461..927084d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ pbr>=0.5.21,<1.0 - argparse requests>=1.2.3 six>=1.5.2 -python-keystoneclient>=0.3.2 +python-keystoneclient>=0.9.0 diff --git a/test-requirements.txt b/test-requirements.txt index 0d6ec30..170f7e0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,7 @@ coverage>=3.6 discover hacking>=0.7.0 +httpretty>=0.8.0 mock>=1.0.1 testrepository>=0.0.17 testtools>=0.9.32,<0.9.35 |