Merge pull request #17 from dmend/master

Client refactor for M3
This commit is contained in:
John Wood
2013-09-05 14:43:19 -07:00
21 changed files with 882 additions and 1093 deletions

3
.gitignore vendored
View File

@@ -26,6 +26,9 @@ pip-log.txt
.tox
nosetests.xml
# pyenv
.python-version
# Translations
*.mo

View File

@@ -1,6 +1,74 @@
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](https://github.com/cloudkeep/python-barbicanclient/wiki/Client-Usage).
### 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.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_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_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.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).
```
usage: keep [-h] [--no-auth | --os-auth-url <auth-url>]
[--os-username <auth-user-name>] [--os-password <auth-password>]
[--os-tenant-name <auth-tenant-name>] [--os-tenant-id <tenant-id>]
[--endpoint <barbican-url>]
<entity> <action> ...
Command-line interface to the Barbican API.
positional arguments:
<entity> 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.
--os-auth-url <auth-url>, -A <auth-url>
Defaults to env[OS_AUTH_URL].
--os-username <auth-user-name>, -U <auth-user-name>
Defaults to env[OS_USERNAME].
--os-password <auth-password>, -P <auth-password>
Defaults to env[OS_PASSWORD].
--os-tenant-name <auth-tenant-name>, -T <auth-tenant-name>
Defaults to env[OS_TENANT_NAME].
--os-tenant-id <tenant-id>, -I <tenant-id>
Defaults to env[OS_TENANT_ID].
--endpoint <barbican-url>, -E <barbican-url>
Defaults to env[BARBICAN_ENDPOINT].
subcommands:
Action to perform
<action>
create Create a new order.
store Store a secret in barbican.
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 href.
```

38
barbicanclient/base.py Normal file
View File

@@ -0,0 +1,38 @@
# 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)
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']

View File

@@ -1,445 +1,141 @@
import eventlet
eventlet.monkey_patch(socket=True, select=True)
# 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 requests
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 import log as logging
from barbicanclient.openstack.common.gettextutils import _
from urlparse import urljoin
from barbicanclient import orders
from barbicanclient import secrets
LOG = log.getLogger(__name__)
log.setup('barbicanclient')
LOG = logging.getLogger(__name__)
logging.setup('barbicanclient')
class Connection(object):
SECRETS_PATH = 'secrets'
ORDERS_PATH = 'orders'
class HTTPError(Exception):
"""Base exception for HTTP errors."""
def __init__(self, message):
super(HTTPError, self).__init__(message)
def __init__(self, auth_endpoint=None, user=None, key=None, tenant=None,
token=None, **kwargs):
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):
"""
Authenticate and connect to the service endpoint, which can be
received through authentication.
Barbican client object used to interact with barbican service.
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 tenant: The tenant ID
default: env('OS_TENANT_NAME')
:keyword param endpoint: The barbican endpoint to connect to
default: env('BARBICAN_ENDPOINT')
If a token is provided, an endpoint should be as well.
:param auth_plugin: Authentication backend plugin
defaults to None
: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 Connection object"))
self.env = kwargs.get('fake_env') or env
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')
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')))
@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"))
LOG.debug(_("Creating Client object"))
self._session = requests.Session()
self.auth_plugin = auth_plugin
# 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.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}
)
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 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,
payload_content_type=None,
payload_content_encoding=None,
algorithm=None,
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'])
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
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 _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):
"""
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 = ''
if endpoint is None:
raise ValueError('Barbican endpoint url must be provided, or '
'must be available from auth_plugin')
if tenant_id is None:
raise ValueError('Tenant ID must be provided, or must be'
' available from the auth_plugin')
if endpoint.endswith('/'):
self._barbican_url = endpoint[:-1]
else:
self._barbican_url = endpoint
self._tenant_id = tenant_id
return response.headers, resp_body
self.base_url = '{0}/{1}'.format(self._barbican_url, self._tenant_id)
self.secrets = secrets.SecretManager(self)
self.orders = orders.OrderManager(self)
def get(self, href, params=None):
headers = {'Accept': 'application/json'}
resp = self._session.get(href, params=params, headers=headers)
self._check_status_code(resp)
return resp.json()
def get_raw(self, href, headers):
resp = self._session.get(href, headers=headers)
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'}
resp = self._session.post(url, data=json.dumps(data), headers=headers)
self._check_status_code(resp)
return resp.json()
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(self._get_error_message(resp)))
raise HTTPAuthError('{0}'.format(self._get_error_message(resp)))
if status >= 500:
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(
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):

View File

@@ -1,70 +1,63 @@
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):
"""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.
class AuthException(Exception):
"""Raised when authorization fails."""
def __init__(self, message):
self.message = message
: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
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]):
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,
auth_url=auth_url)
self._barbican_url = None
#TODO(dmend): make these configurable
self._service_type = 'keystore'
self._endpoint_type = 'publicURL'
:raises: ClientException
"""
insecure = kwargs.get('insecure', False)
endpoint = kwargs.get('endpoint')
cacert = kwargs.get('cacert')
self.tenant_name = self._keystone.tenant_name
self.tenant_id = self._keystone.tenant_id
try:
_ksclient = ksclient.Client(username=user,
password=key,
tenant_name=tenant,
cacert=cacert,
auth_url=auth_url,
insecure=insecure)
@property
def auth_token(self):
return self._keystone.auth_token
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
@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

