support custom admin user and user auth

In order to support cases where pools and keys are pre-created and
ceph-proxy just proxies this data to client applications this change
introduces support for:

* having custom "admin" users which may not actually have admin
privileges on the target cluster (client.admin is probably occupied by
real admins in this case);
* using cephx keys provided via charm config.

Change-Id: I01014b6986f92bf0ad8147a08afa1d61fdd5c088
Closes-bug: #1793991
This commit is contained in:
Dmitrii Shcherbakov 2018-09-20 04:31:15 +03:00
parent df29b5780f
commit 81383a160b
7 changed files with 148 additions and 10 deletions

View File

@ -59,3 +59,18 @@ options:
. .
Valid options are "cephx" and "none". If "none" is specified, keys will Valid options are "cephx" and "none". If "none" is specified, keys will
still be created and deployed so that it can be enabled later. still be created and deployed so that it can be enabled later.
user-keys:
type: string
default: ""
description: |
A space-separated list of <username>:<cephx-base64-key> pairs used to
lookup authentication keys for a specific user instead of trying to
create a user and a key via ceph-mon.
admin-user:
type: string
default: "client.admin"
description: |
A configurable admin user name. Used for scenarios where pools are
pre-created and the user given to charm-ceph-proxy simply needs to
check the existence of a given pool and error out if one does not
exist. Can be used in conjunction with user-keys.

View File

