Add actions to regenerate password
Add regenerate-password action to regenerate passwords for admin, charm or service user. Remove KeystonepasswordManager class. Update zaza-smoke-test job to mandatory. Change-Id: I542894149b533da4cba5371f7412d4cbcc21e305
This commit is contained in:
parent
4705d82fbe
commit
5009f76997
@ -2,14 +2,7 @@
|
||||
templates:
|
||||
- openstack-python3-charm-yoga-jobs
|
||||
- openstack-cover-jobs
|
||||
# - microk8s-func-test
|
||||
check:
|
||||
jobs:
|
||||
- charmbuild:
|
||||
nodeset: ubuntu-focal
|
||||
- zaza-smoke-test:
|
||||
nodeset: ubuntu-focal
|
||||
voting: false
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: keystone-k8s
|
||||
juju_channel: 3.1/stable
|
||||
|
@ -16,3 +16,14 @@ get-service-account:
|
||||
required:
|
||||
- username
|
||||
additionalProperties: False
|
||||
|
||||
regenerate-password:
|
||||
description: |
|
||||
Regenerate password for the given user.
|
||||
params:
|
||||
username:
|
||||
type: string
|
||||
description: The username for the account.
|
||||
required:
|
||||
- username
|
||||
additionalProperties: False
|
||||
|
@ -47,7 +47,6 @@ from ops.charm import (
|
||||
RelationChangedEvent,
|
||||
)
|
||||
from ops.framework import (
|
||||
Object,
|
||||
StoredState,
|
||||
)
|
||||
from ops.main import (
|
||||
@ -58,9 +57,6 @@ from ops.model import (
|
||||
SecretNotFoundError,
|
||||
SecretRotate,
|
||||
)
|
||||
from ops_sunbeam.interfaces import (
|
||||
OperatorPeers,
|
||||
)
|
||||
|
||||
from utils import (
|
||||
manager,
|
||||
@ -202,43 +198,6 @@ class CloudCredentialsProvidesHandler(sunbeam_rhandlers.RelationHandler):
|
||||
return True
|
||||
|
||||
|
||||
class KeystonePasswordManager(Object):
|
||||
"""Helper for management of keystone credential passwords."""
|
||||
|
||||
def __init__(self, charm: ops.charm.CharmBase, interface: OperatorPeers):
|
||||
self.charm = charm
|
||||
self.interface = interface
|
||||
|
||||
def store(self, username: str, password: str):
|
||||
"""Store username and password."""
|
||||
logging.debug(f"Storing password for {username}")
|
||||
self.interface.set_app_data(
|
||||
{
|
||||
f"password_{username}": password,
|
||||
}
|
||||
)
|
||||
|
||||
def retrieve(self, username: str) -> str:
|
||||
"""Retrieve persisted password for provided username."""
|
||||
if not self.interface:
|
||||
return None
|
||||
|
||||
password = self.interface.get_app_data(f"password_{username}")
|
||||
return str(password) if password else None
|
||||
|
||||
def retrieve_or_set(self, username: str) -> str:
|
||||
"""Retrieve or setup a password for a user.
|
||||
|
||||
New passwords will only be created by the lead unit of the
|
||||
application.
|
||||
"""
|
||||
password = self.retrieve(username)
|
||||
if not password and self.charm.unit.is_leader():
|
||||
password = pwgen.pwgen(12)
|
||||
self.store(username, password)
|
||||
return password
|
||||
|
||||
|
||||
class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Charm the service."""
|
||||
|
||||
@ -278,17 +237,37 @@ class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self.framework.observe(
|
||||
self.on.get_admin_password_action, self._get_admin_password_action
|
||||
)
|
||||
|
||||
self.framework.observe(
|
||||
self.on.get_admin_account_action, self._get_admin_account_action
|
||||
)
|
||||
|
||||
self.password_manager = KeystonePasswordManager(self, self.peers)
|
||||
|
||||
self.framework.observe(
|
||||
self.on.get_service_account_action,
|
||||
self._get_service_account_action,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.on.regenerate_password_action,
|
||||
self._regenerate_password_action,
|
||||
)
|
||||
|
||||
def _retrieve_or_set_secret(self, username: str, scope: dict = {}) -> str:
|
||||
credentials_id = self.peers.get_app_data(f"credentials_{username}")
|
||||
if credentials_id:
|
||||
return credentials_id
|
||||
|
||||
password = pwgen.pwgen(12)
|
||||
credentials_secret = self.model.app.add_secret(
|
||||
{"username": username, "password": password},
|
||||
label=f"credentials_{username}",
|
||||
)
|
||||
self.peers.set_app_data(
|
||||
{
|
||||
f"credentials_{username}": credentials_secret.id,
|
||||
}
|
||||
)
|
||||
if "relation" in scope:
|
||||
credentials_secret.grant(scope["relation"])
|
||||
|
||||
return credentials_secret.id
|
||||
|
||||
def _get_admin_password_action(self, event: ActionEvent) -> None:
|
||||
if not self.unit.is_leader():
|
||||
@ -330,6 +309,90 @@ export OS_AUTH_VERSION=3
|
||||
}
|
||||
)
|
||||
|
||||
def _get_service_account_action(self, event: ActionEvent) -> None:
|
||||
"""Create/get details for a service account.
|
||||
|
||||
This action handler will create a new services account
|
||||
for the provided username. This account can be used
|
||||
to provide access to OpenStack services from outside
|
||||
of the Charmed deployment.
|
||||
"""
|
||||
if not self.unit.is_leader():
|
||||
event.fail("Please run action on lead unit.")
|
||||
return
|
||||
|
||||
# TODO: refactor into general helper method.
|
||||
username = event.params["username"]
|
||||
service_domain = self.keystone_manager.create_domain(
|
||||
name="service_domain", may_exist=True
|
||||
)
|
||||
service_project = self.keystone_manager.get_project(
|
||||
name=self.service_project, domain=service_domain
|
||||
)
|
||||
user_password = None
|
||||
try:
|
||||
credentials_id = self._retrieve_or_set_secret(username)
|
||||
credentials = self.model.get_secret(id=credentials_id)
|
||||
user_password = credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning("Secret for {username} not found")
|
||||
|
||||
service_user = self.keystone_manager.create_user(
|
||||
name=username,
|
||||
password=user_password,
|
||||
domain=service_domain.id,
|
||||
may_exist=True,
|
||||
)
|
||||
admin_role = self.keystone_manager.create_role(
|
||||
name=self.admin_role, may_exist=True
|
||||
)
|
||||
# TODO(wolsen) let's not always grant admin role!
|
||||
self.keystone_manager.grant_role(
|
||||
role=admin_role,
|
||||
user=service_user,
|
||||
project=service_project,
|
||||
may_exist=True,
|
||||
)
|
||||
|
||||
event.set_results(
|
||||
{
|
||||
"username": username,
|
||||
"password": user_password,
|
||||
"user-domain-name": service_domain.name,
|
||||
"project-name": service_project.name,
|
||||
"project-domain-name": service_domain.name,
|
||||
"region": self.model.config["region"],
|
||||
"internal-endpoint": self.internal_endpoint,
|
||||
"public-endpoint": self.public_endpoint,
|
||||
"api-version": 3,
|
||||
}
|
||||
)
|
||||
|
||||
def _regenerate_password_action(self, event: ActionEvent) -> None:
|
||||
"""Regenerate password for a user account.
|
||||
|
||||
This action handler will update the user account
|
||||
with a new password.
|
||||
"""
|
||||
if not self.unit.is_leader():
|
||||
event.fail("Please run action on lead unit.")
|
||||
return
|
||||
|
||||
username = event.params["username"]
|
||||
try:
|
||||
credentials_id = self._retrieve_or_set_secret(username)
|
||||
credentials = self.model.get_secret(id=credentials_id)
|
||||
password = pwgen.pwgen(12)
|
||||
self.keystone_manager.update_user(name=username, password=password)
|
||||
credentials.set_content(
|
||||
{"username": username, "password": password}
|
||||
)
|
||||
event.set_results({"password": password})
|
||||
except SecretNotFoundError:
|
||||
event.fail(f"Secret for {username} not found")
|
||||
except Exception as e:
|
||||
event.fail(f"Regeneration of password failed: {e}")
|
||||
|
||||
def _on_peer_data_changed(self, event: RelationChangedEvent):
|
||||
"""Check the peer data updates for updated fernet keys.
|
||||
|
||||
@ -469,6 +532,7 @@ export OS_AUTH_VERSION=3
|
||||
event.secret.label == "fernet-keys"
|
||||
or event.secret.label == "credential-keys"
|
||||
or event.secret.label == f"credentials_{self.admin_user}"
|
||||
or event.secret.label == f"credentials_{self.charm_user}"
|
||||
):
|
||||
# TODO: Remove older revisions of the secret
|
||||
# event.secret.remove_revision(event.revision)
|
||||
@ -703,85 +767,6 @@ export OS_AUTH_VERSION=3
|
||||
region=self.model.config["region"], # XXX(wolsen) region matters?
|
||||
)
|
||||
|
||||
def _get_service_account_action(self, event: ActionEvent) -> None:
|
||||
"""Create/get details for a service account.
|
||||
|
||||
This action handler will create a new services account
|
||||
for the provided username. This account can be used
|
||||
to provide access to OpenStack services from outside
|
||||
of the Charmed deployment.
|
||||
"""
|
||||
if not self.unit.is_leader():
|
||||
event.fail("Please run action on lead unit.")
|
||||
return
|
||||
|
||||
# TODO: refactor into general helper method.
|
||||
username = event.params["username"]
|
||||
service_domain = self.keystone_manager.create_domain(
|
||||
name="service_domain", may_exist=True
|
||||
)
|
||||
service_project = self.keystone_manager.get_project(
|
||||
name=self.service_project, domain=service_domain
|
||||
)
|
||||
user_password = None
|
||||
try:
|
||||
credentials_id = self._retrieve_or_set_secret(username)
|
||||
credentials = self.model.get_secret(id=credentials_id)
|
||||
user_password = credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning("Secret for {username} not found")
|
||||
|
||||
service_user = self.keystone_manager.create_user(
|
||||
name=username,
|
||||
password=user_password,
|
||||
domain=service_domain.id,
|
||||
may_exist=True,
|
||||
)
|
||||
admin_role = self.keystone_manager.create_role(
|
||||
name=self.admin_role, may_exist=True
|
||||
)
|
||||
# TODO(wolsen) let's not always grant admin role!
|
||||
self.keystone_manager.grant_role(
|
||||
role=admin_role,
|
||||
user=service_user,
|
||||
project=service_project,
|
||||
may_exist=True,
|
||||
)
|
||||
|
||||
event.set_results(
|
||||
{
|
||||
"username": username,
|
||||
"password": user_password,
|
||||
"user-domain-name": service_domain.name,
|
||||
"project-name": service_project.name,
|
||||
"project-domain-name": service_domain.name,
|
||||
"region": self.model.config["region"],
|
||||
"internal-endpoint": self.internal_endpoint,
|
||||
"public-endpoint": self.public_endpoint,
|
||||
"api-version": 3,
|
||||
}
|
||||
)
|
||||
|
||||
def _retrieve_or_set_secret(self, username: str, scope: dict = {}) -> str:
|
||||
credentials_id = self.peers.get_app_data(f"credentials_{username}")
|
||||
if credentials_id:
|
||||
return credentials_id
|
||||
|
||||
password = pwgen.pwgen(12)
|
||||
credentials_secret = self.model.app.add_secret(
|
||||
{"username": username, "password": password},
|
||||
label=f"credentials_{username}",
|
||||
)
|
||||
self.peers.set_app_data(
|
||||
{
|
||||
f"credentials_{username}": credentials_secret.id,
|
||||
}
|
||||
)
|
||||
if "relation" in scope:
|
||||
credentials_secret.grant(scope["relation"])
|
||||
|
||||
return credentials_secret.id
|
||||
|
||||
@property
|
||||
def default_public_ingress_port(self):
|
||||
"""Default public ingress port."""
|
||||
|
@ -584,6 +584,27 @@ class KeystoneManager(framework.Object):
|
||||
|
||||
return None
|
||||
|
||||
def update_user(
|
||||
self,
|
||||
name: str,
|
||||
password: str,
|
||||
email: str = None,
|
||||
project: "Project" = None,
|
||||
domain: "Domain" = None,
|
||||
) -> "User":
|
||||
"""Update password for user."""
|
||||
user = self.get_user(name=name, domain=domain, project=project)
|
||||
user = self.api.users.update(
|
||||
user,
|
||||
name=name,
|
||||
default_project=project,
|
||||
domain=domain,
|
||||
password=password,
|
||||
email=email,
|
||||
)
|
||||
logger.debug(f"Updated user {user.name}.")
|
||||
return user
|
||||
|
||||
def create_role(
|
||||
self,
|
||||
name: str,
|
||||
|
@ -460,31 +460,6 @@ class TestKeystoneOperatorCharm(test_utils.CharmTestCase):
|
||||
)
|
||||
self.assertFalse(self.km_mock.setup_keystone.called)
|
||||
|
||||
def test_password_storage(self):
|
||||
"""Test storing password."""
|
||||
self.harness.set_leader()
|
||||
rel_id = self.harness.add_relation("peers", "keystone-k8s")
|
||||
|
||||
self.harness.charm.password_manager.store("test-user", "foobar")
|
||||
|
||||
self.assertEqual(
|
||||
self.harness.charm.password_manager.retrieve("test-user"), "foobar"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.harness.charm.password_manager.retrieve("unknown-user"), None
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.harness.get_relation_data(
|
||||
rel_id,
|
||||
self.harness.charm.app.name,
|
||||
),
|
||||
{
|
||||
"password_test-user": "foobar",
|
||||
},
|
||||
)
|
||||
|
||||
def test_get_service_account_action(self):
|
||||
"""Test get_service_account action."""
|
||||
self.harness.add_relation("peers", "keystone-k8s")
|
||||
|
Loading…
Reference in New Issue
Block a user