From c825e77418200bc3d7557d4a28fa34e8f36724cf Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Mon, 26 Jun 2017 15:47:35 +0100 Subject: [PATCH] DECKHAND-10: Add Barbican integration to Deckhand This commit adds Barbican integration to Deckhand. --- ChangeLog | 3 +- README.rst | 1 - deckhand/barbican/__init__.py | 0 deckhand/barbican/client_wrapper.py | 114 ++++++++++++++++++++++++++++ deckhand/barbican/driver.py | 26 +++++++ deckhand/conf/config.py | 39 +++++++++- deckhand/control/secrets.py | 5 +- deckhand/deckhand.py | 4 +- etc/deckhand/deckhand.conf.sample | 47 ++++++++++++ requirements.txt | 6 +- 10 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 deckhand/barbican/__init__.py create mode 100644 deckhand/barbican/client_wrapper.py create mode 100644 deckhand/barbican/driver.py create mode 100644 etc/deckhand/deckhand.conf.sample diff --git a/ChangeLog b/ChangeLog index 08c66937..d788f3e4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,8 @@ CHANGES ======= -* Implement core Deckhand API framework +* DECKHAND-10: Barbican initial integration +* DECKHAND-2: Design core Deckhand API framework * Oslo config integration (#1) * Add ChangeLog * Initial commit diff --git a/README.rst b/README.rst index 2f709a99..492d1fcf 100644 --- a/README.rst +++ b/README.rst @@ -9,5 +9,4 @@ To run:: $ virtualenv -p python3 /var/tmp/deckhand $ . /var/tmp/deckhand/bin/activate $ sudo pip install . - $ python setup.py install $ uwsgi --http :9000 -w deckhand.deckhand --callable deckhand --enable-threads -L diff --git a/deckhand/barbican/__init__.py b/deckhand/barbican/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/deckhand/barbican/client_wrapper.py b/deckhand/barbican/client_wrapper.py new file mode 100644 index 00000000..c4594e5f --- /dev/null +++ b/deckhand/barbican/client_wrapper.py @@ -0,0 +1,114 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# 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 keystoneauth1.identity import v3 +from keystoneauth1 import session + +from deckhand.conf import config +from deckhand import errors + +from barbicanclient import barbican +from barbicanclient import exceptions as barbican_exc + +CONF = config.CONF + + +class BarbicanClientWrapper(object): + """Barbican client wrapper class that encapsulates authentication logic.""" + + def __init__(self): + """Initialise the BarbicanClientWrapper for use.""" + self._cached_client = None + + def _invalidate_cached_client(self): + """Tell the wrapper to invalidate the cached barbican-client.""" + self._cached_client = None + + def _get_client(self, retry_on_conflict=True): + # If we've already constructed a valid, authed client, just return + # that. + if retry_on_conflict and self._cached_client is not None: + return self._cached_client + + # TODO: Deckhand's configuration file needs to be populated with + # correct Keysone authentication values as well as the Barbican + # endpoint URL automatically. + barbican_url = (CONF.barbican.api_endpoint + if CONF.barbican.api_endpoint + else 'http://127.0.0.1:9311') + + keystone_auth = dict(CONF.keystone_authtoken) + auth = v3.Password(**keystone_auth) + sess = session.Session(auth=auth) + + try: + # TODO: replace with ``barbican_url``. + cli = barbican.client.Client(endpoint=barbican_url, + session=sess) + # Cache the client so we don't have to reconstruct and + # reauthenticate it every time we need it. + if retry_on_conflict: + self._cached_client = cli + + except barbican_exc.HTTPAuthError: + msg = _("Unable to authenticate Barbican client.") + # TODO: Log the error. + raise errors.ApiError(msg) + + return cli + + def _multi_getattr(self, obj, attr): + """Support nested attribute path for getattr(). + + :param obj: Root object. + :param attr: Path of final attribute to get. E.g., "a.b.c.d" + + :returns: The value of the final named attribute. + :raises: AttributeError will be raised if the path is invalid. + """ + for attribute in attr.split("."): + obj = getattr(obj, attribute) + return obj + + def call(self, method, *args, **kwargs): + """Call a barbican client method and retry on stale token. + + :param method: Name of the client method to call as a string. + :param args: Client method arguments. + :param kwargs: Client method keyword arguments. + :param retry_on_conflict: Boolean value. Whether the request should be + retried in case of a conflict error + (HTTP 409) or not. If retry_on_conflict is + False the cached instance of the client + won't be used. Defaults to True. + """ + retry_on_conflict = kwargs.pop('retry_on_conflict', True) + + for attempt in range(2): + client = self._get_client(retry_on_conflict=retry_on_conflict) + + try: + return self._multi_getattr(client, method)(*args, **kwargs) + except barbican_exc.HTTPAuthError: + # In this case, the authorization token of the cached + # barbican-client probably expired. So invalidate the cached + # client and the next try will start with a fresh one. + if not attempt: + self._invalidate_cached_client() + # TODO: include after implementing oslo.log. + # LOG.debug("The Barbican client became unauthorized. " + # "Will attempt to reauthorize and try again.") + else: + # This code should be unreachable actually + raise diff --git a/deckhand/barbican/driver.py b/deckhand/barbican/driver.py new file mode 100644 index 00000000..db8352f3 --- /dev/null +++ b/deckhand/barbican/driver.py @@ -0,0 +1,26 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# 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 deckhand.barbican import client_wrapper + + +class BarbicanDriver(object): + + def __init__(self): + self.barbicanclient = client_wrapper.BarbicanClientWrapper() + + def ca_list(self, **kwargs): + # FIXME(felipemonteiro): Testing cas.list endpoint. + ca_list = self.barbicanclient.call("cas.list", **kwargs) + return ca_list diff --git a/deckhand/conf/config.py b/deckhand/conf/config.py index 55adbed5..25f2bdee 100644 --- a/deckhand/conf/config.py +++ b/deckhand/conf/config.py @@ -14,6 +14,29 @@ from oslo_config import cfg +CONF = cfg.CONF + + +keystone_auth_group = cfg.OptGroup( + name='keystone_authtoken', + title='Keystone Authentication Options' +) + +keystone_auth_opts = [ + cfg.StrOpt(name='project_domain_name', + default='Default'), + cfg.StrOpt(name='project_name', + default='admin'), + cfg.StrOpt(name='user_domain_name', + default='Default'), + cfg.StrOpt(name='password', + default='devstack'), + cfg.StrOpt(name='username', + default='admin'), + cfg.StrOpt(name='auth_url', + default='http://127.0.0.1/identity/v3') +] + barbican_group = cfg.OptGroup( name='barbican', title='Barbican Options', @@ -21,13 +44,25 @@ barbican_group = cfg.OptGroup( Barbican options for allowing Deckhand to communicate with Barbican. """) -barbican_opts = [] +barbican_opts = [ + cfg.StrOpt( + 'api_endpoint', + sample_default='http://barbican.example.org:9311/', + help='URL override for the Barbican API endpoint.'), +] def register_opts(conf): conf.register_group(barbican_group) conf.register_opts(barbican_opts, group=barbican_group) + conf.register_group(keystone_auth_group) + conf.register_opts(keystone_auth_opts, group=keystone_auth_group) + def list_opts(): - return {barbican_group: barbican_opts} + return {keystone_auth_group: keystone_auth_opts, + barbican_group: barbican_opts} + + +register_opts(CONF) diff --git a/deckhand/control/secrets.py b/deckhand/control/secrets.py index 96048a88..79c88f16 100644 --- a/deckhand/control/secrets.py +++ b/deckhand/control/secrets.py @@ -16,6 +16,7 @@ import falcon from oslo_serialization import jsonutils as json +from deckhand.barbican import driver from deckhand.control import base as api_base @@ -29,8 +30,10 @@ class SecretsResource(api_base.BaseResource): def __init__(self, **kwargs): super(SecretsResource, self).__init__(**kwargs) self.authorized_roles = ['user'] + self.barbican_driver = driver.BarbicanDriver() def on_get(self, req, resp): # TODO(felipemonteiro): Implement this API endpoint. - resp.body = json.dumps({'secrets': 'test_secrets'}) + ca_list = self.barbican_driver.ca_list() # Random endpoint to test. + resp.body = json.dumps({'secrets': [c.to_dict() for c in ca_list]}) resp.status = falcon.HTTP_200 diff --git a/deckhand/deckhand.py b/deckhand/deckhand.py index a64b2378..e443323b 100644 --- a/deckhand/deckhand.py +++ b/deckhand/deckhand.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from deckhand.control import api +from .control import api def start_deckhand(): @@ -20,4 +20,4 @@ def start_deckhand(): # Callable to be used by uwsgi. -deckhand = start_deckhand() +deckhand_callable = start_deckhand() diff --git a/etc/deckhand/deckhand.conf.sample b/etc/deckhand/deckhand.conf.sample new file mode 100644 index 00000000..e2d6cf63 --- /dev/null +++ b/etc/deckhand/deckhand.conf.sample @@ -0,0 +1,47 @@ +[DEFAULT] + + +[barbican] +# +# Barbican options for allowing Deckhand to communicate with Barbican. + +# +# From deckhand.conf +# + +# URL override for the Barbican API endpoint. (string value) +#api_endpoint = http://barbican.example.org:9311/ + + +[keystone_authtoken] + +# +# From deckhand.conf +# + +# (string value) +#signing_dir = + +# (string value) +#cafile = + +# (string value) +#project_domain_name = Default + +# (string value) +#project_name = admin + +# (string value) +#user_domain_name = Default + +# (string value) +#password = devstack + +# (string value) +#username = admin + +# (string value) +#auth_url = http://127.0.0.1/identity + +# (string value) +#auth_type = password diff --git a/requirements.txt b/requirements.txt index eb5f7f6e..294054b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ falcon==1.1.0 + +keystoneauth1>=2.21.0 # Apache-2.0 oslo.config>=3.22.0 # Apache-2.0 -oslo.config>=3.22.0 # Apache-2.0 +oslo.context>=2.14.0 # Apache-2.0 +oslo.log>=3.22.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 python-barbicanclient>=4.0.0 # Apache-2.0 -keystoneauth1>=2.21.0 # Apache-2.0