Merge pull request #10 from Aghoreyshi/master

Authentication and Service Endpoint Retrieval; Addition of Environment Variable Defaults to Client Library
This commit is contained in:
John Wood
2013-06-24 16:05:43 -07:00
8 changed files with 172 additions and 80 deletions

4
.gitignore vendored
View File

@@ -36,6 +36,10 @@ nosetests.xml
# Idea
.idea
*.iml
# generic venvs
.venv
# OSX metadata
.DS_Store

View File

@@ -1,4 +1,6 @@
python-barbicanclient
=====================
A python library for accessing the Barbican key management service.
This is a client for the [Barbican](https://github.com/cloudkeep/barbican)
Key Management API. There is a Python library for accessing the API
(`barbicanclient` module), and a command-line script (`keep`).

View File

@@ -2,6 +2,7 @@ import eventlet
eventlet.monkey_patch(socket=True, select=True)
import json
import os
import requests
from barbicanclient.secrets import Secret
@@ -22,34 +23,50 @@ class Connection(object):
SECRETS_PATH = 'secrets'
ORDERS_PATH = 'orders'
def __init__(self, auth_endpoint, user, key, tenant,
token=None, authenticate=None, request=None, **kwargs):
def __init__(self, auth_endpoint=None, user=None, key=None, tenant=None,
token=None, **kwargs):
"""
Authenticate and connect to the service endpoint, which can be
received through authentication.
Environment variables will be used by default when their corresponding
arguments are not passed in.
: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.
"""
LOG.debug(_("Creating Connection object"))
self._auth_endpoint = auth_endpoint
self.authenticate = authenticate or auth.authenticate
self.request = request or requests.request
self._user = user
self._key = key
self._tenant = tenant
self._endpoint = (kwargs.get('endpoint')
or 'https://barbican.api.rackspacecloud.com/v1/')
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)
self.connect(token=(token or self.env('AUTH_TOKEN')))
@property
def _conn(self):
"""
Property to enable decorators to work
properly
"""
"""Property to enable decorators to work properly"""
return self
@property
@@ -62,11 +79,16 @@ class Connection(object):
"""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 the
token will be used for this connection and auth will
not happen.
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"))
@@ -81,14 +103,17 @@ class Connection(object):
self.auth_token = token
else:
LOG.debug(_("Authenticating token"))
self._endpoint, self.auth_token = self.authenticate(
endpoint, self.auth_token = self.authenticate(
self._auth_endpoint,
self._user,
self._key,
self._tenant,
service_type='key-store',
endpoint=self._endpoint,
cacert=self._cacert
)
if self.endpoint is None:
self.endpoint = endpoint
@property
def auth_token(self):
@@ -108,6 +133,9 @@ class Connection(object):
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))
@@ -122,6 +150,8 @@ class Connection(object):
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))
@@ -151,14 +181,13 @@ class Connection(object):
"""
Creates and returns a Secret object with all of its metadata filled in.
arguments:
mime_type - The MIME type of the secret
plain_text - The unencrypted secret
name - A friendly name for the secret
algorithm - The algorithm the secret is used with
bit_length - The bit length of the secret
cypher_type - The cypher type (e.g. block cipher mode of operation)
expiration - The expiration time for the secret in ISO 8601 format
:param mime_type: The MIME type of the secret
:param plain_text: The unencrypted secret
: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 secret of mime_type {0}").format(mime_type))
href = "{0}/{1}".format(self._tenant, self.SECRETS_PATH)
@@ -185,7 +214,9 @@ class Connection(object):
def delete_secret_by_id(self, secret_id):
"""
Deletes a secret using its UUID
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))
@@ -193,14 +224,18 @@ class Connection(object):
def delete_secret(self, href):
"""
Deletes a secret using its full reference
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 using the secret's UUID
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)
@@ -208,7 +243,9 @@ class Connection(object):
def get_secret(self, href):
"""
Returns a Secret object using the secret's full reference
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))
@@ -216,7 +253,10 @@ class Connection(object):
def get_raw_secret_by_id(self, secret_id, mime_type):
"""
Returns the raw secret using the secret's UUID and MIME type
Returns the raw secret
:param secret_id: The UUID of the secret
:param mime_type: The MIME 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)
@@ -224,7 +264,10 @@ class Connection(object):
def get_raw_secret(self, href, mime_type):
"""
Returns the raw secret using the secret's UUID and MIME type
Returns the raw secret
:param href: The reference to the secret
:param mime_type: The MIME type of the secret
"""
hdrs = {"Accept": mime_type}
hdrs, body = self._perform_http(href=href, method='GET', headers=hdrs,
@@ -238,6 +281,9 @@ class Connection(object):
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))
@@ -252,6 +298,8 @@ class Connection(object):
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))
@@ -279,12 +327,11 @@ class Connection(object):
"""
Creates and returns an Order object with all of its metadata filled in.
arguments:
mime_type - The MIME type of the secret
name - A friendly name for the secret
algorithm - The algorithm the secret is used with
bit_length - The bit length of the secret
cypher_type - The cypher type (e.g. block cipher mode of operation)
:param mime_type: The MIME type of the secret
: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)
"""
LOG.debug(_("Creating order of mime_type {0}").format(mime_type))
href = "{0}/{1}".format(self._tenant, self.ORDERS_PATH)
@@ -307,7 +354,9 @@ class Connection(object):
def delete_order_by_id(self, order_id):
"""
Deletes an order using its UUID
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)
@@ -315,14 +364,18 @@ class Connection(object):
def delete_order(self, href):
"""
Deletes an order using its full reference
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 using the order's UUID
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)
@@ -330,7 +383,9 @@ class Connection(object):
def get_order(self, href):
"""
Returns an Order object using the order's full reference
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))
@@ -347,20 +402,25 @@ class Connection(object):
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
:return: (headers, body)
"""
if not isinstance(request_body, str):
request_body = json.dumps(request_body)
url = urljoin(self._endpoint, href)
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))
@@ -376,3 +436,18 @@ class Connection(object):
resp_body = ''
return response.headers, resp_body
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
Source: Keystone's shell.py
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')

