From 7e780a174a91269625cdb965bf87de8f60aa6142 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 29 Aug 2013 14:25:45 -0500 Subject: [PATCH 01/26] Move tests inside main package. --- .gitignore | 3 +++ {tests => barbicanclient/test}/__init__.py | 0 tests/client_test.py => barbicanclient/test/test_client.py | 0 tests/keep_test.py => barbicanclient/test/test_keep.py | 0 4 files changed, 3 insertions(+) rename {tests => barbicanclient/test}/__init__.py (100%) rename tests/client_test.py => barbicanclient/test/test_client.py (100%) rename tests/keep_test.py => barbicanclient/test/test_keep.py (100%) diff --git a/.gitignore b/.gitignore index 820ed7fb..00f981f8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ pip-log.txt .tox nosetests.xml +# pyenv +.python-version + # Translations *.mo diff --git a/tests/__init__.py b/barbicanclient/test/__init__.py similarity index 100% rename from tests/__init__.py rename to barbicanclient/test/__init__.py diff --git a/tests/client_test.py b/barbicanclient/test/test_client.py similarity index 100% rename from tests/client_test.py rename to barbicanclient/test/test_client.py diff --git a/tests/keep_test.py b/barbicanclient/test/test_keep.py similarity index 100% rename from tests/keep_test.py rename to barbicanclient/test/test_keep.py From 1b9f56fde2f4c4f122cf49e173f550bee7f03401 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 29 Aug 2013 15:57:24 -0500 Subject: [PATCH 02/26] Update tox to run pep8 by default. --- tools/hacking.sh | 3 +++ tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100755 tools/hacking.sh diff --git a/tools/hacking.sh b/tools/hacking.sh new file mode 100755 index 00000000..967902bb --- /dev/null +++ b/tools/hacking.sh @@ -0,0 +1,3 @@ +#!/bin/bash +flake8 barbicanclient | tee flake8.log +exit ${PIPESTATUS[0]} diff --git a/tox.ini b/tox.ini index e61c7277..2fd33e05 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27 +envlist = py26, py27, pep8 [testenv] setenv = VIRTUAL_ENV={envdir} @@ -18,7 +18,7 @@ deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires [testenv:pep8] -commands = flake8 +commands = {toxinidir}/tools/hacking.sh [testenv:venv] commands = {posargs} From 071891750797930e37781484b526e2de03218491 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 29 Aug 2013 16:32:54 -0500 Subject: [PATCH 03/26] Rename Connection to Client. --- barbicanclient/client.py | 5 +- barbicanclient/keep.py | 8 +- barbicanclient/test/test_client.py | 150 +++++++++++++---------------- 3 files changed, 76 insertions(+), 87 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index b42d536a..dd90010e 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -3,6 +3,7 @@ eventlet.monkey_patch(socket=True, select=True) import json import os + import requests from barbicanclient.secrets import Secret @@ -18,7 +19,7 @@ LOG = log.getLogger(__name__) log.setup('barbicanclient') -class Connection(object): +class Client(object): SECRETS_PATH = 'secrets' ORDERS_PATH = 'orders' @@ -45,7 +46,7 @@ class Connection(object): If a token is provided, an endpoint should be as well. """ - LOG.debug(_("Creating Connection object")) + LOG.debug(_("Creating Client object")) self.env = kwargs.get('fake_env') or env self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL') diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index b939aef4..5bd81254 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -169,10 +169,10 @@ class Keep: def execute(self, **kwargs): args = self.parser.parse_args(kwargs.get('argv')) - self.conn = client.Connection(args.auth_endpoint, args.user, - args.password, args.tenant, - args.token, - endpoint=args.endpoint) + self.conn = client.Client(args.auth_endpoint, args.user, + args.password, args.tenant, + args.token, + endpoint=args.endpoint) args.func(args) diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index fcc24e50..42610b7f 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -14,23 +14,15 @@ # limitations under the License. import json -import unittest2 as unittest from mock import MagicMock +import unittest2 as unittest from barbicanclient import client from barbicanclient.common.exceptions import ClientException -def suite(): - suite = unittest.TestSuite() - - suite.addTest(WhenTestingConnection()) - - return suite - - -class WhenTestingConnection(unittest.TestCase): +class WhenTestingClient(unittest.TestCase): def setUp(self): self.auth_endpoint = 'https://keystone.com/v2' self.user = 'user' @@ -60,23 +52,23 @@ class WhenTestingConnection(unittest.TestCase): 'req-6c19d09e-1167-445c-b435-d6b0818b59b9' } self.request.return_value.ok = True - self.connection = client.Connection(self.auth_endpoint, self.user, - self.key, self.tenant, - token=self.auth_token, - authenticate=self.authenticate, - request=self.request, - endpoint=self.endpoint) + self.client = client.Client(self.auth_endpoint, self.user, + self.key, self.tenant, + token=self.auth_token, + authenticate=self.authenticate, + request=self.request, + endpoint=self.endpoint) def test_should_connect_with_token(self): self.assertFalse(self.authenticate.called) def test_should_connect_without_token(self): - self.connection = client.Connection(self.auth_endpoint, - self.user, - self.key, - self.tenant, - authenticate=self.authenticate, - endpoint=self.endpoint) + self.client = client.Client(self.auth_endpoint, + self.user, + self.key, + self.tenant, + authenticate=self.authenticate, + endpoint=self.endpoint) self.authenticate\ .assert_called_once_with(self.auth_endpoint, self.user, @@ -86,22 +78,22 @@ class WhenTestingConnection(unittest.TestCase): endpoint=self.endpoint, cacert=None ) - self.assertEqual(self.auth_token, self.connection.auth_token) - self.assertEqual(self.auth_endpoint, self.connection._auth_endpoint) - self.assertEqual(self.user, self.connection._user) - self.assertEqual(self.key, self.connection._key) - self.assertEqual(self.tenant, self.connection._tenant) - self.assertEqual(self.endpoint, self.connection._endpoint) + self.assertEqual(self.auth_token, self.client.auth_token) + self.assertEqual(self.auth_endpoint, self.client._auth_endpoint) + self.assertEqual(self.user, self.client._user) + self.assertEqual(self.key, self.client._key) + self.assertEqual(self.tenant, self.client._tenant) + self.assertEqual(self.endpoint, self.client._endpoint) def test_should_raise_for_bad_args(self): with self.assertRaises(ClientException): - self.connection = client.Connection(None, self.user, - self.key, self.tenant, - fake_env=self.fake_env, - token=self.auth_token, - authenticate=self.authenticate, - request=self.request, - endpoint=self.endpoint) + self.client = client.Client(None, self.user, + self.key, self.tenant, + fake_env=self.fake_env, + token=self.auth_token, + authenticate=self.authenticate, + request=self.request, + endpoint=self.endpoint) def test_should_create_secret(self): body = {'status': "ACTIVE", @@ -117,17 +109,17 @@ class WhenTestingConnection(unittest.TestCase): 'payload_content_type': 'text/plain' } - secret = client.Secret(self.connection, body) + secret = client.Secret(self.client, body) self.request.return_value.content = json.dumps(body) - created = self.connection.create_secret(name='test_secret', - payload='Test secret', - algorithm='aes', - bit_length=256, - cypher_type='cbc', - expiration='2015-06-07T16:13' - ':38.889851', - payload_content_type= - 'text/plain') + created = self.client.create_secret(name='test_secret', + payload='Test secret', + algorithm='aes', + bit_length=256, + cypher_type='cbc', + expiration='2015-06-07T16:13' + ':38.889851', + payload_content_type= + 'text/plain') self.assertTrue(self._are_equivalent(secret, created)) def test_should_create_order(self): @@ -149,20 +141,22 @@ class WhenTestingConnection(unittest.TestCase): "2f53-4c0a-a0f3-33796671efc3" } - order = client.Order(self.connection, body) + order = client.Order(self.client, body) self.request.return_value.content = json.dumps(body) - created = self.connection.create_order(name='test_secret', - payload_content_type='application/octet-stream', - algorithm='aes', - bit_length=256, - cypher_type='cbc') + created = self.client.create_order( + name='test_secret', + payload_content_type='application/octet-stream', + algorithm='aes', + bit_length=256, + cypher_type='cbc' + ) self.assertTrue(self._are_equivalent(order, created)) def test_list_no_secrets(self): body0 = {'secrets': []} secrets = [] self.request.return_value.content = json.dumps(body0) - secret_list, prev_ref, next_ref = self.connection.list_secrets(0, 0) + secret_list, prev_ref, next_ref = self.client.list_secrets(0, 0) self.assertTrue(self._are_equivalent(secrets, secret_list)) self.assertIsNone(prev_ref) self.assertIsNone(next_ref) @@ -182,15 +176,14 @@ class WhenTestingConnection(unittest.TestCase): 'expiration': None, 'bit_length': None, 'mime_type': 'text/plain'}], - 'next': "{0}/{1}?limit={2}&offset={2}".format(self.connection. + 'next': "{0}/{1}?limit={2}&offset={2}".format(self.client. _tenant, - self.connection. + self.client. SECRETS_PATH, limit)} - secrets = [client.Secret(self.connection, body1['secrets'][0])] + secrets = [client.Secret(self.client, body1['secrets'][0])] self.request.return_value.content = json.dumps(body1) - secret_list, prev_ref, next_ref = self.connection.list_secrets(limit, - 0) + secret_list, prev_ref, next_ref = self.client.list_secrets(limit, 0) self.assertTrue(self._are_equivalent(secrets, secret_list)) self.assertIsNone(prev_ref) self.assertEqual(body1['next'], next_ref) @@ -211,8 +204,8 @@ class WhenTestingConnection(unittest.TestCase): 'bit_length': None, 'mime_type': 'text/plain'}], 'previous': "{0}/{1}?limit={2}&offset={2}".format( - self.connection._tenant, - self.connection. + self.client._tenant, + self.client. SECRETS_PATH, limit)} @@ -225,12 +218,11 @@ class WhenTestingConnection(unittest.TestCase): + 'b6e-4ef1-48d1-8950-170c1a5838e1' body2['next'] = None - secrets = [client.Secret(self.connection, b['secrets'][0]) + secrets = [client.Secret(self.client, b['secrets'][0]) for b in (body1, body2)] body2['secrets'].insert(0, body1['secrets'][0]) self.request.return_value.content = json.dumps(body2) - secret_list, prev_ref, next_ref = self.connection.list_secrets(limit, - 1) + secret_list, prev_ref, next_ref = self.client.list_secrets(limit, 1) self.assertTrue(self._are_equivalent(secrets, secret_list)) self.assertEqual(body2['previous'], prev_ref) self.assertIsNone(next_ref) @@ -239,7 +231,7 @@ class WhenTestingConnection(unittest.TestCase): body0 = {'orders': []} orders = [] self.request.return_value.content = json.dumps(body0) - order_list, prev_ref, next_ref = self.connection.list_orders(0, 0) + order_list, prev_ref, next_ref = self.client.list_orders(0, 0) self.assertTrue(self._are_equivalent(orders, order_list)) self.assertIsNone(prev_ref) self.assertIsNone(next_ref) @@ -260,14 +252,14 @@ class WhenTestingConnection(unittest.TestCase): 'expiration': None, 'bit_length': None, 'mime_type': 'text/plain'}}], - 'next': "{0}/{1}?limit={2}&offset={2}".format(self.connection. + 'next': "{0}/{1}?limit={2}&offset={2}".format(self.client. _tenant, - self.connection. + self.client. ORDERS_PATH, limit)} - orders = [client.Order(self.connection, body1['orders'][0])] + orders = [client.Order(self.client, body1['orders'][0])] self.request.return_value.content = json.dumps(body1) - order_list, prev_ref, next_ref = self.connection.list_orders(limit, 0) + order_list, prev_ref, next_ref = self.client.list_orders(limit, 0) self.assertTrue(self._are_equivalent(orders, order_list)) self.assertIsNone(prev_ref) self.assertEqual(body1['next'], next_ref) @@ -289,8 +281,8 @@ class WhenTestingConnection(unittest.TestCase): 'bit_length': None, 'mime_type': 'text/plain'}}], 'previous': "{0}/{1}?limit={2}&offset={2}".format( - self.connection._tenant, - self.connection. + self.client._tenant, + self.client. SECRETS_PATH, limit)} body2 = body1 @@ -307,31 +299,31 @@ class WhenTestingConnection(unittest.TestCase): + 'b6e-4ef1-48d1-8950-170c1a5838e1' body2['next'] = None - orders = [client.Order(self.connection, b['orders'][0]) + orders = [client.Order(self.client, b['orders'][0]) for b in (body1, body2)] body2['orders'].insert(0, body1['orders'][0]) self.request.return_value.content = json.dumps(body2) - order_list, prev_ref, next_ref = self.connection.list_orders(limit, 1) + order_list, prev_ref, next_ref = self.client.list_orders(limit, 1) self.assertTrue(self._are_equivalent(orders, order_list)) self.assertEqual(body2['previous'], prev_ref) self.assertIsNone(next_ref) def test_should_get_response(self): self._setup_request() - headers, body = self.connection._perform_http('GET', self.href) + headers, body = self.client._perform_http('GET', self.href) self.assertEqual(self.request.return_value.headers, headers) self.assertEqual(json.loads(self.request.return_value.content), body) def test_should_parse_json(self): self._setup_request() - headers, body = self.connection._perform_http('GET', self.href, - parse_json=True) + headers, body = self.client._perform_http('GET', self.href, + parse_json=True) self.assertEqual(json.loads(self.request.return_value.content), body) def test_should_not_parse_json(self): self._setup_request() - headers, body = self.connection._perform_http('GET', self.href, - parse_json=False) + headers, body = self.client._perform_http('GET', self.href, + parse_json=False) self.assertEqual(self.request.return_value.content, body) def test_should_raise_for_bad_response(self): @@ -339,7 +331,7 @@ class WhenTestingConnection(unittest.TestCase): self.request.return_value.ok = False self.request.return_value.status_code = 404 with self.assertRaises(ClientException) as e: - self.connection._perform_http('GET', self.href) + self.client._perform_http('GET', self.href) exception = e.exception self.assertEqual(404, exception.http_status) @@ -353,7 +345,3 @@ class WhenTestingConnection(unittest.TestCase): return all([self._are_equivalent(x, y) for x, y in zip(a, b)]) else: return (a.__dict__ == b.__dict__) - - -if __name__ == '__main__': - unittest.main() From c7a32a50530521d332ee0ed26825892c71f09e73 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Fri, 30 Aug 2013 11:34:22 -0500 Subject: [PATCH 04/26] Added auth param to Client. --- barbicanclient/client.py | 47 ++++++++++++++++++++---------- barbicanclient/test/test_client.py | 38 +++++++++++++++++------- tools/test-requires | 13 +++------ tox.ini | 4 +-- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index dd90010e..3b08597e 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -1,6 +1,3 @@ -import eventlet -eventlet.monkey_patch(socket=True, select=True) - import json import os @@ -23,8 +20,9 @@ class Client(object): SECRETS_PATH = 'secrets' ORDERS_PATH = 'orders' - def __init__(self, auth_endpoint=None, user=None, key=None, tenant=None, - token=None, **kwargs): + def __init__(self, auth=True, + auth_endpoint=None, user=None, password=None, tenant=None, + key=None, token=None, **kwargs): """ Authenticate and connect to the service endpoint, which can be received through authentication. @@ -32,27 +30,44 @@ class Client(object): Environment variables will be used by default when their corresponding arguments are not passed in. - :param auth_endpoint: The auth URL to authenticate against - default: env('OS_AUTH_URL') - :param user: The user to authenticate as - default: env('OS_USERNAME') - :param key: The API key or password to auth with - default: env('OS_PASSWORD') + :param auth: Whether the client should use keystone + authentication, defaults to True + :param auth_endpoint: The keystone URL used for authentication + required if auth=True + default: env('OS_AUTH_URL') + :param user: keystone user account, required if auth=True + default: env('OS_USERNAME') + :param password: password associated with the user + required if auth=Tru + default: env('OS_PASSWORD') :param tenant: The tenant ID - default: env('OS_TENANT_NAME') - :keyword param endpoint: The barbican endpoint to connect to - default: env('BARBICAN_ENDPOINT') + default: env('OS_TENANT_NAME') - If a token is provided, an endpoint should be as well. + :param key: The API key or password to auth with + :keyword param endpoint: The barbican endpoint to connect to + default: env('BARBICAN_ENDPOINT') """ LOG.debug(_("Creating Client object")) self.env = kwargs.get('fake_env') or env + + if auth: + LOG.debug(_('Using authentication with keystone')) + self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL') + self._user = user or self.env('OS_USERNAME') + self._password = password or self.env('OS_PASSWORD') + self._tenant = tenant or self.env('OS_TENANT_NAME') + if not all([self._auth_endpoint, self._user, + self._password, self._tenant]): + raise ValueError('Authentication requires an endpoint, user, ' + 'password, and tenant.') + #TODO(dmend): remove these self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL') self._user = user or self.env('OS_USERNAME') - self._key = key or self.env('OS_PASSWORD') self._tenant = tenant or self.env('OS_TENANT_NAME') + self._key = key or self._password + if not all([self._auth_endpoint, self._user, self._key, self._tenant]): raise ClientException("The authorization endpoint, username, key," " and tenant name should either be passed i" diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index 42610b7f..fb3986e3 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -26,8 +26,10 @@ class WhenTestingClient(unittest.TestCase): def setUp(self): self.auth_endpoint = 'https://keystone.com/v2' self.user = 'user' - self.key = 'key' + self.password = 'password' self.tenant = 'tenant' + + self.key = 'key' self.endpoint = 'http://localhost:9311/v1/' self.auth_token = 'token' self.href = 'http://localhost:9311/v1/12345/orders' @@ -52,21 +54,34 @@ class WhenTestingClient(unittest.TestCase): 'req-6c19d09e-1167-445c-b435-d6b0818b59b9' } self.request.return_value.ok = True - self.client = client.Client(self.auth_endpoint, self.user, - self.key, self.tenant, + self.client = client.Client(auth_endpoint=self.auth_endpoint, + user=self.user, + key=self.key, tenant=self.tenant, token=self.auth_token, authenticate=self.authenticate, request=self.request, - endpoint=self.endpoint) + endpoint=self.endpoint, + auth=False) + + def test_authenticated_client_requires_endpoint_user_pw_tenant(self): + with self.assertRaises(ValueError): + c = client.Client(auth=True) + with self.assertRaises(ValueError): + c = client.Client() # default auth=True + c=client.Client(auth_endpoint=self.auth_endpoint, user=self.user, + password=self.password, tenant=self.tenant, + #TODO(dmend): remove authenticate below + authenticate=self.authenticate) def test_should_connect_with_token(self): self.assertFalse(self.authenticate.called) def test_should_connect_without_token(self): - self.client = client.Client(self.auth_endpoint, - self.user, - self.key, - self.tenant, + self.client = client.Client(auth=False, + auth_endpoint=self.auth_endpoint, + user=self.user, + key=self.key, + tenant=self.tenant, authenticate=self.authenticate, endpoint=self.endpoint) self.authenticate\ @@ -87,8 +102,11 @@ class WhenTestingClient(unittest.TestCase): def test_should_raise_for_bad_args(self): with self.assertRaises(ClientException): - self.client = client.Client(None, self.user, - self.key, self.tenant, + self.client = client.Client(auth=False, + auth_endpoint=None, + user=self.user, + key=self.key, + tenant=self.tenant, fake_env=self.fake_env, token=self.auth_token, authenticate=self.authenticate, diff --git a/tools/test-requires b/tools/test-requires index c3f8fb22..c06b5c5e 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,15 +1,10 @@ -distribute>=0.6.24 - -# Install bounded pep8/pyflakes first, then let flake8 install -pep8==1.4.5 -pyflakes==0.7.2 -flake8==2.0 -hacking>=0.5.3,<0.6 - +pep8>=1.4.5 +pyflakes>=0.7.2 +flake8>=2.0 +hacking>=0.5.3 coverage discover -mox mock>=1.0.1 sphinx>=1.1.2 diff --git a/tox.ini b/tox.ini index 2fd33e05..2f35b9ae 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ show-source = True exclude = .venv,.tox,dist,doc,*egg [testenv:py26] -commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbican} +commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbicanclient} [testenv:py27] -commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbican} +commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbicanclient} From d8193a5b914d8d49c566cbc23644622485206892 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Fri, 30 Aug 2013 23:00:34 -0500 Subject: [PATCH 05/26] Added KeystoneAuth class for auth stuff --- barbicanclient/common/auth.py | 44 +++++++++++++++++++++++++ barbicanclient/test/common/__init__.py | 0 barbicanclient/test/common/test_auth.py | 29 ++++++++++++++++ barbicanclient/test/test_client.py | 15 ++++++--- 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 barbicanclient/test/common/__init__.py create mode 100644 barbicanclient/test/common/test_auth.py diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index 891f9d63..5498f5aa 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -68,3 +68,47 @@ def authenticate(auth_url, user, key, tenant, **kwargs): raise ClientException('Endpoint not found in service catalog') return endpoint, _ksclient.auth_token + + +class AuthException(Exception): + """Raised when authorization fails.""" + def __init__(self, message): + self.message = message + + +class KeystoneAuth(object): + def __init__(self, endpoint='', username='', password='', + tenant_name='', tenant_id=''): + if not all([endpoint, username, password, tenant_name or tenant_id]): + raise ValueError('Please provide endpoint, username, password,' + ' and tenant_id or tenant_name)') + self._keystone = ksclient.Client(username=username, + password=password, + tenant_name=tenant_name, + endpoint=endpoint) + self._barbican_url = None + #TODO(dmend): make these configurable + self._service_type = 'keystore' + self._endpoint_type = 'publicURL' + + @property + def auth_token(self): + return self._keystone.auth_token + + @property + def barbican_url(self): + if not self._barbican_url: + try: + self._barbican_url = self._keystone.service_catalog.url_for( + attr='region', + filter_value=self._keystone.region_name, + service_type=self._service_type, + endpoint_type=self._endpoint_type + ) + except exceptions.EmptyCatalog: + LOG.error('Keystone is reporting an empty catalog.') + raise AuthException('Empty keystone catalog.') + except exceptions.EndpointNotFound: + LOG.error('Barbican endpoint not found in keystone catalog.') + raise AuthException('Barbican endpoint not found.') + return self._barbican_url diff --git a/barbicanclient/test/common/__init__.py b/barbicanclient/test/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/barbicanclient/test/common/test_auth.py b/barbicanclient/test/common/test_auth.py new file mode 100644 index 00000000..8376d1b0 --- /dev/null +++ b/barbicanclient/test/common/test_auth.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 unittest2 as unittest + +from barbicanclient.common import auth + + +class WhenTestingKeystoneAuthentication(unittest.TestCase): + def setUp(self): + self.keystone = auth.KeystoneAuth(endpoint='endpoint_url', + username='user', + password='password', + tenant_name='demo') + + def test_endpoint_username_password_tenant_are_required(self): + with self.assertRaises(ValueError): + keystone = auth.KeystoneAuth() diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index fb3986e3..8ef1e6bb 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -19,16 +19,21 @@ from mock import MagicMock import unittest2 as unittest from barbicanclient import client +from barbicanclient.common import auth from barbicanclient.common.exceptions import ClientException class WhenTestingClient(unittest.TestCase): def setUp(self): - self.auth_endpoint = 'https://keystone.com/v2' + self.auth_endpoint = 'https://localhost:5000/v2.0/' self.user = 'user' self.password = 'password' self.tenant = 'tenant' - + self.keystone = auth.KeystoneAuth(endpoint=self.auth_endpoint, + username=self.user, + password=self.password, + tenant_name=self.tenant) + self.key = 'key' self.endpoint = 'http://localhost:9311/v1/' self.auth_token = 'token' @@ -54,7 +59,7 @@ class WhenTestingClient(unittest.TestCase): 'req-6c19d09e-1167-445c-b435-d6b0818b59b9' } self.request.return_value.ok = True - self.client = client.Client(auth_endpoint=self.auth_endpoint, + self.client = client.Client(auth_endpoint=self.auth_endpoint, user=self.user, key=self.key, tenant=self.tenant, token=self.auth_token, @@ -102,8 +107,8 @@ class WhenTestingClient(unittest.TestCase): def test_should_raise_for_bad_args(self): with self.assertRaises(ClientException): - self.client = client.Client(auth=False, - auth_endpoint=None, + self.client = client.Client(auth=False, + auth_endpoint=None, user=self.user, key=self.key, tenant=self.tenant, From bab914d1cbc656ced395b0862fbc0120f79f6f63 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Fri, 30 Aug 2013 23:44:22 -0500 Subject: [PATCH 06/26] Use KeystoneAuth in authenticate method. --- barbicanclient/common/auth.py | 76 +++++------------------------------ 1 file changed, 10 insertions(+), 66 deletions(-) diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index 5498f5aa..9f6051be 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -6,68 +6,12 @@ from keystoneclient import exceptions def authenticate(auth_url, user, key, tenant, **kwargs): - """Authenticates against the endpoint to use. The correct - endpoint to use is looked up in the service catalog. The - caller can override this lookup by passing the endpoint - as a parameter. - - :param auth_url: The keystone auth endpoint to use - :param user: The username to use for auth - :param key: The apikey to use for authentiation - :param endpoint: The Barbican endpoint to use. IOW, don't - look up an endpoint in the service catalog, just use - this one instead. - :param tenant_name: The optional tenant-name to use - :param tenant_id: The optional tenant ID toi use - :param cacert: The cacert PEM file to use - :param service_type: The service type to look for in - the service catalog - :param endpoint_type The endpoint type to reference in - the service catalog - :param region_name The region to pass for authentication - - :returns: Tuple containing Barbican endpoint and token - - :raises: ClientException - """ - insecure = kwargs.get('insecure', False) - endpoint = kwargs.get('endpoint') - cacert = kwargs.get('cacert') - - try: - _ksclient = ksclient.Client(username=user, - password=key, - tenant_name=tenant, - cacert=cacert, - auth_url=auth_url, - insecure=insecure) - - except exceptions.Unauthorized: - raise ClientException('Unauthorized. Check username, password' - ' and tenant name/id') - - except exceptions.AuthorizationFailure: - raise ClientException('Authorization Failure. %s') - - if not endpoint: - # The user did not pass in an endpoint, so we need to - # look one up on their behalf in the service catalog - - # TODO(jdp): Ensure that this is the correct service_type field - service_type = kwargs.get('service_type', 'queueing') - endpoint_type = kwargs.get('endpoint_type', 'publicURL') - region = kwargs.get('region_name') - - try: - endpoint = _ksclient.service_catalog.url_for( - attr='region', - filter_value=region, - service_type=service_type, - endpoint_type=endpoint_type) - except exceptions.EndpointNotFound: - raise ClientException('Endpoint not found in service catalog') - - return endpoint, _ksclient.auth_token + #TODO(dmend): remove this method + keystone = KeystoneAuth(auth_url=auth_url, + username=user, + password=key, + tenant_name=tenant) + return keystone.barbican_url, keysotone.auth_token class AuthException(Exception): @@ -77,15 +21,15 @@ class AuthException(Exception): class KeystoneAuth(object): - def __init__(self, endpoint='', username='', password='', + def __init__(self, auth_url='', username='', password='', tenant_name='', tenant_id=''): - if not all([endpoint, username, password, tenant_name or tenant_id]): - raise ValueError('Please provide endpoint, username, password,' + if not all([auth_url, username, password, tenant_name or tenant_id]): + raise ValueError('Please provide auht_url, username, password,' ' and tenant_id or tenant_name)') self._keystone = ksclient.Client(username=username, password=password, tenant_name=tenant_name, - endpoint=endpoint) + auth_url=auth_url) self._barbican_url = None #TODO(dmend): make these configurable self._service_type = 'keystore' From 7c41da3a2890fa92f15447d97683b93cf3cfc853 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Sun, 1 Sep 2013 01:41:32 -0500 Subject: [PATCH 07/26] Moved secret create and list into secret manager --- barbicanclient/base.py | 27 ++++ barbicanclient/client.py | 159 ++++++++++++------------ barbicanclient/common/auth.py | 3 + barbicanclient/secrets.py | 76 ++++++++++- barbicanclient/test/common/test_auth.py | 6 - barbicanclient/test/test_client.py | 22 +--- 6 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 barbicanclient/base.py diff --git a/barbicanclient/base.py b/barbicanclient/base.py new file mode 100644 index 00000000..449b07a9 --- /dev/null +++ b/barbicanclient/base.py @@ -0,0 +1,27 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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. +""" +Base utilites to build API operation managers. +""" + +class BaseEntityManager(object): + def __init__(self, api, entity): + self.api = api + self.entity = entity + + def _remove_empty_keys(self, dictionary): + for k in dictionary.keys(): + if dictionary[k] is None: + dictionary.pop(k) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 3b08597e..4955f8ac 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -1,8 +1,10 @@ import json import os +import urlparse import requests +from barbicanclient import secrets from barbicanclient.secrets import Secret from barbicanclient.orders import Order from barbicanclient.common import auth @@ -20,9 +22,8 @@ class Client(object): SECRETS_PATH = 'secrets' ORDERS_PATH = 'orders' - def __init__(self, auth=True, - auth_endpoint=None, user=None, password=None, tenant=None, - key=None, token=None, **kwargs): + def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None, + **kwargs): """ Authenticate and connect to the service endpoint, which can be received through authentication. @@ -30,18 +31,9 @@ class Client(object): Environment variables will be used by default when their corresponding arguments are not passed in. - :param auth: Whether the client should use keystone - authentication, defaults to True - :param auth_endpoint: The keystone URL used for authentication - required if auth=True - default: env('OS_AUTH_URL') - :param user: keystone user account, required if auth=True - default: env('OS_USERNAME') - :param password: password associated with the user - required if auth=Tru - default: env('OS_PASSWORD') - :param tenant: The tenant ID - default: env('OS_TENANT_NAME') + :param auth_plugin: Authentication backend plugin + defaults to None + :param endpoint: Barbican endpoint url :param key: The API key or password to auth with :keyword param endpoint: The barbican endpoint to connect to @@ -50,34 +42,49 @@ class Client(object): LOG.debug(_("Creating Client object")) - self.env = kwargs.get('fake_env') or env + self._session = requests.Session() + self.auth_plugin = auth_plugin - if auth: - LOG.debug(_('Using authentication with keystone')) - self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL') - self._user = user or self.env('OS_USERNAME') - self._password = password or self.env('OS_PASSWORD') - self._tenant = tenant or self.env('OS_TENANT_NAME') - if not all([self._auth_endpoint, self._user, - self._password, self._tenant]): - raise ValueError('Authentication requires an endpoint, user, ' - 'password, and tenant.') - #TODO(dmend): remove these - self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL') - self._user = user or self.env('OS_USERNAME') - self._tenant = tenant or self.env('OS_TENANT_NAME') - self._key = key or self._password + if self.auth_plugin is not None: + self._barbican_url = self.auth_plugin.barbican_url + self._tenant_id = self.auth_plugin.tenant_id + self._session.headers.update( + {'X-Auth-Token': self.auth_plugin.auth_token} + ) + 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 auth_plugin') + if endpoint.endswith('/'): + self._barbican_url = endpoint[:-1] + else: + self._barbican_url = endpoint + self._tenant_id = tenant_id - if not all([self._auth_endpoint, self._user, self._key, self._tenant]): - raise ClientException("The authorization endpoint, username, key," - " and tenant name should either be passed i" - "n or defined as environment variables.") - self.authenticate = kwargs.get('authenticate') or auth.authenticate - self.request = kwargs.get('request') or requests.request - self._endpoint = (kwargs.get('endpoint') or - self.env('BARBICAN_ENDPOINT')) - self._cacert = kwargs.get('cacert') - self.connect(token=(token or self.env('AUTH_TOKEN'))) + self.base_url = '{0}/{1}'.format(self._barbican_url, self._tenant_id) + self.secrets = secrets.SecretManager(self) + + # self.env = kwargs.get('fake_env') or env + + # #TODO(dmend): remove these + # self._auth_endpoint = kwargs.get('auth_endpoint') or self.env('OS_AUTH_URL') + # self._user = kwargs.get('user') or self.env('OS_USERNAME') + # self._tenant = kwargs.get('tenant') or self.env('OS_TENANT_NAME') + # self._key = kwargs.get('key') + + # if not all([self._auth_endpoint, self._user, self._key, self._tenant]): + # raise ClientException("The authorization endpoint, username, key," + # " and tenant name should either be passed i" + # "n or defined as environment variables.") + # self.authenticate = kwargs.get('authenticate') or auth.authenticate + # self.request = kwargs.get('request') or requests.request + # self._endpoint = (kwargs.get('endpoint') or + # self.env('BARBICAN_ENDPOINT')) + # self._cacert = kwargs.get('cacert') + # self.connect(token=(kwargs.get('token') or self.env('AUTH_TOKEN'))) @property def _conn(self): @@ -194,40 +201,15 @@ class Client(object): bit_length=None, cypher_type=None, expiration=None): - """ - Creates and returns a Secret object with all of its metadata filled in. - - :param name: A friendly name for the secret - :param payload: The unencrypted secret - :param payload_content_type: The format/type of the secret - :param payload_content_encoding: The encoding of the secret - :param algorithm: The algorithm the secret is used with - :param bit_length: The bit length of the secret - :param cypher_type: The cypher type (e.g. block cipher mode) - :param expiration: The expiration time of the secret in ISO 8601 format - """ - LOG.debug(_("Creating secret of payload content type {0}").format( - payload_content_type)) - href = "{0}/{1}".format(self._tenant, self.SECRETS_PATH) - LOG.debug(_("href: {0}").format(href)) - secret_dict = {} - secret_dict['name'] = name - secret_dict['payload'] = payload - secret_dict['payload_content_type'] = payload_content_type - secret_dict['payload_content_encoding'] = payload_content_encoding - secret_dict['algorithm'] = algorithm - secret_dict['cypher_type'] = cypher_type - secret_dict['bit_length'] = bit_length - secret_dict['expiration'] = expiration - self._remove_empty_keys(secret_dict) - LOG.debug(_("Request body: {0}").format(secret_dict)) - hdrs, body = self._perform_http(href=href, - method='POST', - request_body=json.dumps(secret_dict)) - - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - return self.get_secret(body['secret_ref']) + """Deprecated""" + self.secrets.create(name=name, + payload=payload, + payload_content_type=payload_content_type, + payload_content_encoding=payload_content_encoding, + algorithm=algorithm, + bit_length=bit_length, + mode=cypher_type, + expiration=expiration) def delete_secret_by_id(self, secret_id): """ @@ -411,11 +393,6 @@ class Client(object): LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) return Order(self._conn, body) - def _remove_empty_keys(self, dictionary): - for k in dictionary.keys(): - if dictionary[k] is None: - dictionary.pop(k) - def _perform_http(self, method, href, request_body='', headers={}, parse_json=True): """ @@ -457,6 +434,28 @@ class Client(object): return response.headers, resp_body + def _request(self, url, method, headers): + resp = self._session.request() + + def get(self, path, params): + url = '{0}/{1}/'.format(self.base_url, path) + headers = {'content-type': 'application/json'} + resp = self._session.get(url, params=params, headers=headers) + self._check_status_code(resp) + return resp.json() + + 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) + self._check_status_code(resp) + return resp.json() + + #TODO(dmend): beef this up + def _check_status_code(self, resp): + status = resp.status_code + print('status {0}'.format(status)) + def env(*vars, **kwargs): """Search for the first defined of possibly many env vars diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index 9f6051be..205d91dd 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -35,6 +35,9 @@ class KeystoneAuth(object): self._service_type = 'keystore' self._endpoint_type = 'publicURL' + self.tenant_name = self._keystone.tenant_name + self.tenant_id = self._keystone.tenant_id + @property def auth_token(self): return self._keystone.auth_token diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index d5ad4724..c0529402 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -1,6 +1,13 @@ from urlparse import urlparse + +from openstack.common import log as logging from openstack.common.timeutils import parse_isotime +from barbicanclient import base + + +LOG = logging.getLogger(__name__) + class Secret(object): @@ -8,12 +15,10 @@ class Secret(object): A secret is any data the user has stored in the key management system. """ - def __init__(self, connection, secret_dict): + def __init__(self, secret_dict): """ - Builds a secret object from a json representation. Includes the - connection object for subtasks. + Builds a secret object from a dictionary. """ - self.connection = connection self.secret_ref = secret_dict.get('secret_ref') self.created = parse_isotime(secret_dict.get('created')) self.status = secret_dict.get('status') @@ -60,3 +65,66 @@ class Secret(object): self.payload_content_encoding, self.bit_length, self.algorithm, self.cypher_type, self.expiration) ) + + +class SecretManager(base.BaseEntityManager): + + def __init__(self, api): + super(SecretManager, self).__init__(api, 'secrets') + + def create(self, + name=None, + payload=None, + payload_content_type=None, + payload_content_encoding=None, + algorithm=None, + bit_length=None, + mode=None, + expiration=None): + """ + Stores a new secret in Barbican + + :param name: A friendly name for the secret + :param payload: The unencrypted secret data + :param payload_content_type: The format/type of the secret data + :param payload_content_encoding: The encoding of the secret data + :param algorithm: The algorithm barbican should use to encrypt + :param bit_length: The bit length of the key used for ecnryption + :param mode: The algorithm mode (e.g. CBC or CTR mode) + :param expiration: The expiration time of the secret in ISO 8601 format + :returns: Secret ID for the stored secret + """ + LOG.debug("Creating secret of payload content type {0}".format( + payload_content_type)) + href = self.entity + LOG.debug("href: {0}".format(href)) + + secret_dict = dict() + secret_dict['name'] = name + secret_dict['payload'] = payload + secret_dict['payload_content_type'] = payload_content_type + secret_dict['payload_content_encoding'] = payload_content_encoding + secret_dict['algorithm'] = algorithm + #TODO(dmend): Change this to 'mode' + secret_dict['cypher_type'] = mode + secret_dict['bit_length'] = bit_length + secret_dict['expiration'] = expiration + self._remove_empty_keys(secret_dict) + + LOG.debug("Request body: {0}".format(secret_dict)) + + resp = self.api.post(self.entity, secret_dict) + #TODO(dmend): return secret object? + #secret = Secret(resp) + secret_id = resp['secret_ref'].split('/')[-1] + + return secret_id + + def list(self, limit=10, offset=0): + + LOG.debug('Listing secrets - offset {0} limit {1}'.format(offset, + limit)) + params = {'limit': limit, 'offset': offset} + resp = self.api.get(self.entity, params) + + return resp diff --git a/barbicanclient/test/common/test_auth.py b/barbicanclient/test/common/test_auth.py index 8376d1b0..d91e9701 100644 --- a/barbicanclient/test/common/test_auth.py +++ b/barbicanclient/test/common/test_auth.py @@ -18,12 +18,6 @@ from barbicanclient.common import auth class WhenTestingKeystoneAuthentication(unittest.TestCase): - def setUp(self): - self.keystone = auth.KeystoneAuth(endpoint='endpoint_url', - username='user', - password='password', - tenant_name='demo') - def test_endpoint_username_password_tenant_are_required(self): with self.assertRaises(ValueError): keystone = auth.KeystoneAuth() diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index 8ef1e6bb..418dba9b 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -29,10 +29,6 @@ class WhenTestingClient(unittest.TestCase): self.user = 'user' self.password = 'password' self.tenant = 'tenant' - self.keystone = auth.KeystoneAuth(endpoint=self.auth_endpoint, - username=self.user, - password=self.password, - tenant_name=self.tenant) self.key = 'key' self.endpoint = 'http://localhost:9311/v1/' @@ -66,17 +62,7 @@ class WhenTestingClient(unittest.TestCase): authenticate=self.authenticate, request=self.request, endpoint=self.endpoint, - auth=False) - - def test_authenticated_client_requires_endpoint_user_pw_tenant(self): - with self.assertRaises(ValueError): - c = client.Client(auth=True) - with self.assertRaises(ValueError): - c = client.Client() # default auth=True - c=client.Client(auth_endpoint=self.auth_endpoint, user=self.user, - password=self.password, tenant=self.tenant, - #TODO(dmend): remove authenticate below - authenticate=self.authenticate) + tenant_id='test_tenant') def test_should_connect_with_token(self): self.assertFalse(self.authenticate.called) @@ -88,7 +74,8 @@ class WhenTestingClient(unittest.TestCase): key=self.key, tenant=self.tenant, authenticate=self.authenticate, - endpoint=self.endpoint) + endpoint=self.endpoint, + tenant_id='test_tenant') self.authenticate\ .assert_called_once_with(self.auth_endpoint, self.user, @@ -116,7 +103,8 @@ class WhenTestingClient(unittest.TestCase): token=self.auth_token, authenticate=self.authenticate, request=self.request, - endpoint=self.endpoint) + endpoint=self.endpoint, + tenant_id='test_tenant') def test_should_create_secret(self): body = {'status': "ACTIVE", From 1890f4b760a86cad5c747206e5091b4755228387 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Sun, 1 Sep 2013 23:54:26 -0500 Subject: [PATCH 08/26] Added get and delete to secret manager --- barbicanclient/client.py | 50 ++++----------------------- barbicanclient/secrets.py | 71 ++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 72 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 4955f8ac..b7c197e7 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -149,49 +149,6 @@ class Client(object): self._token = value self._session.headers['X-Auth-Token'] = value - def list_secrets(self, limit=10, offset=0): - """ - Returns a tuple containing three items: a list of secrets pertaining - to the given offset and limit, a reference to the previous set of - secrets, and a reference to the next set of secrets. Either of the - references may be None. - - :param limit: The limit to the number of secrets to list - :param offset: The offset from the beginning to start listing - """ - LOG.debug(_("Listing secrets - offset: {0}, limit: {1}").format(offset, - limit)) - href = "{0}/{1}?limit={2}&offset={3}".format(self._tenant, - self.SECRETS_PATH, - limit, offset) - return self.list_secrets_by_href(href) - - def list_secrets_by_href(self, href): - """ - Returns a tuple containing three items: a list of secrets pertaining - to the offset and limit within href, a reference to the previous set - of secrets, and a reference to the next set of secrets. Either of the - references may be None. - - :param href: The full secrets URI - """ - LOG.debug(_("Listing secrets by href")) - LOG.debug("href: {0}".format(href)) - if href is None: - return [], None, None - - hdrs, body = self._perform_http(href=href, method='GET') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - secrets_dict = body['secrets'] - secrets = [Secret(self._conn, s) for s in secrets_dict] - - prev_ref = body.get('previous') - - next_ref = body.get('next') - - return secrets, prev_ref, next_ref - def create_secret(self, name=None, payload=None, @@ -437,7 +394,7 @@ class Client(object): def _request(self, url, method, headers): resp = self._session.request() - def get(self, path, params): + def get(self, path, params=None): url = '{0}/{1}/'.format(self.base_url, path) headers = {'content-type': 'application/json'} resp = self._session.get(url, params=params, headers=headers) @@ -451,6 +408,11 @@ class Client(object): self._check_status_code(resp) return resp.json() + def delete(self, path): + url = '{0}/{1}/'.format(self.base_url, path) + resp = self._session.delete(url) + self._check_status_code(resp) + #TODO(dmend): beef this up def _check_status_code(self, resp): status = resp.status_code diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index c0529402..81862543 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -10,9 +10,8 @@ LOG = logging.getLogger(__name__) class Secret(object): - """ - A secret is any data the user has stored in the key management system. + Secrets are used to keep track of the data stored in Barbican. """ def __init__(self, secret_dict): @@ -20,59 +19,52 @@ class Secret(object): Builds a secret object from a dictionary. """ self.secret_ref = secret_dict.get('secret_ref') - self.created = parse_isotime(secret_dict.get('created')) + self.name = secret_dict.get('name') self.status = secret_dict.get('status') - self.algorithm = secret_dict.get('algorithm') - self.bit_length = secret_dict.get('bit_length') - self.payload_content_type = secret_dict.get('payload_content_type') - self.payload_content_encoding = secret_dict.get( - 'payload_content_encoding') - - self.cypher_type = secret_dict.get('cypher_type') - self.name = secret_dict.get('name') - + self.created = parse_isotime(secret_dict.get('created')) if secret_dict.get('expiration') is not None: self.expiration = parse_isotime(secret_dict['expiration']) else: self.expiration = None - if secret_dict.get('updated') is not None: self.updated = parse_isotime(secret_dict['updated']) else: self.updated = None - self._id = urlparse(self.secret_ref).path.split('/').pop() + self.algorithm = secret_dict.get('algorithm') + self.bit_length = secret_dict.get('bit_length') + self.mode = secret_dict.get('cypher_type') - @property - def id(self): - return self._id + self.content_types = secret_dict.get('content_types') + self.id = urlparse(self.secret_ref).path.split('/').pop() def __str__(self): return ("Secret - ID: {0}\n" - " reference: {1}\n" + " href: {1}\n" " name: {2}\n" " created: {3}\n" " status: {4}\n" - " payload content type: {5}\n" - " payload content encoding: {6}\n" + " content types: {5}\n" + " algorithm: {6}\n" " bit length: {7}\n" - " algorithm: {8}\n" - " cypher type: {9}\n" - " expiration: {10}\n" + " mode: {8}\n" + " expiration: {9}\n" .format(self.id, self.secret_ref, self.name, self.created, - self.status, self.payload_content_type, - self.payload_content_encoding, self.bit_length, - self.algorithm, self.cypher_type, self.expiration) + self.status, self.content_types, self.algorithm, + self.bit_length, self.mode, self.expiration) ) + def __repr__(self): + return 'Secret(name="{0}")'.format(self.name) + class SecretManager(base.BaseEntityManager): def __init__(self, api): super(SecretManager, self).__init__(api, 'secrets') - def create(self, + def store(self, name=None, payload=None, payload_content_type=None, @@ -120,6 +112,29 @@ class SecretManager(base.BaseEntityManager): return secret_id + def get(self, secret_id): + """ + Returns a Secret object with information about the secret. + + :param secret_id: The UUID of the secret + """ + if not secret_id: + raise ValueError('secret_id is required.') + path = '{0}/{1}'.format(self.entity, secret_id) + resp = self.api.get(path) + return Secret(resp) + + def delete(self, secret_id): + """ + Deletes a secret + + :param secret_id: The UUID of the secret + """ + if not secret_id: + raise ValueError('secret_id is required.') + path = '{0}/{1}'.format(self.entity, secret_id) + self.api.delete(path) + def list(self, limit=10, offset=0): LOG.debug('Listing secrets - offset {0} limit {1}'.format(offset, @@ -127,4 +142,4 @@ class SecretManager(base.BaseEntityManager): params = {'limit': limit, 'offset': offset} resp = self.api.get(self.entity, params) - return resp + return [Secret(s) for s in resp['secrets']] From 9a2e5a47b9ca85e33322c22a0ea5dc80281878c5 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 00:31:54 -0500 Subject: [PATCH 09/26] Move raw secret get to secret manager --- barbicanclient/client.py | 90 +++------------------------------------ barbicanclient/secrets.py | 14 ++++++ 2 files changed, 21 insertions(+), 83 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index b7c197e7..a0f1d847 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -149,88 +149,6 @@ class Client(object): self._token = value self._session.headers['X-Auth-Token'] = value - def create_secret(self, - name=None, - payload=None, - payload_content_type=None, - payload_content_encoding=None, - algorithm=None, - bit_length=None, - cypher_type=None, - expiration=None): - """Deprecated""" - self.secrets.create(name=name, - payload=payload, - payload_content_type=payload_content_type, - payload_content_encoding=payload_content_encoding, - algorithm=algorithm, - bit_length=bit_length, - mode=cypher_type, - expiration=expiration) - - def delete_secret_by_id(self, secret_id): - """ - Deletes a secret - - :param secret_id: The UUID of the secret - """ - href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id) - LOG.info(_("Deleting secret - Secret ID: {0}").format(secret_id)) - return self.delete_secret(href) - - def delete_secret(self, href): - """ - Deletes a secret - - :param href: The full URI of the secret - """ - hdrs, body = self._perform_http(href=href, method='DELETE') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - def get_secret_by_id(self, secret_id): - """ - Returns a Secret object - - :param secret_id: The UUID of the secret - """ - LOG.debug(_("Getting secret - Secret ID: {0}").format(secret_id)) - href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id) - return self.get_secret(href) - - def get_secret(self, href): - """ - Returns a Secret object - - :param href: The full URI of the secret - """ - hdrs, body = self._perform_http(href=href, method='GET') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - return Secret(self._conn, body) - - def get_raw_secret_by_id(self, secret_id, payload_content_type): - """ - Returns the raw secret - - :param secret_id: The UUID of the secret - :param payload_content_type: The data type of the secret - """ - LOG.debug(_("Getting raw secret - Secret ID: {0}").format(secret_id)) - href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id) - return self.get_raw_secret(href, payload_content_type) - - def get_raw_secret(self, href, payload_content_type): - """ - Returns the raw secret - - :param href: The reference to the secret - :param payload_content_type: The data type of the secret - """ - hdrs = {"Accept": payload_content_type} - hdrs, body = self._perform_http(href=href, method='GET', headers=hdrs, - parse_json=False) - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - return body - def list_orders(self, limit=10, offset=0): """ Returns a tuple containing three items: a list of orders pertaining @@ -396,11 +314,17 @@ class Client(object): def get(self, path, params=None): url = '{0}/{1}/'.format(self.base_url, path) - headers = {'content-type': 'application/json'} + headers = {'Accept': 'application/json'} resp = self._session.get(url, params=params, headers=headers) self._check_status_code(resp) return resp.json() + def get_raw(self, path, headers): + url = '{0}/{1}/'.format(self.base_url, path) + resp = self._session.get(url, headers=headers) + self._check_status_code(resp) + return resp.content + def post(self, path, data): url = '{0}/{1}/'.format(self.base_url, path) headers = {'content-type': 'application/json'} diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 81862543..7009cf90 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -124,6 +124,20 @@ class SecretManager(base.BaseEntityManager): resp = self.api.get(path) return Secret(resp) + def raw(self, secret_id, content_type): + """ + Returns the actual secret data stored in Barbican. + + :param secret_id: The UUID of the secret + :param content_type: The content_type of the secret + :returns: secret data + """ + if not all([secret_id, content_type]): + raise ValueError('secret_id and content_type are required.') + path = '{0}/{1}'.format(self.entity, secret_id) + headers = {'Accept': content_type} + return self.api.get_raw(path, headers) + def delete(self, secret_id): """ Deletes a secret From 8eab396a90af84af537826d0fdff7249481a68b3 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 09:22:34 -0500 Subject: [PATCH 10/26] Moved order crud to order manager --- barbicanclient/client.py | 120 +------------------------------- barbicanclient/orders.py | 142 +++++++++++++++++++++++++++++++------- barbicanclient/secrets.py | 34 ++++++--- 3 files changed, 143 insertions(+), 153 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index a0f1d847..3cfc16ac 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -4,6 +4,7 @@ import urlparse import requests +from barbicanclient import orders from barbicanclient import secrets from barbicanclient.secrets import Secret from barbicanclient.orders import Order @@ -66,6 +67,7 @@ class Client(object): self.base_url = '{0}/{1}'.format(self._barbican_url, self._tenant_id) self.secrets = secrets.SecretManager(self) + self.orders = orders.OrderManager(self) # self.env = kwargs.get('fake_env') or env @@ -149,124 +151,6 @@ class Client(object): self._token = value self._session.headers['X-Auth-Token'] = value - def list_orders(self, limit=10, offset=0): - """ - Returns a tuple containing three items: a list of orders pertaining - to the given offset and limit, a reference to the previous set of - orders, and a reference to the next set of orders. Either of the - references may be None. - - :param limit: The limit to the number of orders to list - :param offset: The offset from the beginning to start listing - """ - LOG.debug(_("Listing orders - offset: {0}, limit: {1}").format(offset, - limit)) - href = "{0}/{1}?limit={2}&offset={3}".format(self._tenant, - self.ORDERS_PATH, - limit, offset) - return self.list_orders_by_href(href) - - def list_orders_by_href(self, href): - """ - Returns a tuple containing three items: a list of orders pertaining - to the offset and limit within href, a reference to the previous set - of orders, and a reference to the next set of orders. Either of the - references may be None. - - :param href: The full orders URI - """ - LOG.debug(_("Listing orders by href")) - LOG.debug("href: {0}".format(href)) - if href is None: - return [], None, None - - hdrs, body = self._perform_http(href=href, method='GET') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - orders_dict = body['orders'] - orders = [Order(self._conn, o) for o in orders_dict] - - prev_ref = body.get('previous') - - next_ref = body.get('next') - - return orders, prev_ref, next_ref - - def create_order(self, - name=None, - payload_content_type='application/octet-stream', - algorithm='aes', - bit_length=256, - cypher_type='cbc', - expiration=None): - """ - Creates and returns an Order object with all of its metadata filled in. - - :param name: A friendly name for the secret - :param algorithm: The algorithm the secret is used with - :param bit_length: The bit length of the secret - :param cypher_type: The cypher type (e.g. block cipher mode) - :param expiration: The expiration time of the secret in ISO 8601 format - """ - LOG.debug(_("Creating order")) - href = "{0}/{1}".format(self._tenant, self.ORDERS_PATH) - LOG.debug("href: {0}".format(href)) - order_dict = {'secret': {}} - order_dict['secret']['name'] = name - order_dict['secret'][ - 'payload_content_type'] = payload_content_type - order_dict['secret']['algorithm'] = algorithm - order_dict['secret']['bit_length'] = bit_length - order_dict['secret']['cypher_type'] = cypher_type - order_dict['secret']['expiration'] = expiration - self._remove_empty_keys(order_dict['secret']) - LOG.debug(_("Request body: {0}").format(order_dict['secret'])) - hdrs, body = self._perform_http(href=href, - method='POST', - request_body=json.dumps(order_dict)) - - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - return self.get_order(body['order_ref']) - - def delete_order_by_id(self, order_id): - """ - Deletes an order - - :param order_id: The UUID of the order - """ - LOG.info(_("Deleting order - Order ID: {0}").format(order_id)) - href = "{0}/{1}/{2}".format(self._tenant, self.ORDERS_PATH, order_id) - return self.delete_order(href) - - def delete_order(self, href): - """ - Deletes an order - - :param href: The full URI of the order - """ - hdrs, body = self._perform_http(href=href, method='DELETE') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - - def get_order_by_id(self, order_id): - """ - Returns an Order object - - :param order_id: The UUID of the order - """ - LOG.debug(_("Getting order - Order ID: {0}").format(order_id)) - href = "{0}/{1}/{2}".format(self._tenant, self.ORDERS_PATH, order_id) - return self.get_order(href) - - def get_order(self, href): - """ - Returns an Order object - - :param href: The full URI of the order - """ - hdrs, body = self._perform_http(href=href, method='GET') - LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) - return Order(self._conn, body) def _perform_http(self, method, href, request_body='', headers={}, parse_json=True): diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index c1171845..51512d5e 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -1,47 +1,141 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 urlparse import urlparse + +from openstack.common.gettextutils import _ +from openstack.common import log as logging from openstack.common.timeutils import parse_isotime +from barbicanclient import base +from barbicanclient import secrets + + +LOG = logging.getLogger(__name__) + class Order(object): - def __init__(self, connection, order_dict): + def __init__(self, order_dict): """ Builds an order object from a json representation. Includes the connection object for subtasks. """ - self.connection = connection - self.secret = order_dict['secret'] self.order_ref = order_dict['order_ref'] - self.created = parse_isotime(order_dict['created']) - self.secret_ref = order_dict.get('secret_ref') self.status = order_dict.get('status') - + self.created = parse_isotime(order_dict['created']) if order_dict.get('updated') is not None: self.updated = parse_isotime(order_dict['updated']) else: self.updated = None + secret_dict = order_dict['secret'] + #TODO(dmend): This is a hack because secret_ref is in different + # spots. Secret will be missing content_types also. + # Maybe we should fetch the secret for this? + secret_dict.update({'secret_ref': order_dict['secret_ref'], + 'created': order_dict['created']}) + self.secret = secrets.Secret(secret_dict) - self._id = urlparse(self.order_ref).path.split('/').pop() - - @property - def id(self): - return self._id - - def get_secret(self): - return self.connection.get_secret(self.secret_ref) - - def save(self): - self.connection.update_order(self) - - def delete(self): - self.connection.delete_order(self) + self.id = urlparse(self.order_ref).path.split('/').pop() def __str__(self): return ("Order - ID: {0}\n" - " order reference: {1}\n" - " secret reference: {2}\n" + " order href: {1}\n" + " secret href: {2}\n" " created: {3}\n" " status: {4}\n" - .format(self.id, self.order_ref, self.secret_ref, self.created, - self.status) + .format(self.id, self.order_ref, self.secret.secret_ref, + self.created, self.status) ) + + def __repr__(self): + return 'Order(id="{0}", secret=Secret(id="{1}")'.format( + self.id, self.secret.id + ) + + +class OrderManager(base.BaseEntityManager): + + def __init__(self, api): + super(OrderManager, self).__init__(api, 'orders') + + def create(self, + name=None, + payload_content_type='application/octet-stream', + algorithm=None, + bit_length=None, + mode=None, + expiration=None): + """ + Creates a new Order in Barbican + + :param name: A friendly name for the + :param payload_content_type: The format/type of the secret data + :param algorithm: The algorithm the secret is used with + :param bit_length: The bit length of the secret + :param mode: The algorithm mode (e.g. CBC or CTR mode) + :param expiration: The expiration time of the secret in ISO 8601 + format + :returns: Order ID for the created order + """ + LOG.debug(_("Creating order")) + + order_dict = {'secret': {}} + order_dict['secret']['name'] = name + order_dict['secret'][ + 'payload_content_type'] = payload_content_type + order_dict['secret']['algorithm'] = algorithm + order_dict['secret']['bit_length'] = bit_length + #TODO(dmend): Change this to mode + order_dict['secret']['cypher_type'] = mode + order_dict['secret']['expiration'] = expiration + self._remove_empty_keys(order_dict['secret']) + + LOG.debug(_("Request body: {0}").format(order_dict['secret'])) + + resp = self.api.post(self.entity, order_dict) + #TODO(dmend): return order object? + order_id = resp['order_ref'].split('/')[-1] + + return order_id + + def get(self, order_id): + """ + Returns an Order object + + :param order_id: The UUID of the order + """ + LOG.debug(_("Getting order - Order ID: {0}").format(order_id)) + if not order_id: + raise ValueError('order_id is required.') + path = '{0}/{1}'.format(self.entity, order_id) + resp = self.api.get(path) + return Order(resp) + + def delete(self, order_id): + """ + Deletes an order + + :param order_id: The UUID of the order + """ + if not order_id: + raise ValueError('order_id is required.') + path = '{0}/{1}'.format(self.entity, order_id) + self.api.delete(path) + + def list(self, limit=10, offset=0): + params = {'limit': limit, 'offset': offset} + resp = self.api.get(self.entity, params) + + return [Order(o) for o in resp['orders']] diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 7009cf90..59cba8fe 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -1,3 +1,17 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 urlparse import urlparse from openstack.common import log as logging @@ -65,16 +79,16 @@ class SecretManager(base.BaseEntityManager): super(SecretManager, self).__init__(api, 'secrets') def store(self, - name=None, - payload=None, - payload_content_type=None, - payload_content_encoding=None, - algorithm=None, - bit_length=None, - mode=None, - expiration=None): + name=None, + payload=None, + payload_content_type=None, + payload_content_encoding=None, + algorithm=None, + bit_length=None, + mode=None, + expiration=None): """ - Stores a new secret in Barbican + Stores a new Secret in Barbican :param name: A friendly name for the secret :param payload: The unencrypted secret data @@ -88,8 +102,6 @@ class SecretManager(base.BaseEntityManager): """ LOG.debug("Creating secret of payload content type {0}".format( payload_content_type)) - href = self.entity - LOG.debug("href: {0}".format(href)) secret_dict = dict() secret_dict['name'] = name From aa793528c366c613b926df8ddc69dce004e0ce54 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 09:48:02 -0500 Subject: [PATCH 11/26] Client.py cleanup --- barbicanclient/client.py | 175 ++++++--------------------------------- 1 file changed, 25 insertions(+), 150 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 3cfc16ac..5fafaf34 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -1,46 +1,48 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 json import os -import urlparse import requests +from barbicanclient.openstack.common import log as logging +from barbicanclient.openstack.common.gettextutils import _ from barbicanclient import orders from barbicanclient import secrets -from barbicanclient.secrets import Secret -from barbicanclient.orders import Order -from barbicanclient.common import auth -from barbicanclient.openstack.common import log -from barbicanclient.common.exceptions import ClientException -from barbicanclient.openstack.common.gettextutils import _ -from urlparse import urljoin -LOG = log.getLogger(__name__) -log.setup('barbicanclient') +LOG = logging.getLogger(__name__) +logging.setup('barbicanclient') class Client(object): - SECRETS_PATH = 'secrets' - ORDERS_PATH = 'orders' def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None, **kwargs): """ - Authenticate and connect to the service endpoint, which can be - received through authentication. - - Environment variables will be used by default when their corresponding - arguments are not passed in. + Barbican client object used to interact with barbican service. :param auth_plugin: Authentication backend plugin defaults to None - :param endpoint: Barbican endpoint url - - :param key: The API key or password to auth with - :keyword param endpoint: The barbican endpoint to connect to - default: env('BARBICAN_ENDPOINT') + :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. """ - LOG.debug(_("Creating Client object")) self._session = requests.Session() @@ -69,133 +71,6 @@ class Client(object): self.secrets = secrets.SecretManager(self) self.orders = orders.OrderManager(self) - # self.env = kwargs.get('fake_env') or env - - # #TODO(dmend): remove these - # self._auth_endpoint = kwargs.get('auth_endpoint') or self.env('OS_AUTH_URL') - # self._user = kwargs.get('user') or self.env('OS_USERNAME') - # self._tenant = kwargs.get('tenant') or self.env('OS_TENANT_NAME') - # self._key = kwargs.get('key') - - # if not all([self._auth_endpoint, self._user, self._key, self._tenant]): - # raise ClientException("The authorization endpoint, username, key," - # " and tenant name should either be passed i" - # "n or defined as environment variables.") - # self.authenticate = kwargs.get('authenticate') or auth.authenticate - # self.request = kwargs.get('request') or requests.request - # self._endpoint = (kwargs.get('endpoint') or - # self.env('BARBICAN_ENDPOINT')) - # self._cacert = kwargs.get('cacert') - # self.connect(token=(kwargs.get('token') or self.env('AUTH_TOKEN'))) - - @property - def _conn(self): - """Property to enable decorators to work properly""" - return self - - @property - def auth_endpoint(self): - """The fully-qualified URI of the auth endpoint""" - return self._auth_endpoint - - @property - def endpoint(self): - """The fully-qualified URI of the endpoint""" - return self._endpoint - - @endpoint.setter - def endpoint(self, value): - self._endpoint = value - - def connect(self, token=None): - """ - Establishes a connection. If token is not None or empty, it will be - used for this connection, and authentication will not take place. - - :param token: An authentication token - """ - - LOG.debug(_("Establishing connection")) - - self._session = requests.Session() - - # headers = {"Client-Id": self._client_id} - # self._session.headers.update(headers) - self._session.verify = True - - if token: - self.auth_token = token - else: - LOG.debug(_("Authenticating token")) - endpoint, self.auth_token = self.authenticate( - self._auth_endpoint, - self._user, - self._key, - self._tenant, - service_type='key-store', - endpoint=self._endpoint, - cacert=self._cacert - ) - if self.endpoint is None: - self.endpoint = endpoint - - @property - def auth_token(self): - try: - return self._session.headers['X-Auth-Token'] - except KeyError: - return None - - @auth_token.setter - def auth_token(self, value): - self._token = value - self._session.headers['X-Auth-Token'] = value - - - def _perform_http(self, method, href, request_body='', headers={}, - parse_json=True): - """ - Perform an HTTP operation, checking for appropriate - errors, etc. and returns the response - - Returns the headers and body as a tuple. - - :param method: The http method to use (GET, PUT, etc) - :param body: The optional body to submit - :param headers: Any additional headers to submit - :param parse_json: Whether the response body should be parsed as json - """ - if not isinstance(request_body, str): - request_body = json.dumps(request_body) - - if not self.endpoint.endswith('/'): - self.endpoint += '/' - - url = urljoin(self.endpoint, href) - - headers['X-Auth-Token'] = self.auth_token - - response = self.request(method=method, url=url, data=request_body, - headers=headers) - # Check if the status code is 2xx class - if not response.ok: - LOG.error('Bad response: {0}'.format(response.status_code)) - raise ClientException(href=href, method=method, - http_status=response.status_code, - http_response_content=response.content) - - if response.content and parse_json is True: - resp_body = json.loads(response.content) - elif response.content and parse_json is False: - resp_body = response.content - else: - resp_body = '' - - return response.headers, resp_body - - def _request(self, url, method, headers): - resp = self._session.request() - def get(self, path, params=None): url = '{0}/{1}/'.format(self.base_url, path) headers = {'Accept': 'application/json'} From d0ff07d0974dcb6a11a10865fbb3782029375305 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 13:42:21 -0500 Subject: [PATCH 12/26] Update keep to use refactored client --- barbicanclient/client.py | 3 +- barbicanclient/keep.py | 138 ++++++++++++++++++++++----------------- 2 files changed, 78 insertions(+), 63 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 5fafaf34..ba886255 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -29,8 +29,7 @@ logging.setup('barbicanclient') class Client(object): - def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None, - **kwargs): + def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None): """ Barbican client object used to interact with barbican service. diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index 5bd81254..2783f8f9 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -1,5 +1,20 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 argparse +from barbicanclient.common import auth from barbicanclient import client @@ -15,32 +30,36 @@ class Keep: self.add_list_args() def get_main_parser(self): - parser = argparse.ArgumentParser(description='Access the Barbican' - ' key management sevice.') + parser = argparse.ArgumentParser( + description='Access the Barbican key management sevice.' + ) parser.add_argument('type', choices=["order", "secret"], help="type to operate on") - parser.add_argument('--auth_endpoint', '-A', - default=client.env('OS_AUTH_URL'), - help='the URL to authenticate against (default: ' - '%(default)s)') - parser.add_argument('--user', '-U', default=client.env('OS_USERNAME'), - help='the user to authenticate as (default: %(de' - 'fault)s)') + auth_group = parser.add_mutually_exclusive_group() + auth_group.add_argument('--no_auth', '-N', action='store_true', + help='Do not use authentication') + auth_group.add_argument('--auth_url', '-A', + default=client.env('OS_AUTH_URL'), + help='the URL used for authentication ' + '(default: %(default)s)') + parser.add_argument('--username', '-U', default=client.env('OS_USERNAME'), + help='the user for authentication ' + '(default: %(default)s)') parser.add_argument('--password', '-P', default=client.env('OS_PASSWORD'), - help='the API key or password to authenticate with' + help='the password for authentication' ' (default: %(default)s)') - parser.add_argument('--tenant', '-T', + parser.add_argument('--tenant_name', '-T', default=client.env('OS_TENANT_NAME'), - help='the tenant ID (default: %(default)s)') + help='the tenant name for authentication ' + '(default: %(default)s)') + parser.add_argument('--tenant_id', '-I', + help='the tenant ID for context ') parser.add_argument('--endpoint', '-E', default=client.env('BARBICAN_ENDPOINT'), - help='the URL of the barbican server (default: %' - '(default)s)') - parser.add_argument('--token', '-K', - default=client.env('AUTH_TOKEN'), help='the au' - 'thentication token (default: %(default)s)') + help='the URL of the barbican server (default: ' + '%(default)s)') return parser def add_create_args(self): @@ -55,9 +74,9 @@ class Keep: help='the bit length of the secret; used ' 'only for reference (default: %(default)s)', type=int) - create_parser.add_argument('--cypher_type', '-c', default="cbc", - help='the cypher type; used only for refere' - 'nce (default: %(default)s)') + create_parser.add_argument('--mode', '-m', default="cbc", + help='the algorithmm mode; used only for ' + 'reference (default: %(default)s)') create_parser.add_argument('--payload', '-p', help='the unencrypted' ' secret; if provided, you must also provid' 'e a payload_content_type (only used for se' @@ -110,70 +129,67 @@ class Keep: list_parser.add_argument('--offset', '-o', default=0, help='specify t' 'he page offset (default: %(default)s)', type=int) - list_parser.add_argument('--URI', '-u', help='the full reference to ' - 'what is to be listed; put in quotes to avoid' - ' backgrounding when \'&\' is in the URI') - list_parser.set_defaults(func=self.lst) + list_parser.set_defaults(func=self.list) def create(self, args): if args.type == 'secret': - secret = self.conn.create_secret(args.name, - args.payload, - args.payload_content_type, - args.payload_content_encoding, - args.algorithm, - args.bit_length, - args.cypher_type, - args.expiration) + secret = self.client.secrets.store(args.name, + args.payload, + args.payload_content_type, + args.payload_content_encoding, + args.algorithm, + args.bit_length, + args.mode, + args.expiration) print secret else: - order = self.conn.create_order(args.name, - args.payload_content_type, - args.algorithm, - args.bit_length, - args.cypher_type, - args.expiration) + order = self.client.orders.create(args.name, + args.payload_content_type, + args.algorithm, + args.bit_length, + args.mode, + args.expiration) print order def delete(self, args): if args.type == 'secret': - self.conn.delete_secret_by_id(args.UUID) + self.client.secret.delete(args.UUID) else: - self.conn.delete_order_by_id(args.UUID) + self.client.orders.delete(args.UUID) def get(self, args): if args.type == 'secret': if args.raw: - print self.conn.get_raw_secret_by_id(args.UUID, - args.payload_content_type) + print self.client.secrets.raw(args.UUID, + args.payload_content_type) else: - print self.conn.get_secret_by_id(args.UUID) + print self.client.secrets.get(args.UUID) else: - print self.conn.get_order_by_id(args.UUID) + print self.client.orers.get(args.UUID) - def lst(self, args): + def list(self, args): if args.type == 'secret': - if args.URI: - l = self.conn.list_secrets_by_href(args.URI) - else: - l = self.conn.list_secrets(args.limit, args.offset) + ls = self.client.secrets.list(args.limit, args.offset) else: - if args.URI: - l = self.conn.list_orders_by_href(args.URI) - else: - l = self.conn.list_orders(args.limit, args.offset) - for i in l[0]: - print i - print '{0}s displayed: {1} - offset: {2}'.format(args.type, len(l[0]), + ls = self.client.orders.list(args.limit, args.offset) + for obj in ls: + print obj + print '{0}s displayed: {1} - offset: {2}'.format(args.type, len(ls), args.offset) def execute(self, **kwargs): args = self.parser.parse_args(kwargs.get('argv')) - self.conn = client.Client(args.auth_endpoint, args.user, - args.password, args.tenant, - args.token, - endpoint=args.endpoint) - + if args.no_auth: + self.client = client.Client(endpoint=args.endpoint, + tenant_id=args.tenant_id) + else: + self._keystone = auth.KeystoneAuth(auth_url=args.auth_url, + username=args.username, + password=args.password, + tenant_name=args.tenant_name) + self.client = client.Client(auth_plugin=self._keystone, + endpoint=args.endpoint, + tenant_id=args.tenant_id) args.func(args) From 3c9f3cdf52c19fc23af59bb6366fcc56bcc0cfe1 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 16:38:14 -0500 Subject: [PATCH 13/26] Add store command to keep. Rename options to match other openstack clients. --- barbicanclient/keep.py | 170 ++++++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 70 deletions(-) diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index 2783f8f9..29859a86 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -12,6 +12,9 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Command-line interface to the Barbican API. +""" import argparse from barbicanclient.common import auth @@ -25,93 +28,118 @@ class Keep: description= 'Action to perform') self.add_create_args() - self.add_delete_args() + self._add_store_args() self.add_get_args() self.add_list_args() + self.add_delete_args() def get_main_parser(self): parser = argparse.ArgumentParser( - description='Access the Barbican key management sevice.' + description=__doc__.strip() ) - parser.add_argument('type', - choices=["order", "secret"], - help="type to operate on") + parser.add_argument('command', + choices=['order', 'secret'], + help='Entity used for command.') auth_group = parser.add_mutually_exclusive_group() - auth_group.add_argument('--no_auth', '-N', action='store_true', + auth_group.add_argument('--no-auth', '-N', action='store_true', help='Do not use authentication') - auth_group.add_argument('--auth_url', '-A', + auth_group.add_argument('--os-auth-url', '-A', + metavar='', default=client.env('OS_AUTH_URL'), - help='the URL used for authentication ' - '(default: %(default)s)') - parser.add_argument('--username', '-U', default=client.env('OS_USERNAME'), - help='the user for authentication ' - '(default: %(default)s)') - parser.add_argument('--password', '-P', + help='Defaults to env[OS_AUTH_URL].') + parser.add_argument('--os-username', '-U', + metavar='', + default=client.env('OS_USERNAME'), + help='Defaults to env[OS_USERNAME].') + parser.add_argument('--os-password', '-P', + metavar='', default=client.env('OS_PASSWORD'), - help='the password for authentication' - ' (default: %(default)s)') - parser.add_argument('--tenant_name', '-T', + help='Defaults to env[OS_PASSWORD].') + parser.add_argument('--os-tenant-name', '-T', + metavar='', default=client.env('OS_TENANT_NAME'), - help='the tenant name for authentication ' - '(default: %(default)s)') - parser.add_argument('--tenant_id', '-I', - help='the tenant ID for context ') + help='Defaults to env[OS_TENANT_NAME].') + parser.add_argument('--os-tenant-id', '-I', + metavar='', + default=client.env('OS_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID].') parser.add_argument('--endpoint', '-E', + metavar='', default=client.env('BARBICAN_ENDPOINT'), - help='the URL of the barbican server (default: ' - '%(default)s)') + help='Defaults to env[BARBICAN_ENDPOINT].') return parser def add_create_args(self): - create_parser = self.subparsers.add_parser('create', help='Create a ' - 'secret or an order') + create_parser = self.subparsers.add_parser('create', + help='Create a new order.') create_parser.add_argument('--name', '-n', - help='a human-friendly name') - create_parser.add_argument('--algorithm', '-a', default='aes', help='t' - 'he algorithm; used only for reference (def' - 'ault: %(default)s)') - create_parser.add_argument('--bit_length', '-b', default=256, - help='the bit length of the secret; used ' - 'only for reference (default: %(default)s)', + help='a human-friendly name.') + create_parser.add_argument('--algorithm', '-a', default='aes', + help='the algorithm (default: %(default)s).') + create_parser.add_argument('--bit-length', '-b', default=256, + help='the bit length ' + '(default: %(default)s).', type=int) - create_parser.add_argument('--mode', '-m', default="cbc", + create_parser.add_argument('--mode', '-m', default='cbc', help='the algorithmm mode; used only for ' 'reference (default: %(default)s)') - create_parser.add_argument('--payload', '-p', help='the unencrypted' - ' secret; if provided, you must also provid' - 'e a payload_content_type (only used for se' - 'crets)') - create_parser.add_argument('--payload_content_type', '-t', - help='the type/format of the provided ' - 'secret data; "text/plain" is assumed to be' - ' UTF-8; required when --payload is su' - 'pplied and when creating orders') - create_parser.add_argument('--payload_content_encoding', '-d', - help='required if --payload_content_type is' - ' "application/octet-stream" (only used for' - ' secrets)') - + create_parser.add_argument('--payload-content-type', '-t', + help='the type/format of the secret to be' + ' generated.') create_parser.add_argument('--expiration', '-e', help='the expiration ' - 'time for the secret in ISO 8601 format') + 'time for the secret in ISO 8601 format.') create_parser.set_defaults(func=self.create) + def _add_store_args(self): + store_parser = self.subparsers.add_parser( + 'store', + help='Store a secret in barbican.' + ) + store_parser.add_argument('--name', '-n', + help='a human-friendly name.') + store_parser.add_argument('--payload', '-p', help='the unencrypted' + ' secret; if provided, you must also provide' + ' a payload_content_type') + store_parser.add_argument('--payload-content-type', '-t', + help='the type/format of the provided ' + 'secret data; "text/plain" is assumed to be' + ' UTF-8; required when --payload is' + ' supplied.') + store_parser.add_argument('--payload-content-encoding', '-d', + help='required if --payload-content-type is' + ' "application/octet-stream".') + store_parser.add_argument('--algorithm', '-a', default='aes', + help='the algorithm (default: %(default)s).') + store_parser.add_argument('--bit-length', '-b', default=256, + help='the bit length ' + '(default: %(default)s).', + type=int) + store_parser.add_argument('--mode', '-m', default='cbc', + help='the algorithmm mode; used only for ' + 'reference (default: %(default)s)') + store_parser.add_argument('--expiration', '-e', help='the expiration ' + 'time for the secret in ISO 8601 format.') + store_parser.set_defaults(func=self.store) + def add_delete_args(self): - delete_parser = self.subparsers.add_parser('delete', help='Delete a se' - 'cret or an order by provid' - 'ing its UUID') + delete_parser = self.subparsers.add_parser( + 'delete', + help='Delete a secret or an order by providing its UUID.' + ) delete_parser.add_argument('UUID', help='the universally unique identi' 'fier of the the secret or order') delete_parser.set_defaults(func=self.delete) def add_get_args(self): - get_parser = self.subparsers.add_parser('get', help='Retrieve a secret' - ' or an order by providing its' - ' UUID.') + get_parser = self.subparsers.add_parser( + 'get', + help='Retrieve a secret or an order by providing its UUID.' + ) get_parser.add_argument('UUID', help='the universally unique identi' - 'fier of the the secret or order') + 'fier of the the secret or order.') get_parser.add_argument('--raw', '-r', help='if specified, gets the ra' 'w secret of type specified with --payload_con' - 'tent_type (only used for secrets)', + 'tent_type (only used for secrets).', action='store_true') get_parser.add_argument('--payload_content_type', '-t', default='text/plain', @@ -131,18 +159,20 @@ class Keep: type=int) list_parser.set_defaults(func=self.list) - def create(self, args): - if args.type == 'secret': + def store(self, args): + if args.command == 'secret': secret = self.client.secrets.store(args.name, - args.payload, - args.payload_content_type, - args.payload_content_encoding, - args.algorithm, - args.bit_length, - args.mode, - args.expiration) + args.payload, + args.payload_content_type, + args.payload_content_encoding, + args.algorithm, + args.bit_length, + args.mode, + args.expiration) print secret - else: + + def create(self, args): + if args.command == 'order': order = self.client.orders.create(args.name, args.payload_content_type, args.algorithm, @@ -152,29 +182,29 @@ class Keep: print order def delete(self, args): - if args.type == 'secret': + if args.command == 'secret': self.client.secret.delete(args.UUID) else: self.client.orders.delete(args.UUID) def get(self, args): - if args.type == 'secret': + if args.command == 'secret': if args.raw: print self.client.secrets.raw(args.UUID, args.payload_content_type) else: print self.client.secrets.get(args.UUID) else: - print self.client.orers.get(args.UUID) + print self.client.orders.get(args.UUID) def list(self, args): - if args.type == 'secret': + if args.command == 'secret': ls = self.client.secrets.list(args.limit, args.offset) else: ls = self.client.orders.list(args.limit, args.offset) for obj in ls: print obj - print '{0}s displayed: {1} - offset: {2}'.format(args.type, len(ls), + print '{0}s displayed: {1} - offset: {2}'.format(args.command, len(ls), args.offset) def execute(self, **kwargs): From 0c132168604633a1ab1eaf4981a5a163afcebaca Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 18:13:17 -0500 Subject: [PATCH 14/26] Clean up imports and update requirements --- barbicanclient/client.py | 1 + barbicanclient/common/auth.py | 26 ++++++++++++++------------ barbicanclient/orders.py | 15 +++++++-------- barbicanclient/secrets.py | 12 ++++++------ setup.py | 16 +++++++++------- tools/pip-requires | 8 +++----- tools/test-requires | 17 ++++------------- 7 files changed, 44 insertions(+), 51 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index ba886255..f7e476f4 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -99,6 +99,7 @@ class Client(object): def _check_status_code(self, resp): status = resp.status_code print('status {0}'.format(status)) + print('body {0}'.format(resp.content)) def env(*vars, **kwargs): diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index 205d91dd..6017626e 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -1,19 +1,21 @@ - -from exceptions import ClientException - +# Copyright (c) 2013 Rackspace, Inc. +# +# 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 keystoneclient.v2_0 import client as ksclient from keystoneclient import exceptions -def authenticate(auth_url, user, key, tenant, **kwargs): - #TODO(dmend): remove this method - keystone = KeystoneAuth(auth_url=auth_url, - username=user, - password=key, - tenant_name=tenant) - return keystone.barbican_url, keysotone.auth_token - - class AuthException(Exception): """Raised when authorization fails.""" def __init__(self, message): diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index 51512d5e..2f2ef4c8 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -12,13 +12,12 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from urlparse import urlparse - -from openstack.common.gettextutils import _ -from openstack.common import log as logging -from openstack.common.timeutils import parse_isotime +import urlparse from barbicanclient import base +from barbicanclient.openstack.common.gettextutils import _ +from barbicanclient.openstack.common import log as logging +from barbicanclient.openstack.common import timeutils from barbicanclient import secrets @@ -34,9 +33,9 @@ class Order(object): """ self.order_ref = order_dict['order_ref'] self.status = order_dict.get('status') - self.created = parse_isotime(order_dict['created']) + self.created = timeutils.parse_isotime(order_dict['created']) if order_dict.get('updated') is not None: - self.updated = parse_isotime(order_dict['updated']) + self.updated = timeutils.parse_isotime(order_dict['updated']) else: self.updated = None secret_dict = order_dict['secret'] @@ -47,7 +46,7 @@ class Order(object): 'created': order_dict['created']}) self.secret = secrets.Secret(secret_dict) - self.id = urlparse(self.order_ref).path.split('/').pop() + self.id = urlparse.urlparse(self.order_ref).path.split('/').pop() def __str__(self): return ("Order - ID: {0}\n" diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 59cba8fe..2d369d38 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -12,12 +12,11 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from urlparse import urlparse - -from openstack.common import log as logging -from openstack.common.timeutils import parse_isotime +import urlparse from barbicanclient import base +from barbicanclient.openstack.common import log as logging +from barbicanclient.openstack.common.timeutils import parse_isotime LOG = logging.getLogger(__name__) @@ -51,7 +50,7 @@ class Secret(object): self.mode = secret_dict.get('cypher_type') self.content_types = secret_dict.get('content_types') - self.id = urlparse(self.secret_ref).path.split('/').pop() + self.id = urlparse.urlparse(self.secret_ref).path.split('/').pop() def __str__(self): return ("Secret - ID: {0}\n" @@ -97,7 +96,8 @@ class SecretManager(base.BaseEntityManager): :param algorithm: The algorithm barbican should use to encrypt :param bit_length: The bit length of the key used for ecnryption :param mode: The algorithm mode (e.g. CBC or CTR mode) - :param expiration: The expiration time of the secret in ISO 8601 format + :param expiration: The expiration time of the secret in ISO 8601 + format :returns: Secret ID for the stored secret """ LOG.debug("Creating secret of payload content type {0}".format( diff --git a/setup.py b/setup.py index 86b4fd64..96584ec0 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ import os import setuptools + name = 'python-barbicanclient' @@ -46,15 +47,16 @@ setuptools.setup( keywords="openstack encryption key-management secret", url='https://github.com/cloudkeep/barbican', license='Apache License (2.0)', - author='OpenStack, LLC.', - author_email='openstack-admins@lists.launchpad.net', - packages=setuptools.find_packages(exclude=['tests', 'tests.*', 'examples', 'examples.*']), + author='Rackspace, Inc.', + author_email='openstack-dev@lists.openstack.org', + packages=setuptools.find_packages( + exclude=['tests', 'tests.*', 'examples', 'examples.*'] + ), install_requires=[ - 'eventlet>=0.12.1', - 'httplib2>=0.7.7', 'argparse>=1.2.1', - 'python-keystoneclient>=0.2.3', - 'iso8601>=0.1.4' + 'eventlet>=0.13.0', + 'requests>=1.2.3', + 'python-keystoneclient>=0.3.2', ], test_suite='nose.collector', tests_require=['nose'], diff --git a/tools/pip-requires b/tools/pip-requires index 9f26f417..3b29530b 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,6 +1,4 @@ -httplib2>=0.7.7 argparse>=1.2.1 -python-keystoneclient>=0.2.3 -eventlet>=0.12.1 -oslo.config>=1.1.0 -iso8601>=0.1.4 \ No newline at end of file +eventlet>=0.13.0 +requests>=1.2.3 +python-keystoneclient>=0.3.2 diff --git a/tools/test-requires b/tools/test-requires index c06b5c5e..3c5872c1 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,15 +1,6 @@ -pep8>=1.4.5 -pyflakes>=0.7.2 -flake8>=2.0 -hacking>=0.5.3 - -coverage -discover +hacking>=0.7.0 mock>=1.0.1 -sphinx>=1.1.2 - -nose>=1.2.1 -nosexcover>=1.0.7 -openstack.nose_plugin>=0.11 +nose>=1.3.0 +nosexcover>=1.0.8 +tox>=1.6.0 unittest2>=0.5.1 -tox From 1375dfe9fc81622ab9d80dd2be0435fe74a5be6c Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Mon, 2 Sep 2013 22:35:37 -0500 Subject: [PATCH 15/26] Update README.md --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f962b79..0be31c02 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,76 @@ -python-barbicanclient -===================== +# python-barbicanclient -This is a client for the [Barbican](https://github.com/cloudkeep/barbican) +This is a client for the [Barbican](https://github.com/stackforge/barbican) Key Management API. There is a Python library for accessing the API (`barbicanclient` module), and a command-line script (`keep`). + +## barbicanclient - Python API +The full api is documented in the wiki. + +### Quickstart +Store a secret in barbican using keystone for authentication: +```python +>>> from barbicanclient.common import auth +>>> from barbicanclient import client +# We'll use keystone for authentication +>>> keystone = auth.KeystoneAuth(auth_url='http://keystone-int.cloudkeep.io:5000/v2.0', +... username=USER, password=PASSWORD, tenant_name=TENANT) +>>> barbican = client.Client(auth_plugin=keystone) +# Let's store some sensitive data, Barbican encrypts it and stores it securely in the cloud +>>> secret_id = barbican.secrets.store(name='Self destruction sequence', +... payload='the magic words are squeamish ossifrage', +... payload_content_type='text/plain') +# Let's look at some properties of a barbican Secret +>>> secret = barbican.secrets.get(secret_id) +>>> print(secret.id) +d46883b4-e072-4452-98c9-36d652dfcdd6 +>>> print(secret.name) +Self destruction sequence +# Now let's retrieve the secret payload. Barbican decrypts it and sends it back. +>>> print(barbican.secrets.raw(secret.id, secret.content_types['default'])) +the magic words are squeamish ossifrage +``` + +## keep - Command Line Client + + +``` +$ keep -h +usage: keep [-h] [--no-auth | --os-auth-url ] + [--os-username ] [--os-password ] + [--os-tenant-name ] [--os-tenant-id ] + [--endpoint ] + {order,secret} {create,store,get,list,delete} ... + +Command-line interface to the Barbican API. + +positional arguments: + {order,secret} Entity used for command. + +optional arguments: + -h, --help show this help message and exit + --no-auth, -N Do not use authentication + --os-auth-url , -A + Defaults to env[OS_AUTH_URL]. + --os-username , -U + Defaults to env[OS_USERNAME]. + --os-password , -P + Defaults to env[OS_PASSWORD]. + --os-tenant-name , -T + Defaults to env[OS_TENANT_NAME]. + --os-tenant-id , -I + Defaults to env[OS_TENANT_ID]. + --endpoint , -E + Defaults to env[BARBICAN_ENDPOINT]. + +subcommands: + Action to perform + + {create,store,get,list,delete} + create Create a new order. + store Store a secret in barbican. + get Retrieve a secret or an order by providing its UUID. + list List secrets or orders + delete Delete a secret or an order by providing its UUID. + +``` From d783b4408771a9c3698a76ddfc0523e7e732ad02 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Tue, 3 Sep 2013 23:00:23 -0500 Subject: [PATCH 16/26] Raise exceptions for error responses from the server --- barbicanclient/client.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index f7e476f4..24f14592 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -27,6 +27,27 @@ LOG = logging.getLogger(__name__) logging.setup('barbicanclient') +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 + + class Client(object): def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None): @@ -95,11 +116,18 @@ class Client(object): resp = self._session.delete(url) self._check_status_code(resp) - #TODO(dmend): beef this up def _check_status_code(self, resp): status = resp.status_code - print('status {0}'.format(status)) - print('body {0}'.format(resp.content)) + LOG.debug('Response status {0}'.format(status)) + if status == 401: + LOG.error('Auth error: {0}'.format(resp.content)) + raise HTTPAuthError('{0}'.format(resp.content)) + if status >=500: + LOG.error('5xx Server error: {0}'.format(resp.content)) + raise HTTPServerError('{0}'.format(resp.content)) + if status >=400: + LOG.error('4xx Client error: {0}'.format(resp.content)) + raise HTTPClientError('{0}'.format(resp.content)) def env(*vars, **kwargs): From a8ba12f67337044595cdd6ac5e9cb6ce55d0403c Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 00:19:30 -0500 Subject: [PATCH 17/26] Update Client to use hrefs instead of parsing IDs --- barbicanclient/client.py | 19 ++++------ barbicanclient/orders.py | 66 ++++++++++++--------------------- barbicanclient/secrets.py | 77 +++++++++++++++++---------------------- 3 files changed, 65 insertions(+), 97 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 24f14592..c7159809 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -91,16 +91,14 @@ class Client(object): self.secrets = secrets.SecretManager(self) self.orders = orders.OrderManager(self) - def get(self, path, params=None): - url = '{0}/{1}/'.format(self.base_url, path) + def get(self, href, params=None): headers = {'Accept': 'application/json'} - resp = self._session.get(url, params=params, headers=headers) + resp = self._session.get(href, params=params, headers=headers) self._check_status_code(resp) return resp.json() - def get_raw(self, path, headers): - url = '{0}/{1}/'.format(self.base_url, path) - resp = self._session.get(url, headers=headers) + def get_raw(self, href, headers): + resp = self._session.get(href, headers=headers) self._check_status_code(resp) return resp.content @@ -111,9 +109,8 @@ class Client(object): self._check_status_code(resp) return resp.json() - def delete(self, path): - url = '{0}/{1}/'.format(self.base_url, path) - resp = self._session.delete(url) + def delete(self, href): + resp = self._session.delete(href) self._check_status_code(resp) def _check_status_code(self, resp): @@ -122,10 +119,10 @@ class Client(object): if status == 401: LOG.error('Auth error: {0}'.format(resp.content)) raise HTTPAuthError('{0}'.format(resp.content)) - if status >=500: + if status >= 500: LOG.error('5xx Server error: {0}'.format(resp.content)) raise HTTPServerError('{0}'.format(resp.content)) - if status >=400: + if status >= 400: LOG.error('4xx Client error: {0}'.format(resp.content)) raise HTTPClientError('{0}'.format(resp.content)) diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index 2f2ef4c8..fb9bef8e 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -28,8 +28,7 @@ class Order(object): def __init__(self, order_dict): """ - Builds an order object from a json representation. Includes the - connection object for subtasks. + Builds an order object from a dictionary. """ self.order_ref = order_dict['order_ref'] self.status = order_dict.get('status') @@ -38,30 +37,19 @@ class Order(object): self.updated = timeutils.parse_isotime(order_dict['updated']) else: self.updated = None - secret_dict = order_dict['secret'] - #TODO(dmend): This is a hack because secret_ref is in different - # spots. Secret will be missing content_types also. - # Maybe we should fetch the secret for this? - secret_dict.update({'secret_ref': order_dict['secret_ref'], - 'created': order_dict['created']}) - self.secret = secrets.Secret(secret_dict) - - self.id = urlparse.urlparse(self.order_ref).path.split('/').pop() + self.secret_ref = order_dict.get('secret_ref') def __str__(self): - return ("Order - ID: {0}\n" - " order href: {1}\n" - " secret href: {2}\n" - " created: {3}\n" - " status: {4}\n" - .format(self.id, self.order_ref, self.secret.secret_ref, + return ("Order - order href: {0}\n" + " secret href: {1}\n" + " created: {2}\n" + " status: {3}\n" + .format(self.order_ref, self.secret.secret_ref, self.created, self.status) ) def __repr__(self): - return 'Order(id="{0}", secret=Secret(id="{1}")'.format( - self.id, self.secret.id - ) + return 'Order(order_ref={0})'.format(self.order_ref) class OrderManager(base.BaseEntityManager): @@ -79,14 +67,14 @@ class OrderManager(base.BaseEntityManager): """ Creates a new Order in Barbican - :param name: A friendly name for the + :param name: A friendly name for the secret :param payload_content_type: The format/type of the secret data - :param algorithm: The algorithm the secret is used with + :param algorithm: The algorithm the secret associated with :param bit_length: The bit length of the secret :param mode: The algorithm mode (e.g. CBC or CTR mode) :param expiration: The expiration time of the secret in ISO 8601 format - :returns: Order ID for the created order + :returns: Order href for the created order """ LOG.debug(_("Creating order")) @@ -96,42 +84,36 @@ class OrderManager(base.BaseEntityManager): 'payload_content_type'] = payload_content_type order_dict['secret']['algorithm'] = algorithm order_dict['secret']['bit_length'] = bit_length - #TODO(dmend): Change this to mode - order_dict['secret']['cypher_type'] = mode + order_dict['secret']['mode'] = mode order_dict['secret']['expiration'] = expiration self._remove_empty_keys(order_dict['secret']) LOG.debug(_("Request body: {0}").format(order_dict['secret'])) resp = self.api.post(self.entity, order_dict) - #TODO(dmend): return order object? - order_id = resp['order_ref'].split('/')[-1] + return resp['order_ref'] - return order_id - - def get(self, order_id): + def get(self, order_ref): """ Returns an Order object - :param order_id: The UUID of the order + :param order_ref: The href for the order """ - LOG.debug(_("Getting order - Order ID: {0}").format(order_id)) - if not order_id: - raise ValueError('order_id is required.') - path = '{0}/{1}'.format(self.entity, order_id) - resp = self.api.get(path) + LOG.debug(_("Getting order - Order href: {0}").format(secret_ref)) + if not order_ref: + raise ValueError('order_ref is required.') + resp = self.api.get(order_ref) return Order(resp) - def delete(self, order_id): + def delete(self, order_ref): """ Deletes an order - :param order_id: The UUID of the order + :param order_ref: The href for the order """ - if not order_id: - raise ValueError('order_id is required.') - path = '{0}/{1}'.format(self.entity, order_id) - self.api.delete(path) + if not order_ref: + raise ValueError('order_ref is required.') + self.api.delete(order_ref) def list(self, limit=10, offset=0): params = {'limit': limit, 'offset': offset} diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 2d369d38..4bdada65 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -34,6 +34,7 @@ class Secret(object): self.secret_ref = secret_dict.get('secret_ref') self.name = secret_dict.get('name') self.status = secret_dict.get('status') + self.content_types = secret_dict.get('content_types') self.created = parse_isotime(secret_dict.get('created')) if secret_dict.get('expiration') is not None: @@ -47,23 +48,19 @@ class Secret(object): self.algorithm = secret_dict.get('algorithm') self.bit_length = secret_dict.get('bit_length') - self.mode = secret_dict.get('cypher_type') - - self.content_types = secret_dict.get('content_types') - self.id = urlparse.urlparse(self.secret_ref).path.split('/').pop() + self.mode = secret_dict.get('mode') def __str__(self): - return ("Secret - ID: {0}\n" - " href: {1}\n" - " name: {2}\n" - " created: {3}\n" - " status: {4}\n" - " content types: {5}\n" - " algorithm: {6}\n" - " bit length: {7}\n" - " mode: {8}\n" - " expiration: {9}\n" - .format(self.id, self.secret_ref, self.name, self.created, + return ("Secret - href: {0}\n" + " name: {1}\n" + " created: {2}\n" + " status: {3}\n" + " content types: {4}\n" + " algorithm: {5}\n" + " bit length: {6}\n" + " mode: {7}\n" + " expiration: {8}\n" + .format(self.secret_ref, self.name, self.created, self.status, self.content_types, self.algorithm, self.bit_length, self.mode, self.expiration) ) @@ -93,12 +90,12 @@ class SecretManager(base.BaseEntityManager): :param payload: The unencrypted secret data :param payload_content_type: The format/type of the secret data :param payload_content_encoding: The encoding of the secret data - :param algorithm: The algorithm barbican should use to encrypt - :param bit_length: The bit length of the key used for ecnryption - :param mode: The algorithm mode (e.g. CBC or CTR mode) + :param algorithm: The algorithm associated with this secret key + :param bit_length: The bit length of this secret key + :param mode: The algorithm mode used with this secret key :param expiration: The expiration time of the secret in ISO 8601 format - :returns: Secret ID for the stored secret + :returns: Secret href for the stored secret """ LOG.debug("Creating secret of payload content type {0}".format( payload_content_type)) @@ -109,8 +106,7 @@ class SecretManager(base.BaseEntityManager): secret_dict['payload_content_type'] = payload_content_type secret_dict['payload_content_encoding'] = payload_content_encoding secret_dict['algorithm'] = algorithm - #TODO(dmend): Change this to 'mode' - secret_dict['cypher_type'] = mode + secret_dict['mode'] = mode secret_dict['bit_length'] = bit_length secret_dict['expiration'] = expiration self._remove_empty_keys(secret_dict) @@ -118,48 +114,41 @@ class SecretManager(base.BaseEntityManager): LOG.debug("Request body: {0}".format(secret_dict)) resp = self.api.post(self.entity, secret_dict) - #TODO(dmend): return secret object? - #secret = Secret(resp) - secret_id = resp['secret_ref'].split('/')[-1] + return resp['secret_ref'] - return secret_id - - def get(self, secret_id): + def get(self, secret_ref): """ Returns a Secret object with information about the secret. - :param secret_id: The UUID of the secret + :param secret_ref: The href for the secret """ - if not secret_id: - raise ValueError('secret_id is required.') - path = '{0}/{1}'.format(self.entity, secret_id) - resp = self.api.get(path) + if not secret_ref: + raise ValueError('secret_ref is required.') + resp = self.api.get(secret_ref) return Secret(resp) - def raw(self, secret_id, content_type): + def decrypt(self, secret_ref, content_type): """ Returns the actual secret data stored in Barbican. - :param secret_id: The UUID of the secret + :param secret_ref: The href for the secret :param content_type: The content_type of the secret :returns: secret data """ - if not all([secret_id, content_type]): - raise ValueError('secret_id and content_type are required.') - path = '{0}/{1}'.format(self.entity, secret_id) + if not all([secret_ref, content_type]): + raise ValueError('secret_ref and content_type are required.') headers = {'Accept': content_type} - return self.api.get_raw(path, headers) + return self.api.get_raw(secret_ref, headers) - def delete(self, secret_id): + def delete(self, secret_ref): """ Deletes a secret - :param secret_id: The UUID of the secret + :param secret_ref: The href for the secret """ - if not secret_id: - raise ValueError('secret_id is required.') - path = '{0}/{1}'.format(self.entity, secret_id) - self.api.delete(path) + if not secret_ref: + raise ValueError('secret_ref is required.') + self.api.delete(secret_ref) def list(self, limit=10, offset=0): From 8df8f8033806e7d1004667a10d388bed6f9b9483 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 11:50:10 -0500 Subject: [PATCH 18/26] Last of the changes to Client --- barbicanclient/base.py | 1 + barbicanclient/client.py | 31 +++++++++++++++++++++---------- barbicanclient/orders.py | 16 +++++++++++----- barbicanclient/secrets.py | 26 ++++++++++++++++++-------- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/barbicanclient/base.py b/barbicanclient/base.py index 449b07a9..0736e5f9 100644 --- a/barbicanclient/base.py +++ b/barbicanclient/base.py @@ -16,6 +16,7 @@ Base utilites to build API operation managers. """ + class BaseEntityManager(object): def __init__(self, api, entity): self.api = api diff --git a/barbicanclient/client.py b/barbicanclient/client.py index c7159809..7e77eb32 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -102,6 +102,10 @@ class Client(object): self._check_status_code(resp) return resp.content + def delete(self, href): + 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'} @@ -109,22 +113,29 @@ class Client(object): self._check_status_code(resp) return resp.json() - def delete(self, href): - resp = self._session.delete(href) - self._check_status_code(resp) - def _check_status_code(self, resp): status = resp.status_code LOG.debug('Response status {0}'.format(status)) if status == 401: - LOG.error('Auth error: {0}'.format(resp.content)) - raise HTTPAuthError('{0}'.format(resp.content)) + LOG.error('Auth error: {0}'.format(self._get_error_message(resp))) + raise HTTPAuthError('{0}'.format(self._get_error_message(resp))) if status >= 500: - LOG.error('5xx Server error: {0}'.format(resp.content)) - raise HTTPServerError('{0}'.format(resp.content)) + LOG.error('5xx Server error: {0}'.format( + self._get_error_message(resp) + )) + raise HTTPServerError('{0}'.format(self._get_error_message(resp))) if status >= 400: - LOG.error('4xx Client error: {0}'.format(resp.content)) - raise HTTPClientError('{0}'.format(resp.content)) + LOG.error('4xx Client error: {0}'.format( + self._get_error_message(resp) + )) + raise HTTPClientError('{0}'.format(self._get_error_message(resp))) + + def _get_error_message(self, resp): + try: + message = resp.json()['title'] + except ValueError: + message = resp.content + return message def env(*vars, **kwargs): diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index fb9bef8e..34ed191e 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -12,13 +12,10 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -import urlparse - from barbicanclient import base from barbicanclient.openstack.common.gettextutils import _ from barbicanclient.openstack.common import log as logging from barbicanclient.openstack.common import timeutils -from barbicanclient import secrets LOG = logging.getLogger(__name__) @@ -99,7 +96,7 @@ class OrderManager(base.BaseEntityManager): :param order_ref: The href for the order """ - LOG.debug(_("Getting order - Order href: {0}").format(secret_ref)) + LOG.debug(_("Getting order - Order href: {0}").format(order_ref)) if not order_ref: raise ValueError('order_ref is required.') resp = self.api.get(order_ref) @@ -116,7 +113,16 @@ class OrderManager(base.BaseEntityManager): self.api.delete(order_ref) def list(self, limit=10, offset=0): + """ + Lists all orders for the tenant + + :param limit: Max number of orders returned + :param offset: Offset orders to begin list + :returns: list of Order objects + """ + LOG.debug('Listing orders - offest {0} limit {1}').format(offset, limit) + href = '{0}/{1}'.format(self.api.base_url, self.entity) params = {'limit': limit, 'offset': offset} - resp = self.api.get(self.entity, params) + resp = self.api.get(href, params) return [Order(o) for o in resp['orders']] diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 4bdada65..f4dda80e 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -12,8 +12,6 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -import urlparse - from barbicanclient import base from barbicanclient.openstack.common import log as logging from barbicanclient.openstack.common.timeutils import parse_isotime @@ -118,7 +116,7 @@ class SecretManager(base.BaseEntityManager): def get(self, secret_ref): """ - Returns a Secret object with information about the secret. + Returns a Secret object with metadata about the secret. :param secret_ref: The href for the secret """ @@ -127,16 +125,21 @@ class SecretManager(base.BaseEntityManager): resp = self.api.get(secret_ref) return Secret(resp) - def decrypt(self, secret_ref, content_type): + def decrypt(self, secret_ref, content_type=None): """ Returns the actual secret data stored in Barbican. :param secret_ref: The href for the secret - :param content_type: The content_type of the secret + :param content_type: The content_type of the secret, if not + provided, the client will fetch the secret meta and use the + default content_type to decrypt the secret :returns: secret data """ - if not all([secret_ref, content_type]): - raise ValueError('secret_ref and content_type are required.') + if not secret_ref: + raise ValueError('secret_ref is required.') + if not content_type: + secret = self.get(secret_ref) + content_type = secret.content_types['default'] headers = {'Accept': content_type} return self.api.get_raw(secret_ref, headers) @@ -151,10 +154,17 @@ class SecretManager(base.BaseEntityManager): self.api.delete(secret_ref) def list(self, limit=10, offset=0): + """ + List all secrets for the tenant + :param limit: Max number of secrets returned + :param offset: Offset secrets to begin list + :returns: list of Secret metadata objects + """ LOG.debug('Listing secrets - offset {0} limit {1}'.format(offset, limit)) + href = '{0}/{1}'.format(self.api.base_url, self.entity) params = {'limit': limit, 'offset': offset} - resp = self.api.get(self.entity, params) + resp = self.api.get(href, params) return [Secret(s) for s in resp['secrets']] From 67aace449f270daf4f49ce55c79f348409a6e524 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 16:11:25 -0500 Subject: [PATCH 19/26] Update keep to use URIs instead of UUIDs --- barbicanclient/keep.py | 114 +++++++++++++++++++++++---------------- barbicanclient/orders.py | 4 +- 2 files changed, 69 insertions(+), 49 deletions(-) diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index 29859a86..f873df1d 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -23,26 +23,30 @@ from barbicanclient import client class Keep: def __init__(self): - self.parser = self.get_main_parser() - self.subparsers = self.parser.add_subparsers(title='subcommands', - description= - 'Action to perform') - self.add_create_args() + self.parser = self._get_main_parser() + self.subparsers = self.parser.add_subparsers( + title='subcommands', + metavar='', + description='Action to perform' + ) + self._add_create_args() self._add_store_args() - self.add_get_args() - self.add_list_args() - self.add_delete_args() + self._add_get_args() + self._add_list_args() + self._add_delete_args() - def get_main_parser(self): + def _get_main_parser(self): parser = argparse.ArgumentParser( description=__doc__.strip() ) parser.add_argument('command', + metavar='', choices=['order', 'secret'], - help='Entity used for command.') + help='Entity used for command, e.g.,' + ' order, secret.') auth_group = parser.add_mutually_exclusive_group() auth_group.add_argument('--no-auth', '-N', action='store_true', - help='Do not use authentication') + help='Do not use authentication.') auth_group.add_argument('--os-auth-url', '-A', metavar='', default=client.env('OS_AUTH_URL'), @@ -69,24 +73,26 @@ class Keep: help='Defaults to env[BARBICAN_ENDPOINT].') return parser - def add_create_args(self): + def _add_create_args(self): create_parser = self.subparsers.add_parser('create', help='Create a new order.') create_parser.add_argument('--name', '-n', help='a human-friendly name.') create_parser.add_argument('--algorithm', '-a', default='aes', - help='the algorithm (default: %(default)s).') + help='the algorithm to be used with the ' + 'requested key (default: %(default)s).') create_parser.add_argument('--bit-length', '-b', default=256, - help='the bit length ' - '(default: %(default)s).', + help='the bit length of the requested secret' + ' key (default: %(default)s).', type=int) create_parser.add_argument('--mode', '-m', default='cbc', - help='the algorithmm mode; used only for ' - 'reference (default: %(default)s)') + help='the algorithmm mode to be used with ' + 'the rquested key (default: %(default)s).') create_parser.add_argument('--payload-content-type', '-t', + default='application/octet-stream', help='the type/format of the secret to be' - ' generated.') - create_parser.add_argument('--expiration', '-e', help='the expiration ' + ' generated (default: %(default)s).') + create_parser.add_argument('--expiration', '-x', help='the expiration ' 'time for the secret in ISO 8601 format.') create_parser.set_defaults(func=self.create) @@ -105,7 +111,7 @@ class Keep: 'secret data; "text/plain" is assumed to be' ' UTF-8; required when --payload is' ' supplied.') - store_parser.add_argument('--payload-content-encoding', '-d', + store_parser.add_argument('--payload-content-encoding', '-e', help='required if --payload-content-type is' ' "application/octet-stream".') store_parser.add_argument('--algorithm', '-a', default='aes', @@ -117,37 +123,39 @@ class Keep: store_parser.add_argument('--mode', '-m', default='cbc', help='the algorithmm mode; used only for ' 'reference (default: %(default)s)') - store_parser.add_argument('--expiration', '-e', help='the expiration ' + store_parser.add_argument('--expiration', '-x', help='the expiration ' 'time for the secret in ISO 8601 format.') store_parser.set_defaults(func=self.store) - def add_delete_args(self): + def _add_delete_args(self): delete_parser = self.subparsers.add_parser( 'delete', - help='Delete a secret or an order by providing its UUID.' + help='Delete a secret or an order by providing its href.' ) - delete_parser.add_argument('UUID', help='the universally unique identi' - 'fier of the the secret or order') + delete_parser.add_argument('URI', help='The URI reference for the' + ' secret or order') delete_parser.set_defaults(func=self.delete) - def add_get_args(self): + def _add_get_args(self): get_parser = self.subparsers.add_parser( 'get', - help='Retrieve a secret or an order by providing its UUID.' + help='Retrieve a secret or an order by providing its URI.' ) - get_parser.add_argument('UUID', help='the universally unique identi' - 'fier of the the secret or order.') - get_parser.add_argument('--raw', '-r', help='if specified, gets the ra' - 'w secret of type specified with --payload_con' - 'tent_type (only used for secrets).', + get_parser.add_argument('URI', help='The URI reference for the secret' + ' or order.') + get_parser.add_argument('--decrypt', '-d', help='if specified, keep' + ' will retrieve the unencrypted secret data;' + ' the data type can be specified with' + ' --payload-content-type (only used for' + ' secrets).', action='store_true') get_parser.add_argument('--payload_content_type', '-t', default='text/plain', - help='the content type of the raw secret (defa' - 'ult: %(default)s; only used for secrets)') + help='the content type of the decrypted secret ' + '(default: %(default)s; only used for secrets)') get_parser.set_defaults(func=self.get) - def add_list_args(self): + def _add_list_args(self): list_parser = self.subparsers.add_parser('list', help='List secrets or orders') list_parser.add_argument('--limit', '-l', default=10, help='specify t' @@ -170,6 +178,9 @@ class Keep: args.mode, args.expiration) print secret + else: + self.parser.exit(status=1, message='ERROR: store is only supported' + ' for secrets\n') def create(self, args): if args.command == 'order': @@ -180,22 +191,25 @@ class Keep: args.mode, args.expiration) print order + else: + self.parser.exit(status=1, message='ERROR: create is only supported' + ' for orders\n') def delete(self, args): if args.command == 'secret': - self.client.secret.delete(args.UUID) + self.client.secret.delete(args.URI) else: - self.client.orders.delete(args.UUID) + self.client.orders.delete(args.URI) def get(self, args): if args.command == 'secret': - if args.raw: - print self.client.secrets.raw(args.UUID, + if args.decrypt: + print self.client.secrets.raw(args.URI, args.payload_content_type) else: - print self.client.secrets.get(args.UUID) + print self.client.secrets.get(args.URI) else: - print self.client.orders.get(args.UUID) + print self.client.orders.get(args.URI) def list(self, args): if args.command == 'secret': @@ -211,15 +225,21 @@ class Keep: args = self.parser.parse_args(kwargs.get('argv')) if args.no_auth: self.client = client.Client(endpoint=args.endpoint, - tenant_id=args.tenant_id) - else: - self._keystone = auth.KeystoneAuth(auth_url=args.auth_url, - username=args.username, - password=args.password, - tenant_name=args.tenant_name) + tenant_id=args.os_tenant_id) + elif all([args.os_auth_url, args.os_username, args.os_password, + args.os_tenant_name]): + self._keystone = auth.KeystoneAuth(auth_url=args.os_auth_url, + username=args.os_username, + password=args.os_password, + tenant_name=args.os_tenant_name) self.client = client.Client(auth_plugin=self._keystone, endpoint=args.endpoint, tenant_id=args.tenant_id) + else: + self.parser.exit( + status=1, + message='ERROR: please specify authentication credentials\n' + ) args.func(args) diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index 34ed191e..b7420048 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -41,7 +41,7 @@ class Order(object): " secret href: {1}\n" " created: {2}\n" " status: {3}\n" - .format(self.order_ref, self.secret.secret_ref, + .format(self.order_ref, self.secret_ref, self.created, self.status) ) @@ -120,7 +120,7 @@ class OrderManager(base.BaseEntityManager): :param offset: Offset orders to begin list :returns: list of Order objects """ - LOG.debug('Listing orders - offest {0} limit {1}').format(offset, limit) + LOG.debug('Listing orders - offest {0} limit {1}'.format(offset, limit)) href = '{0}/{1}'.format(self.api.base_url, self.entity) params = {'limit': limit, 'offset': offset} resp = self.api.get(href, params) From dfa9eba7ae9dad58d55152286cd7431f1ddb4bfe Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 16:22:18 -0500 Subject: [PATCH 20/26] Rename KeystoneAuth to KeystoneAuthV2 This makes it more explicit that we're using keystone authentication v2.0 --- barbicanclient/common/auth.py | 2 +- barbicanclient/keep.py | 10 ++++++---- barbicanclient/test/common/test_auth.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py index 6017626e..d49900a7 100644 --- a/barbicanclient/common/auth.py +++ b/barbicanclient/common/auth.py @@ -22,7 +22,7 @@ class AuthException(Exception): self.message = message -class KeystoneAuth(object): +class KeystoneAuthV2(object): def __init__(self, auth_url='', username='', password='', tenant_name='', tenant_id=''): if not all([auth_url, username, password, tenant_name or tenant_id]): diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index f873df1d..9f570c66 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -228,10 +228,12 @@ class Keep: tenant_id=args.os_tenant_id) elif all([args.os_auth_url, args.os_username, args.os_password, args.os_tenant_name]): - self._keystone = auth.KeystoneAuth(auth_url=args.os_auth_url, - username=args.os_username, - password=args.os_password, - tenant_name=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 + ) self.client = client.Client(auth_plugin=self._keystone, endpoint=args.endpoint, tenant_id=args.tenant_id) diff --git a/barbicanclient/test/common/test_auth.py b/barbicanclient/test/common/test_auth.py index d91e9701..640301ea 100644 --- a/barbicanclient/test/common/test_auth.py +++ b/barbicanclient/test/common/test_auth.py @@ -20,4 +20,4 @@ from barbicanclient.common import auth class WhenTestingKeystoneAuthentication(unittest.TestCase): def test_endpoint_username_password_tenant_are_required(self): with self.assertRaises(ValueError): - keystone = auth.KeystoneAuth() + keystone = auth.KeystoneAuthV2() From 5ec37db11ed0759b813c9312ff927489b413e75b Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 16:40:09 -0500 Subject: [PATCH 21/26] Update README.md --- README.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0be31c02..1e4c8a84 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Key Management API. There is a Python library for accessing the API (`barbicanclient` module), and a command-line script (`keep`). ## barbicanclient - Python API -The full api is documented in the wiki. +The full api is [documented in the wiki](https://github.com/cloudkeep/python-barbicanclient/wiki/Client-Usage). ### Quickstart Store a secret in barbican using keystone for authentication: @@ -13,43 +13,42 @@ Store a secret in barbican using keystone for authentication: >>> from barbicanclient.common import auth >>> from barbicanclient import client # We'll use keystone for authentication ->>> keystone = auth.KeystoneAuth(auth_url='http://keystone-int.cloudkeep.io:5000/v2.0', -... username=USER, password=PASSWORD, tenant_name=TENANT) +>>> keystone = auth.KeystoneAuthV2(auth_url='http://keystone-int.cloudkeep.io:5000/v2.0', +... username='USER', password='PASSWORD', tenant_name='TENANT') >>> barbican = client.Client(auth_plugin=keystone) # Let's store some sensitive data, Barbican encrypts it and stores it securely in the cloud ->>> secret_id = barbican.secrets.store(name='Self destruction sequence', -... payload='the magic words are squeamish ossifrage', -... payload_content_type='text/plain') +>>> secret_uri = barbican.secrets.store(name='Self destruction sequence', +... payload='the magic words are squeamish ossifrage', +... payload_content_type='text/plain') # Let's look at some properties of a barbican Secret ->>> secret = barbican.secrets.get(secret_id) ->>> print(secret.id) -d46883b4-e072-4452-98c9-36d652dfcdd6 +>>> secret = barbican.secrets.get(secret_uri) +>>> print(secret.secret_ref) +u'http://api-01-int.cloudkeep.io:9311/v1/test_tenant/secrets/49496a6d-c674-4384-b208-7cf4988f84ee' >>> print(secret.name) Self destruction sequence # Now let's retrieve the secret payload. Barbican decrypts it and sends it back. ->>> print(barbican.secrets.raw(secret.id, secret.content_types['default'])) +>>> print(barbican.secrets.decrypt(secret.secret_ref)) the magic words are squeamish ossifrage ``` ## keep - Command Line Client - +Command line client configuration and usage is [documented in the wiki](https://github.com/cloudkeep/python-barbicanclient/wiki/Command-Line-Client). ``` -$ keep -h usage: keep [-h] [--no-auth | --os-auth-url ] [--os-username ] [--os-password ] [--os-tenant-name ] [--os-tenant-id ] [--endpoint ] - {order,secret} {create,store,get,list,delete} ... + ... Command-line interface to the Barbican API. positional arguments: - {order,secret} Entity used for command. + Entity used for command, e.g., order, secret. optional arguments: -h, --help show this help message and exit - --no-auth, -N Do not use authentication + --no-auth, -N Do not use authentication. --os-auth-url , -A Defaults to env[OS_AUTH_URL]. --os-username , -U @@ -66,11 +65,10 @@ optional arguments: subcommands: Action to perform - {create,store,get,list,delete} + create Create a new order. store Store a secret in barbican. - get Retrieve a secret or an order by providing its UUID. + get Retrieve a secret or an order by providing its URI. list List secrets or orders - delete Delete a secret or an order by providing its UUID. - + delete Delete a secret or an order by providing its href. ``` From c322aece44916ba0f3231275a1a8bd487ec3b4a7 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Wed, 4 Sep 2013 17:06:48 -0500 Subject: [PATCH 22/26] pep-8 fixes. --- barbicanclient/client.py | 4 ++-- barbicanclient/keep.py | 16 +++++++++------- barbicanclient/orders.py | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 7e77eb32..81d2c6f9 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -79,8 +79,8 @@ class Client(object): 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 auth_plugin') + raise ValueError('Tenant ID must be provided, or must be' + ' available from the auth_plugin') if endpoint.endswith('/'): self._barbican_url = endpoint[:-1] else: diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py index 9f570c66..f81ff42d 100644 --- a/barbicanclient/keep.py +++ b/barbicanclient/keep.py @@ -80,10 +80,11 @@ class Keep: help='a human-friendly name.') create_parser.add_argument('--algorithm', '-a', default='aes', help='the algorithm to be used with the ' - 'requested key (default: %(default)s).') + 'requested key (default: ' + '%(default)s).') create_parser.add_argument('--bit-length', '-b', default=256, - help='the bit length of the requested secret' - ' key (default: %(default)s).', + help='the bit length of the requested' + ' secret key (default: %(default)s).', type=int) create_parser.add_argument('--mode', '-m', default='cbc', help='the algorithmm mode to be used with ' @@ -151,8 +152,9 @@ class Keep: action='store_true') get_parser.add_argument('--payload_content_type', '-t', default='text/plain', - help='the content type of the decrypted secret ' - '(default: %(default)s; only used for secrets)') + help='the content type of the decrypted' + ' secret (default: %(default)s; only used for' + ' secrets)') get_parser.set_defaults(func=self.get) def _add_list_args(self): @@ -192,8 +194,8 @@ class Keep: args.expiration) print order else: - self.parser.exit(status=1, message='ERROR: create is only supported' - ' for orders\n') + self.parser.exit(status=1, message='ERROR: create is only ' + 'supported for orders\n') def delete(self, args): if args.command == 'secret': diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index b7420048..27d52a59 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -120,7 +120,8 @@ class OrderManager(base.BaseEntityManager): :param offset: Offset orders to begin list :returns: list of Order objects """ - LOG.debug('Listing orders - offest {0} limit {1}'.format(offset, limit)) + LOG.debug('Listing orders - offest {0} limit {1}'.format(offset, + limit)) href = '{0}/{1}'.format(self.api.base_url, self.entity) params = {'limit': limit, 'offset': offset} resp = self.api.get(href, params) From 71ae37fef1e02e486997aaddd48ee5606c19ea7b Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 5 Sep 2013 13:13:42 -0500 Subject: [PATCH 23/26] Add total() to both secrets and orders. --- barbicanclient/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/barbicanclient/base.py b/barbicanclient/base.py index 0736e5f9..c0b07a48 100644 --- a/barbicanclient/base.py +++ b/barbicanclient/base.py @@ -26,3 +26,13 @@ class BaseEntityManager(object): for k in dictionary.keys(): if dictionary[k] is None: dictionary.pop(k) + + def total(self): + """ + Returns the toatl number of entities stored in Barbican. + """ + href = '{0}/{1}'.format(self.api.base_url, self.entity) + params ={'limit': 0, 'offset': 0} + resp = self.api.get(href, params) + + return resp['total'] From cfcb18bdba18f3cf1b9c94f0c8578a6a83de8a09 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 5 Sep 2013 16:12:13 -0500 Subject: [PATCH 24/26] Update unit tests for Client --- barbicanclient/base.py | 2 +- barbicanclient/test/test_client.py | 370 +++++------------------------ 2 files changed, 54 insertions(+), 318 deletions(-) diff --git a/barbicanclient/base.py b/barbicanclient/base.py index c0b07a48..ca157a13 100644 --- a/barbicanclient/base.py +++ b/barbicanclient/base.py @@ -32,7 +32,7 @@ class BaseEntityManager(object): Returns the toatl number of entities stored in Barbican. """ href = '{0}/{1}'.format(self.api.base_url, self.entity) - params ={'limit': 0, 'offset': 0} + params = {'limit': 0, 'offset': 0} resp = self.api.get(href, params) return resp['total'] diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index 418dba9b..ef929a20 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -15,7 +15,7 @@ import json -from mock import MagicMock +import mock import unittest2 as unittest from barbicanclient import client @@ -23,336 +23,72 @@ from barbicanclient.common import auth from barbicanclient.common.exceptions import ClientException +class FakeAuth(object): + def __init__(self, auth_token, barbican_url, tenant_name, tenant_id): + self.auth_token = auth_token + self.barbican_url = barbican_url + self.tenant_name = tenant_name + self.tenant_id = tenant_id + + class WhenTestingClient(unittest.TestCase): def setUp(self): self.auth_endpoint = 'https://localhost:5000/v2.0/' + self.auth_token = 'fake_auth_token' self.user = 'user' self.password = 'password' - self.tenant = 'tenant' + self.tenant_name = 'tenant' + self.tenant_id = 'tenant_id' - self.key = 'key' self.endpoint = 'http://localhost:9311/v1/' - self.auth_token = 'token' - self.href = 'http://localhost:9311/v1/12345/orders' - self.fake_env = MagicMock() - self.fake_env.return_value = None - self.authenticate = MagicMock() - self.authenticate.return_value = (self.endpoint, self.auth_token) - self.request = MagicMock() - self.request.return_value.content = json.dumps( - { - "secret_ref": "http://localhost:9311/None/secrets" - "/8502cea9-9d35-46d7-96f5-80e43905e4c5" - } - ) - self.request.return_value.headers = { - 'content-length': '92', - 'content-type': 'application/json; charset=utf-8', - 'location': 'http://localhost:9311/None/' - 'secrets/8502cea9-9d35-46d7-96f5-80e43905e4c5', - 'x-openstack-request-id': - 'req-6c19d09e-1167-445c-b435-d6b0818b59b9' - } - self.request.return_value.ok = True - self.client = client.Client(auth_endpoint=self.auth_endpoint, - user=self.user, - key=self.key, tenant=self.tenant, - token=self.auth_token, - authenticate=self.authenticate, - request=self.request, - endpoint=self.endpoint, - tenant_id='test_tenant') + self.fake_auth = FakeAuth(self.auth_token, self.endpoint, + self.tenant_name, self.tenant_id) - def test_should_connect_with_token(self): - self.assertFalse(self.authenticate.called) + 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) - def test_should_connect_without_token(self): - self.client = client.Client(auth=False, - auth_endpoint=self.auth_endpoint, - user=self.user, - key=self.key, - tenant=self.tenant, - authenticate=self.authenticate, - endpoint=self.endpoint, - tenant_id='test_tenant') - self.authenticate\ - .assert_called_once_with(self.auth_endpoint, - self.user, - self.key, - self.tenant, - service_type='key-store', - endpoint=self.endpoint, - cacert=None - ) - self.assertEqual(self.auth_token, self.client.auth_token) - self.assertEqual(self.auth_endpoint, self.client._auth_endpoint) - self.assertEqual(self.user, self.client._user) - self.assertEqual(self.key, self.client._key) - self.assertEqual(self.tenant, self.client._tenant) - self.assertEqual(self.endpoint, self.client._endpoint) + 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.auth_token) - def test_should_raise_for_bad_args(self): - with self.assertRaises(ClientException): - self.client = client.Client(auth=False, - auth_endpoint=None, - user=self.user, - key=self.key, - tenant=self.tenant, - fake_env=self.fake_env, - token=self.auth_token, - authenticate=self.authenticate, - request=self.request, - endpoint=self.endpoint, - tenant_id='test_tenant') + def test_error_thrown_when_no_auth_and_no_endpoint(self): + with self.assertRaises(ValueError): + c = client.Client(tenant_id=self.tenant_id) - def test_should_create_secret(self): - body = {'status': "ACTIVE", - 'updated': '2013-06-07T16:13:38.889857', - 'cypher_type': 'cbc', - 'name': 'test_secret', - 'algorithm': 'aes', - 'created': '2013-06-07T16:13:38.889851', - 'secret_ref': 'http://localhost:9311/v1/None/secrets/e6e7d' - 'b5e-3738-408e-aaba-05a7177cade5', - 'expiration': '2015-06-07T16:13:38.889851', - 'bit_length': 256, - 'payload_content_type': 'text/plain' - } + def test_error_thrown_when_no_auth_and_no_tenant_id(self): + with self.assertRaises(ValueError): + c = client.Client(endpoint=self.endpoint) - secret = client.Secret(self.client, body) - self.request.return_value.content = json.dumps(body) - created = self.client.create_secret(name='test_secret', - payload='Test secret', - algorithm='aes', - bit_length=256, - cypher_type='cbc', - expiration='2015-06-07T16:13' - ':38.889851', - payload_content_type= - 'text/plain') - self.assertTrue(self._are_equivalent(secret, created)) + def test_client_strips_trailing_slash_from_endpoint(self): + c = client.Client(endpoint=self.endpoint, tenant_id=self.tenant_id) + self.assertEqual(c._barbican_url, self.endpoint.strip('/')) - def test_should_create_order(self): - body = {"status": "ACTIVE", - "secret_ref": "http://localhost:9311/v1/12345/secrets/5706054" - "9-2fcf-46eb-92bb-bf49fcf5d089", - "updated": "2013-06-07T19:00:37.338386", - "created": "2013-06-07T19:00:37.298704", - "secret": { - 'cypher_type': 'cbc', - 'name': 'test_secret', - 'algorithm': 'aes', - 'created': '2013-06-07T16:13:38.889851', - 'expiration': '2015-06-07T16:13:38.889851', - 'bit_length': 256, - 'payload_content_type': 'application/octet-stream' - }, - "order_ref": "http://localhost:9311/v1/12345/orders/003f2b91-" - "2f53-4c0a-a0f3-33796671efc3" - } + 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)) - order = client.Order(self.client, body) - self.request.return_value.content = json.dumps(body) - created = self.client.create_order( - name='test_secret', - payload_content_type='application/octet-stream', - algorithm='aes', - bit_length=256, - cypher_type='cbc' - ) - self.assertTrue(self._are_equivalent(order, created)) + def test_should_raise_for_unauthorized_response(self): + resp = mock.MagicMock() + resp.status_code = 401 + c = client.Client(auth_plugin=self.fake_auth) + with self.assertRaises(client.HTTPAuthError): + c._check_status_code(resp) - def test_list_no_secrets(self): - body0 = {'secrets': []} - secrets = [] - self.request.return_value.content = json.dumps(body0) - secret_list, prev_ref, next_ref = self.client.list_secrets(0, 0) - self.assertTrue(self._are_equivalent(secrets, secret_list)) - self.assertIsNone(prev_ref) - self.assertIsNone(next_ref) + def test_should_raise_for_server_error(self): + resp = mock.MagicMock() + resp.status_code = 500 + c = client.Client(auth_plugin=self.fake_auth) + with self.assertRaises(client.HTTPServerError): + c._check_status_code(resp) - def test_list_single_secret(self): - limit = 1 - body1 = {'secrets': [{'status': 'ACTIVE', - 'content_types': {'default': 'text/plain'}, - 'updated': '2013-06-03T21:16:58.349230', - 'cypher_type': None, - 'name': 'test_1', - 'algorithm': None, - 'created': '2013-06-03T21:16:58.349222', - 'secret_ref': 'http://localhost:9311/v1/' - 'None/secrets/bbd2036f-730' - '7-4090-bbef-bbb6025e5e7b', - 'expiration': None, - 'bit_length': None, - 'mime_type': 'text/plain'}], - 'next': "{0}/{1}?limit={2}&offset={2}".format(self.client. - _tenant, - self.client. - SECRETS_PATH, - limit)} - secrets = [client.Secret(self.client, body1['secrets'][0])] - self.request.return_value.content = json.dumps(body1) - secret_list, prev_ref, next_ref = self.client.list_secrets(limit, 0) - self.assertTrue(self._are_equivalent(secrets, secret_list)) - self.assertIsNone(prev_ref) - self.assertEqual(body1['next'], next_ref) - - def test_list_multiple_secrets(self): - limit = 2 - body1 = {'secrets': [{'status': 'ACTIVE', - 'content_types': {'default': 'text/plain'}, - 'updated': '2013-06-03T21:16:58.349230', - 'cypher_type': None, - 'name': 'test_1', - 'algorithm': None, - 'created': '2013-06-03T21:16:58.349222', - 'secret_ref': 'http://localhost:9311/v1/' - 'None/secrets/bbd2036f-730' - '7-4090-bbef-bbb6025e5e7b', - 'expiration': None, - 'bit_length': None, - 'mime_type': 'text/plain'}], - 'previous': "{0}/{1}?limit={2}&offset={2}".format( - self.client._tenant, - self.client. - SECRETS_PATH, - limit)} - - body2 = body1 - body2['secrets'][0]['name'] = 'test_2' - body2['secrets'][0]['secret_ref'] = 'http://localhost:9311/v1/No'\ - + 'ne/secrets/bbd2036f-7307-'\ - + '4090-bbef-bbb6025eabcd' - body2['previous'] = 'http://localhost:9311/v1/None/secrets/19106'\ - + 'b6e-4ef1-48d1-8950-170c1a5838e1' - body2['next'] = None - - secrets = [client.Secret(self.client, b['secrets'][0]) - for b in (body1, body2)] - body2['secrets'].insert(0, body1['secrets'][0]) - self.request.return_value.content = json.dumps(body2) - secret_list, prev_ref, next_ref = self.client.list_secrets(limit, 1) - self.assertTrue(self._are_equivalent(secrets, secret_list)) - self.assertEqual(body2['previous'], prev_ref) - self.assertIsNone(next_ref) - - def test_list_no_orders(self): - body0 = {'orders': []} - orders = [] - self.request.return_value.content = json.dumps(body0) - order_list, prev_ref, next_ref = self.client.list_orders(0, 0) - self.assertTrue(self._are_equivalent(orders, order_list)) - self.assertIsNone(prev_ref) - self.assertIsNone(next_ref) - - def test_list_single_order(self): - limit = 1 - body1 = {'orders': [{'status': 'PENDING', - 'updated': '2013-06-05T15:15:30.904760', - 'created': '2013-06-05T15:15:30.904752', - 'order_ref': 'http://localhost:9311/v1/' - 'None/orders/9f651441-3ccd' - '-45b3-bc60-3051656d5168', - 'secret_ref': 'http://localhost:9311/' - 'v1/None/secrets/????', - 'secret': {'cypher_type': None, - 'name': 'test_1', - 'algorithm': None, - 'expiration': None, - 'bit_length': None, - 'mime_type': 'text/plain'}}], - 'next': "{0}/{1}?limit={2}&offset={2}".format(self.client. - _tenant, - self.client. - ORDERS_PATH, - limit)} - orders = [client.Order(self.client, body1['orders'][0])] - self.request.return_value.content = json.dumps(body1) - order_list, prev_ref, next_ref = self.client.list_orders(limit, 0) - self.assertTrue(self._are_equivalent(orders, order_list)) - self.assertIsNone(prev_ref) - self.assertEqual(body1['next'], next_ref) - - def test_list_multiple_orders(self): - limit = 2 - body1 = {'orders': [{'status': 'PENDING', - 'updated': '2013-06-05T15:15:30.904760', - 'created': '2013-06-05T15:15:30.904752', - 'order_ref': 'http://localhost:9311/v1/' - 'None/orders/9f651441-3ccd' - '-45b3-bc60-3051656d5168', - 'secret_ref': 'http://localhost:9311/' - 'v1/None/secrets/????', - 'secret': {'cypher_type': None, - 'name': 'test_1', - 'algorithm': None, - 'expiration': None, - 'bit_length': None, - 'mime_type': 'text/plain'}}], - 'previous': "{0}/{1}?limit={2}&offset={2}".format( - self.client._tenant, - self.client. - SECRETS_PATH, - limit)} - body2 = body1 - body2['orders'][0]['order_ref'] = 'http://localhost:9311/v1/No'\ - + 'ne/orders/9f651441-3ccd-4'\ - + '5b3-bc60-3051656382fj' - body2['orders'][0]['secret']['name'] = 'test_2' - - body2['orders'][0]['name'] = 'test_2' - body2['orders'][0]['secret_ref'] = 'http://localhost:9311/v1/No'\ - + 'ne/secrets/bbd2036f-7307-'\ - + '4090-bbef-bbb6025eabcd' - body2['previous'] = 'http://localhost:9311/v1/None/orders/19106'\ - + 'b6e-4ef1-48d1-8950-170c1a5838e1' - body2['next'] = None - - orders = [client.Order(self.client, b['orders'][0]) - for b in (body1, body2)] - body2['orders'].insert(0, body1['orders'][0]) - self.request.return_value.content = json.dumps(body2) - order_list, prev_ref, next_ref = self.client.list_orders(limit, 1) - self.assertTrue(self._are_equivalent(orders, order_list)) - self.assertEqual(body2['previous'], prev_ref) - self.assertIsNone(next_ref) - - def test_should_get_response(self): - self._setup_request() - headers, body = self.client._perform_http('GET', self.href) - self.assertEqual(self.request.return_value.headers, headers) - self.assertEqual(json.loads(self.request.return_value.content), body) - - def test_should_parse_json(self): - self._setup_request() - headers, body = self.client._perform_http('GET', self.href, - parse_json=True) - self.assertEqual(json.loads(self.request.return_value.content), body) - - def test_should_not_parse_json(self): - self._setup_request() - headers, body = self.client._perform_http('GET', self.href, - parse_json=False) - self.assertEqual(self.request.return_value.content, body) - - def test_should_raise_for_bad_response(self): - self._setup_request() - self.request.return_value.ok = False - self.request.return_value.status_code = 404 - with self.assertRaises(ClientException) as e: - self.client._perform_http('GET', self.href) - exception = e.exception - self.assertEqual(404, exception.http_status) - - def _setup_request(self): - self.request.return_value.headers = {'Accept': 'application/json'} - self.request.return_value.content = '{"test": "response"}' - self.href = 'http://localhost:9311/v1/12345/orders' - - def _are_equivalent(self, a, b): - if isinstance(a, list) and isinstance(b, list): - return all([self._are_equivalent(x, y) for x, y in zip(a, b)]) - else: - return (a.__dict__ == b.__dict__) + def test_should_raise_for_client_errors(self): + resp = mock.MagicMock() + resp.status_code = 400 + c = client.Client(auth_plugin=self.fake_auth) + with self.assertRaises(client.HTTPClientError): + c._check_status_code(resp) From 2c5e8e61f8bf5d1e04f645e637488b24efa5a135 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 5 Sep 2013 16:16:24 -0500 Subject: [PATCH 25/26] Remove unused exception. --- barbicanclient/common/exceptions.py | 15 --------------- barbicanclient/test/test_client.py | 1 - 2 files changed, 16 deletions(-) delete mode 100644 barbicanclient/common/exceptions.py diff --git a/barbicanclient/common/exceptions.py b/barbicanclient/common/exceptions.py deleted file mode 100644 index 49c5f6ab..00000000 --- a/barbicanclient/common/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -class ClientException(Exception): - """Exception for wrapping up Barbican client errors""" - def __init__(self, href='', http_status=0, - method='', http_response_content=''): - - self.method = method - self.href = href - self.http_status = http_status - self.http_response_content = http_response_content - - msg = "%s %s returned %d with msg: %s" % (self.method, - self.href, - self.http_status, - self.http_response_content) - Exception.__init__(self, msg) diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py index ef929a20..c4645685 100644 --- a/barbicanclient/test/test_client.py +++ b/barbicanclient/test/test_client.py @@ -20,7 +20,6 @@ import unittest2 as unittest from barbicanclient import client from barbicanclient.common import auth -from barbicanclient.common.exceptions import ClientException class FakeAuth(object): From ac5078433f237f2be09cb995c8440f61c6052335 Mon Sep 17 00:00:00 2001 From: Douglas Mendizabal Date: Thu, 5 Sep 2013 16:42:56 -0500 Subject: [PATCH 26/26] Version bump. --- barbicanclient/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/barbicanclient/version.py b/barbicanclient/version.py index 63159c1b..54b6a44b 100644 --- a/barbicanclient/version.py +++ b/barbicanclient/version.py @@ -17,5 +17,5 @@ Cloudkeep's Barbican Client version """ -__version__ = '0.3.0' +__version__ = '0.4.0' __version_info__ = tuple(__version__.split('.'))