Add rotate-admin-password action

This action allows the user to easily rotate the admin user's
password by replacing it with a randomly generated one.

Change-Id: I6ce69be15b11b00f804d3143d835ec3ce6515865
Related-Bug: #1927280
Func-Test-PR: https://github.com/openstack-charmers/zaza-openstack-tests/pull/720
This commit is contained in:
Pedro Castillo 2022-03-08 15:36:29 +00:00
parent 4949830cea
commit ae178d7471
6 changed files with 102 additions and 2 deletions

View File

@ -9,6 +9,11 @@ resume:
Resume keystone services.
If the keystone deployment is clustered using the hacluster charm, the
corresponding hacluster unit on the node must be resumed as well.
rotate-admin-password:
description: |
Rotate the admin user's password.
The current password is replaced with a randomly generated password. The
new password is stored in the leader's admin_passwd bucket.
openstack-upgrade:
description: |
Perform openstack upgrades. Config option action-managed-upgrade must be
@ -18,4 +23,4 @@ security-checklist:
Validate the running configuration against the OpenStack security guides
checklist.
get-admin-password:
description: Retrieve the admin password for the Keystone service.
description: Retrieve the admin password for the Keystone service.

View File

@ -33,12 +33,21 @@ _add_path(_root)
from charmhelpers.core.hookenv import action_fail
from keystone_utils import (
rotate_admin_passwd,
pause_unit_helper,
resume_unit_helper,
register_configs,
)
def rotate_admin_password(args):
"""Rotate the admin user's password.
@raises Exception if keystone client cannot update the password
"""
rotate_admin_passwd()
def pause(args):
"""Pause all the Keystone services.
@ -57,7 +66,11 @@ def resume(args):
# A dictionary of all the defined actions to callables (which take
# parsed arguments).
ACTIONS = {"pause": pause, "resume": resume}
ACTIONS = {
"rotate-admin-password": rotate_admin_password,
"pause": pause,
"resume": resume,
}
def main(args):

View File

@ -0,0 +1 @@
actions.py

View File

@ -1500,6 +1500,22 @@ def set_admin_passwd(passwd, user=None):
_leader_set_secret({'{}_passwd'.format(user): passwd})
def rotate_admin_passwd():
if not is_leader():
raise RuntimeError("This unit is not the leader and therefore can't "
"rotate the admin password.")
admin_passwd = config('admin-password')
if admin_passwd and admin_passwd.strip().lower() != 'none':
raise RuntimeError(
"The 'admin-password' config is present, so the action will be "
"aborted. To allow randomly generated passwords, unset the "
"config value.")
user = config('admin-user')
new_passwd = pwgen(16)
update_user_password(user, new_passwd, ADMIN_DOMAIN)
leader_set({'admin_passwd': new_passwd})
def get_api_version():
api_version = config('preferred-api-version')
cmp_release = CompareOpenStackReleases(

View File

@ -25,6 +25,17 @@ with patch('charmhelpers.contrib.openstack.utils.'
import actions.actions
class ChangeAdminPasswordTestCase(CharmTestCase):
def setUp(self):
super(ChangeAdminPasswordTestCase, self).setUp(
actions.actions, ["rotate_admin_passwd"])
def test_rotate_admin_password(self):
actions.actions.rotate_admin_password([])
self.rotate_admin_passwd.assert_called_once()
class PauseTestCase(CharmTestCase):
def setUp(self):

View File

@ -2131,3 +2131,57 @@ class TestKeystoneUtils(CharmTestCase):
self.assertEqual(
utils.get_add_role_to_admin({}),
[])
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_without_config(
self, is_leader, pwgen, leader_set, update_user_password):
user = 'test-user'
password = 'password'
is_leader.return_value = True
pwgen.return_value = password
self.test_config.set('admin-user', user)
self.test_config.set('admin-password', '')
utils.rotate_admin_passwd()
pwgen.assert_called_once()
update_user_password.assert_called_once_with(
user, password, utils.ADMIN_DOMAIN)
leader_set.assert_called_once_with({'admin_passwd': password})
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_with_config(
self, is_leader, pwgen, leader_set, update_user_password):
user = 'test-user'
password = 'password'
is_leader.return_value = True
self.test_config.set('admin-user', user)
self.test_config.set('admin-password', password)
with self.assertRaises(RuntimeError):
utils.rotate_admin_passwd()
pwgen.assert_not_called()
update_user_password.assert_not_called()
leader_set.assert_not_called()
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_outside_leader(
self, is_leader, pwgen, leader_set, update_user_password):
is_leader.return_value = False
with self.assertRaises(RuntimeError):
utils.rotate_admin_passwd()
pwgen.assert_not_called()
update_user_password.assert_not_called()
leader_set.assert_not_called()