@ -31,6 +31,7 @@ from charmhelpers.core.hookenv import (
cached, cached,
status_set, status_set,
WARNING, WARNING,
config,
) )
from charmhelpers.fetch import ( from charmhelpers.fetch import (
apt_cache apt_cache
@ -369,14 +370,30 @@ def get_upgrade_key():
return get_named_key('upgrade-osd', _upgrade_caps) return get_named_key('upgrade-osd', _upgrade_caps)
def _config_user_key(name):
user_keys_list = config('user-keys')
if user_keys_list:
for ukpair in user_keys_list.split(' '):
uk = ukpair.split(':')
if len(uk) == 2:
user_type, k = uk
t, u = user_type.split('.')
if u == name:
return k
def get_named_key(name, caps=None): def get_named_key(name, caps=None):
config_user_key = _config_user_key(name)
if config_user_key:
return config_user_key
caps = caps or _default_caps caps = caps or _default_caps
cmd = [ cmd = [
"sudo", "sudo",
"-u", "-u",
ceph_user(), ceph_user(),
'ceph', 'ceph',
'--name', 'client.admin', '--name', config('admin-user'),
'--keyring', '--keyring',
'/var/lib/ceph/mon/ceph-{}/keyring'.format( '/var/lib/ceph/mon/ceph-{}/keyring'.format(
get_unit_hostname() get_unit_hostname()

View File

@ -86,11 +86,16 @@ def emit_cephconf():
render('ceph.conf', charm_ceph_conf, cephcontext, perms=0o644) render('ceph.conf', charm_ceph_conf, cephcontext, perms=0o644)
install_alternative('ceph.conf', '/etc/ceph/ceph.conf', install_alternative('ceph.conf', '/etc/ceph/ceph.conf',
charm_ceph_conf, 100) charm_ceph_conf, 100)
keyring = 'ceph.client.admin.keyring'
keyring_template = 'ceph.keyring'
keyring = 'ceph.{}.keyring'.format(config('admin-user'))
keyring_path = '/etc/ceph/' + keyring keyring_path = '/etc/ceph/' + keyring
ctx = {'admin_key': config('admin-key')} ctx = {
'admin_key': config('admin-key'),
'admin_user': config('admin-user'),
}
user = ceph.ceph_user() user = ceph.ceph_user()
render(keyring, keyring_path, ctx, owner=user, perms=0o600) render(keyring_template, keyring_path, ctx, owner=user, perms=0o600)
keyring = 'keyring' keyring = 'keyring'
keyring_path = ( keyring_path = (

View File

@ -1,3 +1,3 @@
[client.admin] [{{ admin_user }}]
key = {{admin_key}} key = {{admin_key}}

View File

@ -1,3 +1,3 @@
[client.admin] [{{ admin_user }}]
key = {{admin_key}} key = {{admin_key}}

95
unit_tests/test_ceph.py Normal file
View File

@ -0,0 +1,95 @@
import unittest
import mock
import ceph
class CephTestCase(unittest.TestCase):
def setUp(self):
super(CephTestCase, self).setUp()
@staticmethod
def populated_config_side_effect(key):
return {
'user-keys':
'client.cinder-ceph:AQAij2tbMNjMOhAAqInpXQLFrltDgmYid6KXbg== '
'client.glance:AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g== '
'client.gnocchi:AQDk7qJb0csAFRAAQqPU6HchVW3PT6ymgXdI/A== '
'client.nova-compute-kvm:'
'AQBkjmtb1hWxLxAA3UhxSblgFSCtHVoZ8W6rNQ== '
'client.radosgw.gateway:'
'AQBljmtb65mrHhAAGy9VRkfsatWVLb9EpoWDfw==',
'admin-user': 'client.myadmin'
}[key]
@staticmethod
def empty_config_side_effect(key):
return {
'user-keys': '',
'admin-user': 'client.myadmin'
}[key]
@mock.patch('ceph.config')
def test_config_user_key_populated(self, mock_config):
user_name = 'glance'
user_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g=='
mock_config.side_effect = self.populated_config_side_effect
named_key = ceph._config_user_key(user_name)
self.assertEqual(user_key, named_key)
@mock.patch('ceph.config')
def test_config_empty_user_key(self, mock_config):
user_name = 'cinder-ceph'
mock_config.side_effect = self.empty_config_side_effect
named_key = ceph._config_user_key(user_name)
self.assertEqual(named_key, None)
@mock.patch('ceph.config')
def test_get_named_key_populated(self, mock_config):
user_name = 'glance'
user_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g=='
mock_config.side_effect = self.populated_config_side_effect
named_key = ceph.get_named_key(user_name)
self.assertEqual(user_key, named_key)
@mock.patch('subprocess.check_output')
@mock.patch('ceph.get_unit_hostname')
@mock.patch('ceph.ceph_user')
@mock.patch('ceph.config')
def test_get_named_key_empty(self, mock_config, mock_ceph_user,
mock_get_unit_hostname, mock_check_output):
user_name = 'cinder-ceph'
user_type = 'client'
admin_user = 'client.myadmin'
user_spec = '{}.{}'.format(user_type, user_name)
expected_key = 'AQCnjmtbuEACMxAA7joUmgLIGI4/3LKkPzUy8g=='
expected_output = ('[client.testuser]\n key = {}'
.format(expected_key))
caps = {
'mon': ['allow rw'],
'osd': ['allow rwx']
}
ceph_user = 'ceph'
ceph_proxy_host = 'cephproxy'
mock_get_unit_hostname.return_value = ceph_proxy_host
def check_output_side_effect(cmd):
return {
' '.join(['sudo', '-u', ceph_user, 'ceph', '--name',
admin_user,
'--keyring',
'/var/lib/ceph/mon/ceph-{}/keyring'.format(
ceph_proxy_host),
'auth', 'get-or-create', user_spec, 'mon',
'allow rw', 'osd', 'allow rwx']): expected_output
}[' '.join(cmd)]
mock_check_output.side_effect = check_output_side_effect
mock_config.side_effect = self.empty_config_side_effect
mock_ceph_user.return_value = ceph_user
named_key = ceph.get_named_key(user_name, caps)
self.assertEqual(named_key, expected_key)

View File

@ -76,6 +76,7 @@ class TestHooks(test_utils.CharmTestCase):
self.test_config.set('monitor-hosts', '127.0.0.1:1234') self.test_config.set('monitor-hosts', '127.0.0.1:1234')
self.test_config.set('fsid', 'abc123') self.test_config.set('fsid', 'abc123')
self.test_config.set('admin-key', 'key123') self.test_config.set('admin-key', 'key123')
self.test_config.set('admin-user', 'client.myadmin')
def c(k): def c(k):
x = {'radosgw': ['rados:1'], x = {'radosgw': ['rados:1'],
@ -105,10 +106,15 @@ class TestHooks(test_utils.CharmTestCase):
'/etc/ceph/ceph.conf', '/etc/ceph/ceph.conf',
'%s/ceph.conf' % dirname, '%s/ceph.conf' % dirname,
100) 100)
keyring = 'ceph.client.admin.keyring' keyring_template = 'ceph.keyring'
context = {'admin_key': self.test_config.get('admin-key')} keyring_name = 'ceph.{}.keyring'.format(
self.render.assert_any_call(keyring, self.test_config.get('admin-user'))
'/etc/ceph/' + keyring, context = {
'admin_key': self.test_config.get('admin-key'),
'admin_user': self.test_config.get('admin-user'),
}
self.render.assert_any_call(keyring_template,
'/etc/ceph/' + keyring_name,
context, owner='ceph-user', perms=0o600) context, owner='ceph-user', perms=0o600)
mock_rgw_rel.assert_called_with(relid='rados:1', unit='rados/1') mock_rgw_rel.assert_called_with(relid='rados:1', unit='rados/1')