Skip to content

Commit

Permalink
Add rotate-admin-password action
Browse files Browse the repository at this point in the history
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: openstack-charmers/zaza-openstack-tests#720
  • Loading branch information
peterctl committed Mar 21, 2022
1 parent 4949830 commit ae178d7
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 2 deletions.
7 changes: 6 additions & 1 deletion actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
15 changes: 14 additions & 1 deletion actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,21 @@ def _add_path(path):
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.
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions actions/rotate-admin-password
16 changes: 16 additions & 0 deletions hooks/keystone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions unit_tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
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):
Expand Down
54 changes: 54 additions & 0 deletions unit_tests/test_keystone_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2131,3 +2131,57 @@ def test_get_add_role_to_admin(self):
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()

0 comments on commit ae178d7

Please sign in to comment.