From ce3f15310e33a1730422ccc4cb92e62387bff091 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Thu, 8 Sep 2016 11:08:47 -0400 Subject: [PATCH] Implement swauth This change implements the alternative authentication system, swauth in addition to adding an action to add users to swauth Change-Id: Ib752cd3a2a58f6c8cb06119c6be595cfc07ddc9f --- actions.yaml | 18 +++++++ actions/add-user | 1 + actions/add_user.py | 70 ++++++++++++++++++++++++++++ config.yaml | 6 ++- hooks/swift_hooks.py | 7 +-- lib/swift_context.py | 9 +++- lib/swift_utils.py | 31 +++++++++++- templates/grizzly/proxy-server.conf | 10 +++- templates/havana/proxy-server.conf | 10 +++- templates/icehouse/proxy-server.conf | 10 +++- templates/kilo/proxy-server.conf | 10 +++- unit_tests/test_actions.py | 47 ++++++++++++++++++- unit_tests/test_swift_context.py | 8 +++- unit_tests/test_swift_utils.py | 24 ++++++++++ 14 files changed, 247 insertions(+), 14 deletions(-) create mode 120000 actions/add-user create mode 100755 actions/add_user.py diff --git a/actions.yaml b/actions.yaml index fade4b2..e2a610a 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,3 +1,21 @@ +add-user: + description: | + Add a user to swauth. + This adds a given user / pass to swauth. Auth-type must be set to swauth. + params: + account: + type: string + description: account to add this user to + username: + type: string + description: username for the newly created user + password: + type: string + description: password for the newly created user + required: + - account + - username + - password pause: description: | Pause swift-proxy services. diff --git a/actions/add-user b/actions/add-user new file mode 120000 index 0000000..24367bd --- /dev/null +++ b/actions/add-user @@ -0,0 +1 @@ +add_user.py \ No newline at end of file diff --git a/actions/add_user.py b/actions/add_user.py new file mode 100755 index 0000000..74cf5f6 --- /dev/null +++ b/actions/add_user.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# +# Copyright 2016 Canonical Ltd +# +# 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 subprocess import ( + check_call, + CalledProcessError +) + +from charmhelpers.core.hookenv import ( + action_get, + config, + action_set, + action_fail, + leader_get, + log, +) + +from lib.swift_utils import ( + try_initialize_swauth, +) + +from charmhelpers.contrib.hahelpers.cluster import ( + determine_api_port, +) + + +def add_user(): + """Add a swauth user to swift.""" + if config('auth-type') == 'swauth': + try_initialize_swauth() + account = action_get('account') + username = action_get('username') + password = action_get('password') + bind_port = config('bind-port') + bind_port = determine_api_port(bind_port, singlenode_mode=True) + success = True + try: + check_call([ + "swauth-add-user", + "-A", "http://localhost:{}/auth/".format(bind_port), + "-K", leader_get('swauth-admin-key'), + "-a", account, username, password]) + except CalledProcessError as e: + success = False + log("Has a problem adding user: {}".format(e.output)) + action_fail( + "Adding user {} failed with: \"{}\"" + .format(username, e.message)) + if success: + message = "Successfully added the user {}".format(username) + action_set({ + 'add-user.result': 'Success', + 'add-user.message': message, + }) + +if __name__ == '__main__': + add_user() diff --git a/config.yaml b/config.yaml index 4647096..ba61c09 100644 --- a/config.yaml +++ b/config.yaml @@ -110,7 +110,11 @@ options: auth-type: default: tempauth type: string - description: Auth method to use, tempauth or keystone + description: Auth method to use, tempauth, swauth or keystone + swauth-admin-key: + default: + type: string + description: The secret key to use to authenticate as an swauth admin delay-auth-decision: default: true type: boolean diff --git a/hooks/swift_hooks.py b/hooks/swift_hooks.py index 57afa32..ebde093 100755 --- a/hooks/swift_hooks.py +++ b/hooks/swift_hooks.py @@ -52,6 +52,7 @@ from lib.swift_utils import ( is_most_recent_timestamp, timestamps_available, assess_status, + try_initialize_swauth, ) import charmhelpers.contrib.openstack.utils as openstack @@ -62,11 +63,9 @@ from charmhelpers.contrib.openstack.ha.utils import ( from charmhelpers.contrib.hahelpers.cluster import ( get_hacluster_config, -) - -from charmhelpers.contrib.hahelpers.cluster import ( is_elected_leader, ) + from charmhelpers.core.hookenv import ( config, local_unit, @@ -183,6 +182,7 @@ def config_changed(): for r_id in relation_ids('object-store'): object_store_joined(relation_id=r_id) + try_initialize_swauth() @hooks.hook('identity-service-relation-joined') @@ -233,6 +233,7 @@ def storage_joined(): # possibility of storage nodes getting out-of-date rings by deprecating # any existing ones from the www dir. mark_www_rings_deleted() + try_initialize_swauth() def get_host_ip(rid=None, unit=None): diff --git a/lib/swift_context.py b/lib/swift_context.py index 4d07fa9..9a3c476 100644 --- a/lib/swift_context.py +++ b/lib/swift_context.py @@ -9,6 +9,7 @@ from charmhelpers.core.hookenv import ( relation_get, unit_get, service_name, + leader_get, ) from charmhelpers.contrib.openstack.context import ( OSContextGenerator, @@ -107,9 +108,13 @@ class SwiftIdentityContext(OSContextGenerator): 'delay_auth_decision': config('delay-auth-decision'), 'node_timeout': config('node-timeout'), 'recoverable_node_timeout': config('recoverable-node-timeout'), - 'log_headers': config('log-headers') + 'log_headers': config('log-headers'), } + admin_key = leader_get('swauth-admin-key') + if admin_key is not None: + ctxt['swauth_admin_key'] = admin_key + if config('debug'): ctxt['log_level'] = 'DEBUG' else: @@ -126,6 +131,8 @@ class SwiftIdentityContext(OSContextGenerator): ctxt['ssl'] = False auth_type = config('auth-type') + ctxt['auth_type'] = auth_type + auth_host = config('keystone-auth-host') admin_user = config('keystone-admin-user') admin_password = config('keystone-admin-user') diff --git a/lib/swift_utils.py b/lib/swift_utils.py index 811a581..3f5ae05 100644 --- a/lib/swift_utils.py +++ b/lib/swift_utils.py @@ -34,19 +34,23 @@ from charmhelpers.contrib.openstack.utils import ( from charmhelpers.contrib.hahelpers.cluster import ( is_elected_leader, peer_units, + determine_api_port, ) from charmhelpers.core.hookenv import ( log, DEBUG, INFO, WARNING, - config, local_unit, relation_get, unit_get, relation_set, relation_ids, related_units, + config, + is_leader, + leader_set, + leader_get, ) from charmhelpers.fetch import ( apt_update, @@ -116,7 +120,7 @@ BASE_PACKAGES = [ 'python-keystone', ] # > Folsom specific packages -FOLSOM_PACKAGES = BASE_PACKAGES + ['swift-plugin-s3'] +FOLSOM_PACKAGES = BASE_PACKAGES + ['swift-plugin-s3', 'swauth'] SWIFT_HA_RES = 'grp_swift_vips' TEMPLATES = 'templates/' @@ -304,6 +308,29 @@ class SwiftProxyClusterRPC(object): return rq +def try_initialize_swauth(): + if is_leader() and config('auth-type') == 'swauth': + if leader_get('swauth-init') is not True: + try: + admin_key = config('swauth-admin-key') + if admin_key == '' or admin_key is None: + admin_key = leader_get('swauth-admin-key') + if admin_key is None: + admin_key = uuid.uuid4() + leader_set({'swauth-admin-key': admin_key}) + + bind_port = config('bind-port') + bind_port = determine_api_port(bind_port, singlenode_mode=True) + subprocess.check_call([ + 'swauth-prep', + '-A', + 'http://localhost:{}/auth'.format(bind_port), + '-K', admin_key]) + leader_set({'swauth-init': True}) + except subprocess.CalledProcessError: + log("had a problem initializing swauth!") + + def get_first_available_value(responses, key, default=None): for r in responses: if key in r: diff --git a/templates/grizzly/proxy-server.conf b/templates/grizzly/proxy-server.conf index a46934c..3a00248 100644 --- a/templates/grizzly/proxy-server.conf +++ b/templates/grizzly/proxy-server.conf @@ -12,7 +12,7 @@ key_file = {{ ssl_key }} pipeline = healthcheck cache swift3 s3token authtoken keystone container-quotas account-quotas proxy-server {% else %} [pipeline:main] -pipeline = healthcheck cache tempauth container-quotas account-quotas proxy-server +pipeline = healthcheck cache {{ auth_type }} container-quotas account-quotas proxy-server {% endif %} [app:proxy-server] @@ -70,3 +70,11 @@ admin_token = {{ admin_token }} [filter:swift3] use = egg:swift3#swift3 {% endif %} + +{% if auth_type == 'swauth' %} +[filter:swauth] +use = egg:swauth#swauth +set log_name = swauth +super_admin_key = {{ swauth_admin_key }} +default_swift_cluster = local#https://{{ proxy_ip }}:8080/v1 +{% endif %} diff --git a/templates/havana/proxy-server.conf b/templates/havana/proxy-server.conf index 87a2256..dbf714e 100644 --- a/templates/havana/proxy-server.conf +++ b/templates/havana/proxy-server.conf @@ -12,7 +12,7 @@ key_file = {{ ssl_key }} pipeline = healthcheck cache swift3 authtoken keystoneauth container-quotas account-quotas proxy-server {% else %} [pipeline:main] -pipeline = healthcheck cache tempauth container-quotas account-quotas proxy-server +pipeline = healthcheck cache {{ auth_type }} container-quotas account-quotas proxy-server {% endif %} [app:proxy-server] @@ -71,3 +71,11 @@ admin_token = {{ admin_token }} [filter:swift3] use = egg:swift3#swift3 {% endif %} + +{% if auth_type == 'swauth' %} +[filter:swauth] +use = egg:swauth#swauth +set log_name = swauth +super_admin_key = {{ swauth_admin_key }} +default_swift_cluster = local#https://{{ proxy_ip }}:8080/v1 +{% endif %} diff --git a/templates/icehouse/proxy-server.conf b/templates/icehouse/proxy-server.conf index 561d457..a5f0173 100644 --- a/templates/icehouse/proxy-server.conf +++ b/templates/icehouse/proxy-server.conf @@ -19,7 +19,7 @@ key_file = {{ ssl_key }} pipeline = gatekeeper healthcheck proxy-logging cache swift3 s3token container_sync bulk tempurl slo dlo formpost authtoken keystoneauth staticweb container-quotas account-quotas proxy-logging proxy-server {% else %} [pipeline:main] -pipeline = gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl slo dlo formpost tempauth staticweb container-quotas account-quotas proxy-logging proxy-server +pipeline = gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl slo dlo formpost {{ auth_type }} staticweb container-quotas account-quotas proxy-logging proxy-server {% endif %} [app:proxy-server] @@ -104,3 +104,11 @@ admin_token = {{ admin_token }} [filter:swift3] use = egg:swift3#swift3 {% endif %} + +{% if auth_type == 'swauth' %} +[filter:swauth] +use = egg:swauth#swauth +set log_name = swauth +super_admin_key = {{ swauth_admin_key }} +default_swift_cluster = local#https://{{ proxy_ip }}:8080/v1 +{% endif %} diff --git a/templates/kilo/proxy-server.conf b/templates/kilo/proxy-server.conf index 443f9e0..86ae416 100644 --- a/templates/kilo/proxy-server.conf +++ b/templates/kilo/proxy-server.conf @@ -19,7 +19,7 @@ key_file = {{ ssl_key }} pipeline = gatekeeper healthcheck proxy-logging cache swift3 s3token container_sync bulk tempurl slo dlo formpost authtoken keystoneauth staticweb container-quotas account-quotas proxy-logging proxy-server {% else %} [pipeline:main] -pipeline = gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl slo dlo formpost tempauth staticweb container-quotas account-quotas proxy-logging proxy-server +pipeline = gatekeeper healthcheck proxy-logging cache container_sync bulk tempurl slo dlo formpost {{ auth_type }} staticweb container-quotas account-quotas proxy-logging proxy-server {% endif %} [app:proxy-server] @@ -106,3 +106,11 @@ auth_uri = {{ auth_protocol }}://{{ keystone_host }}:{{ auth_port }} [filter:swift3] use = egg:swift3#swift3 {% endif %} + +{% if auth_type == 'swauth' %} +[filter:swauth] +use = egg:swauth#swauth +set log_name = swauth +super_admin_key = {{ swauth_admin_key }} +default_swift_cluster = local#https://{{ proxy_ip }}:8080/v1 +{% endif %} diff --git a/unit_tests/test_actions.py b/unit_tests/test_actions.py index 34aa119..2e7ede4 100644 --- a/unit_tests/test_actions.py +++ b/unit_tests/test_actions.py @@ -15,12 +15,13 @@ import argparse import sys import tempfile +import subprocess import mock import yaml import unittest -from mock import patch, MagicMock +from mock import patch, MagicMock, call # python-apt is not installed as part of test-requirements but is imported by # some charmhelpers modules so create a fake import. @@ -32,6 +33,7 @@ with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec, \ mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f: lambda *args, **kwargs: f(*args, **kwargs)) import actions.actions + import actions.add_user class CharmTestCase(unittest.TestCase): @@ -202,3 +204,46 @@ class MainTestCase(CharmTestCase): with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}): actions.actions.main([]) self.assertEqual(dummy_calls, ["uh oh"]) + + +class AddUserTestCase(CharmTestCase): + + def setUp(self): + super(AddUserTestCase, self).setUp( + actions.add_user, ["action_get", "action_set", + "action_fail", "check_call", + "try_initialize_swauth", "config", + "determine_api_port", "leader_get"]) + + def test_success(self): + """Ensure that the action_set is called on succees.""" + self.config.return_value = "swauth" + self.action_get.return_value = "test" + self.determine_api_port.return_value = 8070 + actions.add_user.add_user() + self.leader_get.assert_called_with("swauth-admin-key") + calls = [call("account"), call("username"), call("password")] + self.action_get.assert_has_calls(calls) + self.action_set.assert_called_once_with({ + 'add-user.result': 'Success', + 'add-user.message': "Successfully added the user test", + }) + + def test_failure(self): + """Ensure that action_fail is called on failure.""" + self.config.return_value = "swauth" + self.action_get.return_value = "test" + self.determine_api_port.return_value = 8070 + self.CalledProcessError = ValueError + + self.check_call.side_effect = subprocess.CalledProcessError(0, + "hi", + "no") + actions.add_user.add_user() + self.leader_get.assert_called_with("swauth-admin-key") + calls = [call("account"), call("username"), call("password")] + self.action_get.assert_has_calls(calls) + self.action_set.assert_not_called() + + self.action_fail.assert_called_once_with( + 'Adding user test failed with: ""') diff --git a/unit_tests/test_swift_context.py b/unit_tests/test_swift_context.py index 0d6ae7a..ec985bc 100644 --- a/unit_tests/test_swift_context.py +++ b/unit_tests/test_swift_context.py @@ -25,6 +25,7 @@ with mock.patch('charmhelpers.core.hookenv.config'): class SwiftIdentityContextTest(unittest.TestCase): + @mock.patch('lib.swift_context.leader_get') @mock.patch('lib.swift_context.relation_get') @mock.patch('lib.swift_context.related_units') @mock.patch('lib.swift_context.relation_ids') @@ -36,7 +37,8 @@ class SwiftIdentityContextTest(unittest.TestCase): def test_context_api_v2(self, mock_config, mock_get_host_ip, mock_unit_get, mock_determine_api_port, mock_IdentityServiceContext, mock_relation_ids, - mock_related_units, mock_relation_get): + mock_related_units, mock_relation_get, + mock_leader_get): _relinfo = { 'auth_protocol': 'http', 'service_protocol': 'http', @@ -57,6 +59,7 @@ class SwiftIdentityContextTest(unittest.TestCase): ctxt = swift_context.SwiftIdentityContext() self.assertEqual(ctxt()['api_version'], '2') + @mock.patch('lib.swift_context.leader_get') @mock.patch('lib.swift_context.relation_get') @mock.patch('lib.swift_context.related_units') @mock.patch('lib.swift_context.relation_ids') @@ -68,7 +71,8 @@ class SwiftIdentityContextTest(unittest.TestCase): def test_context_api_v3(self, mock_config, mock_get_host_ip, mock_unit_get, mock_determine_api_port, mock_IdentityServiceContext, mock_relation_ids, - mock_related_units, mock_relation_get): + mock_related_units, mock_relation_get, + mock_leader_get): _relinfo = { 'auth_protocol': 'http', 'service_protocol': 'http', diff --git a/unit_tests/test_swift_utils.py b/unit_tests/test_swift_utils.py index 625ee11..41e6715 100644 --- a/unit_tests/test_swift_utils.py +++ b/unit_tests/test_swift_utils.py @@ -18,6 +18,7 @@ import shutil import tempfile import uuid import unittest +import subprocess with mock.patch('charmhelpers.core.hookenv.config'): import lib.swift_utils as swift_utils @@ -421,3 +422,26 @@ class SwiftUtilsTestCase(unittest.TestCase): 'test-config', required_interfaces, charm_func=swift_utils.customer_check_assess_status, services='s1', ports=None) + + @mock.patch.object(swift_utils, 'leader_set') + @mock.patch.object(swift_utils, 'determine_api_port') + @mock.patch.object(swift_utils, 'is_leader') + @mock.patch.object(swift_utils, 'config') + @mock.patch.object(swift_utils, 'leader_get') + @mock.patch.object(subprocess, 'check_call') + def test_config_and_leader_get(self, check_call, leader_get, config, + is_leader, determine_api_port, leader_set): + """Ensure that we config_get, and then leader_get.""" + config.side_effect = lambda key: { + 'auth-type': 'swauth', + 'swauth-admin-key': None, + 'bind-port': 8080}[key] + determine_api_port.return_value = 8080 + is_leader.return_value = True + leader_get.return_value = "Test" + swift_utils.try_initialize_swauth() + check_call.assert_called_with(['swauth-prep', + '-A', + 'http://localhost:8080/auth', + '-K', + 'Test'])