View File

@@ -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)

View File

@@ -1,106 +1,163 @@
# 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.
"""
Command-line interface to the Barbican API.
"""
import argparse
from barbicanclient.common import auth
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.add_delete_args()
self.add_get_args()
self.add_list_args()
self.parser = self._get_main_parser()
self.subparsers = self.parser.add_subparsers(
title='subcommands',
metavar='<action>',
description='Action to perform'
)
self._add_create_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.')
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)')
parser.add_argument('--password', '-P',
def _get_main_parser(self):
parser = argparse.ArgumentParser(
description=__doc__.strip()
)
parser.add_argument('command',
metavar='<entity>',
choices=['order', 'secret'],
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.')
auth_group.add_argument('--os-auth-url', '-A',
metavar='<auth-url>',
default=client.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL].')
parser.add_argument('--os-username', '-U',
metavar='<auth-user-name>',
default=client.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME].')
parser.add_argument('--os-password', '-P',
metavar='<auth-password>',
default=client.env('OS_PASSWORD'),
help='the API key or password to authenticate with'
' (default: %(default)s)')
parser.add_argument('--tenant', '-T',
help='Defaults to env[OS_PASSWORD].')
parser.add_argument('--os-tenant-name', '-T',
metavar='<auth-tenant-name>',
default=client.env('OS_TENANT_NAME'),
help='the tenant ID (default: %(default)s)')
help='Defaults to env[OS_TENANT_NAME].')
parser.add_argument('--os-tenant-id', '-I',
metavar='<tenant-id>',
default=client.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID].')
parser.add_argument('--endpoint', '-E',
metavar='<barbican-url>',
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='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')
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='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 to be used with the '
'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).',
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('--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('--expiration', '-e', help='the expiration '
'time for the secret in ISO 8601 format')
create_parser.add_argument('--mode', '-m', default='cbc',
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 (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)
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.add_argument('UUID', help='the universally unique identi'
'fier of the the secret or order')
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', '-e',
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', '-x', 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 secret or an order by providing its href.'
)
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):
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')
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)',
def _add_get_args(self):
get_parser = self.subparsers.add_parser(
'get',
help='Retrieve a secret or an order by providing its URI.'
)
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'
@@ -110,70 +167,83 @@ 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)
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)
print secret
else:
order = self.conn.create_order(args.name,
args.payload_content_type,
args.algorithm,
args.bit_length,
args.cypher_type,
args.expiration)
self.parser.exit(status=1, message='ERROR: store is only supported'
' for secrets\n')
def create(self, args):
if args.command == 'order':
order = self.client.orders.create(args.name,
args.payload_content_type,
args.algorithm,
args.bit_length,
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.type == 'secret':
self.conn.delete_secret_by_id(args.UUID)
if args.command == 'secret':
self.client.secret.delete(args.URI)
else:
self.conn.delete_order_by_id(args.UUID)
self.client.orders.delete(args.URI)
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)
if args.command == 'secret':
if args.decrypt:
print self.client.secrets.raw(args.URI,
args.payload_content_type)
else:
print self.conn.get_secret_by_id(args.UUID)
print self.client.secrets.get(args.URI)
else:
print self.conn.get_order_by_id(args.UUID)
print self.client.orders.get(args.URI)
def lst(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)
def list(self, args):
if args.command == 'secret':
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.command, len(ls),
args.offset)
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)
if args.no_auth:
self.client = client.Client(endpoint=args.endpoint,
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.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)
else:
self.parser.exit(
status=1,
message='ERROR: please specify authentication credentials\n'
)
args.func(args)

View File

