3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,6 +26,9 @@ pip-log.txt
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
|
||||
74
README.md
74
README.md
@@ -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
38
barbicanclient/base.py
Normal 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']
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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']]
|
||||
|
||||
@@ -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']]
|
||||
|
||||
0
barbicanclient/test/common/__init__.py
Normal file
0
barbicanclient/test/common/__init__.py
Normal file
23
barbicanclient/test/common/test_auth.py
Normal file
23
barbicanclient/test/common/test_auth.py
Normal 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()
|
||||
93
barbicanclient/test/test_client.py
Normal file
93
barbicanclient/test/test_client.py
Normal 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)
|
||||
@@ -17,5 +17,5 @@
|
||||
Cloudkeep's Barbican Client version
|
||||
"""
|
||||
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.4.0'
|
||||
__version_info__ = tuple(__version__.split('.'))
|
||||
|
||||
16
setup.py
16
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'],
|
||||
|
||||
@@ -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
3
tools/hacking.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
flake8 barbicanclient | tee flake8.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
8
tox.ini
8
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}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user