46
keep → barbicanclient/keep.py Executable file → Normal file
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
import argparse
import os
from barbicanclient import client
@@ -24,23 +23,26 @@ class Keep:
choices=["order", "secret"],
help="type to operate on")
parser.add_argument('--auth_endpoint', '-A',
default=env('OS_AUTH_URL'),
default=client.env('OS_AUTH_URL'),
help='the URL to authenticate against (default: '
'%(default)s)')
parser.add_argument('--user', '-U', default=env('OS_USERNAME'),
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', default=env('OS_PASSWORD'),
parser.add_argument('--password', '-P',
default=client.env('OS_PASSWORD'),
help='the API key or password to authenticate with'
' (default: %(default)s)')
parser.add_argument('--tenant', '-T', default=env('OS_TENANT_NAME'),
parser.add_argument('--tenant', '-T',
default=client.env('OS_TENANT_NAME'),
help='the tenant ID (default: %(default)s)')
parser.add_argument('--endpoint', '-E', default=env('SERVICE_ENDPOINT')
, help='the URL of the barbican server (default: %'
parser.add_argument('--endpoint', '-E',
default=client.env('BARBICAN_ENDPOINT'),
help='the URL of the barbican server (default: %'
'(default)s)')
parser.add_argument('--token', '-K', default=env('SERVICE_TOKEN'),
help='the authentication token (default: %(default'
')s)')
parser.add_argument('--token', '-K',
default=client.env('AUTH_TOKEN'), help='the au'
'thentication token (default: %(default)s)')
return parser
def add_create_args(self):
@@ -149,33 +151,19 @@ class Keep:
l = self.conn.list_orders(args.limit, args.offset)
for i in l[0]:
print i
print 'Displayed {0} {1}s - offset: {2}'.format(len(l[0]), args.type,
args.offset)
print '{0}s displayed: {1} - offset: {2}'.format(args.type, len(l[0]),
args.offset)
def execute(self):
args = self.parser.parse_args()
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)
args.func(args)
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
Source: Keystone's shell.py
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
def main():
k = Keep()
k.execute()

Binary file not shown.

6
clientrc Normal file
View File

@@ -0,0 +1,6 @@
export OS_TENANT_NAME=demo
export OS_USERNAME=demo
export OS_PASSWORD=password
export OS_AUTH_URL="http://keystone-int.cloudkeep.io:5000/v2.0/"
export BARBICAN_ENDPOINT="http://api-01-int.cloudkeep.io:9311/v1/"
export AUTH_TOKEN=''

View File

@@ -55,5 +55,8 @@ setuptools.setup(
'Programming Language :: Python :: 2.7',
'Environment :: No Input/Output (Daemon)',
],
scripts = ['keep']
entry_points="""
[console_scripts]
keep = barbicanclient.keep:main
"""
)

View File

@@ -40,6 +40,8 @@ class WhenTestingConnection(unittest.TestCase):
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()
@@ -62,7 +64,8 @@ class WhenTestingConnection(unittest.TestCase):
self.key, self.tenant,
token=self.auth_token,
authenticate=self.authenticate,
request=self.request)
request=self.request,
endpoint=self.endpoint)
def test_should_connect_with_token(self):
self.assertFalse(self.authenticate.called)
@@ -79,6 +82,7 @@ class WhenTestingConnection(unittest.TestCase):
self.user,
self.key,
self.tenant,
service_type='key-store',
endpoint=self.endpoint,
cacert=None
)
@@ -89,6 +93,16 @@ class WhenTestingConnection(unittest.TestCase):
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',
'content_types': {'default': 'text/plain'},
@@ -318,7 +332,7 @@ class WhenTestingConnection(unittest.TestCase):
parse_json=False)
self.assertEqual(self.request.return_value.content, body)
def test_should_raise_exception(self):
def test_should_raise_for_bad_response(self):
self._setup_request()
self.request.return_value.ok = False
self.request.return_value.status_code = 404