@@ -1,47 +1,129 @@
from urlparse import urlparse
from openstack.common.timeutils import parse_isotime
# 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 barbicanclient import base
from barbicanclient.openstack.common.gettextutils import _
from barbicanclient.openstack.common import log as logging
from barbicanclient.openstack.common import timeutils
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.
Builds an order object from a dictionary.
"""
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 = 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
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.secret_ref = order_dict.get('secret_ref')
def __str__(self):
return ("Order - ID: {0}\n"
" order reference: {1}\n"
" secret reference: {2}\n"
" created: {3}\n"
" status: {4}\n"
.format(self.id, self.order_ref, self.secret_ref, self.created,
self.status)
return ("Order - order href: {0}\n"
" secret href: {1}\n"
" created: {2}\n"
" status: {3}\n"
.format(self.order_ref, self.secret_ref,
self.created, self.status)
)
def __repr__(self):
return 'Order(order_ref={0})'.format(self.order_ref)
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 secret
:param payload_content_type: The format/type of the secret data
: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 href 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
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)
return resp['order_ref']
def get(self, order_ref):
"""
Returns an Order object
:param order_ref: The href for the order
"""
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)
return Order(resp)
def delete(self, order_ref):
"""
Deletes an order
:param order_ref: The href for the order
"""
if not order_ref:
raise ValueError('order_ref is required.')
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(href, params)
return [Order(o) for o in resp['orders']]

View File

@@ -1,62 +1,170 @@
from urlparse import urlparse
from openstack.common.timeutils import parse_isotime
# 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 barbicanclient import base
from barbicanclient.openstack.common import log as logging
from barbicanclient.openstack.common.timeutils import parse_isotime
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, 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')
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.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:
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()
@property
def id(self):
return self._id
self.algorithm = secret_dict.get('algorithm')
self.bit_length = secret_dict.get('bit_length')
self.mode = secret_dict.get('mode')
def __str__(self):
return ("Secret - ID: {0}\n"
" reference: {1}\n"
" name: {2}\n"
" created: {3}\n"
" status: {4}\n"
" payload content type: {5}\n"
" payload content encoding: {6}\n"
" bit length: {7}\n"
" algorithm: {8}\n"
" cypher type: {9}\n"
" expiration: {10}\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)
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)
)
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 store(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 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 href for the stored secret
"""
LOG.debug("Creating secret of payload content type {0}".format(
payload_content_type))
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
secret_dict['mode'] = 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)
return resp['secret_ref']
def get(self, secret_ref):
"""
Returns a Secret object with metadata about the secret.
:param secret_ref: The href for the secret
"""
if not secret_ref:
raise ValueError('secret_ref is required.')
resp = self.api.get(secret_ref)
return Secret(resp)
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, 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 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)
def delete(self, secret_ref):
"""
Deletes a secret
:param secret_ref: The href for the secret
"""
if not secret_ref:
raise ValueError('secret_ref is required.')
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(href, params)
return [Secret(s) for s in resp['secrets']]

View File

View File

@@ -0,0 +1,23 @@
# 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 test_endpoint_username_password_tenant_are_required(self):
with self.assertRaises(ValueError):
keystone = auth.KeystoneAuthV2()

View File

@@ -0,0 +1,93 @@
# 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 mock
import unittest2 as unittest
from barbicanclient import client
from barbicanclient.common import auth
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_name = 'tenant'
self.tenant_id = 'tenant_id'
self.endpoint = 'http://localhost:9311/v1/'
self.fake_auth = FakeAuth(self.auth_token, self.endpoint,
self.tenant_name, self.tenant_id)
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_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_error_thrown_when_no_auth_and_no_endpoint(self):
with self.assertRaises(ValueError):
c = client.Client(tenant_id=self.tenant_id)
def test_error_thrown_when_no_auth_and_no_tenant_id(self):
with self.assertRaises(ValueError):
c = client.Client(endpoint=self.endpoint)
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_base_url_ends_with_tenant_id(self):
c = client.Client(auth_plugin=self.fake_auth)
self.assertTrue(c.base_url.endswith(self.tenant_id))
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_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_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)

View File

@@ -17,5 +17,5 @@
Cloudkeep's Barbican Client version
"""
__version__ = '0.3.0'
__version__ = '0.4.0'
__version_info__ = tuple(__version__.split('.'))

View File

@@ -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'],

View File

@@ -1,359 +0,0 @@
# 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 unittest2 as unittest
from mock import MagicMock
from barbicanclient import client
from barbicanclient.common.exceptions import ClientException
def suite():
suite = unittest.TestSuite()
suite.addTest(WhenTestingConnection())
return suite
class WhenTestingConnection(unittest.TestCase):
def setUp(self):
self.auth_endpoint = 'https://keystone.com/v2'
self.user = 'user'
self.key = 'key'
self.tenant = 'tenant'
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.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)
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.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.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)
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)
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'
}
secret = client.Secret(self.connection, 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')
self.assertTrue(self._are_equivalent(secret, created))
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"
}
order = client.Order(self.connection, 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')
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)
self.assertTrue(self._are_equivalent(secrets, secret_list))
self.assertIsNone(prev_ref)
self.assertIsNone(next_ref)
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.connection.
_tenant,
self.connection.
SECRETS_PATH,
limit)}
secrets = [client.Secret(self.connection, body1['secrets'][0])]
self.request.return_value.content = json.dumps(body1)
secret_list, prev_ref, next_ref = self.connection.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.connection._tenant,
self.connection.
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.connection, 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)
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.connection.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.connection.
_tenant,
self.connection.
ORDERS_PATH,
limit)}
orders = [client.Order(self.connection, body1['orders'][0])]
self.request.return_value.content = json.dumps(body1)
order_list, prev_ref, next_ref = self.connection.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.connection._tenant,
self.connection.
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.connection, 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)
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)
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)
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)
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.connection._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__)
if __name__ == '__main__':
unittest.main()

3
tools/hacking.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
flake8 barbicanclient | tee flake8.log
exit ${PIPESTATUS[0]}

View File

@@ -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
eventlet>=0.13.0
requests>=1.2.3
python-keystoneclient>=0.3.2

View File

@@ -1,20 +1,6 @@
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
coverage
discover
mox
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

View File

@@ -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}
@@ -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}