Use juju secrets for admin/service passwords

Currently the admin and service passwords are saved
in plain text in the relation data. Use juju secrets
and save the secret id in the relation data.

Make zaza-smoke-tests job optional as this patch is
not compatible with glance-k8s on channel edge.

Change-Id: Iab3d60e314d606da05acd155f7900b9baf66b53b
This commit is contained in:
Hemanth Nakkina
2022-12-06 13:56:12 +05:30
parent ef6be0e060
commit 4705d82fbe
4 changed files with 610 additions and 12 deletions

View File

@@ -2,7 +2,14 @@
templates:
- openstack-python3-charm-yoga-jobs
- openstack-cover-jobs
- microk8s-func-test
# - microk8s-func-test
check:
jobs:
- charmbuild:
nodeset: ubuntu-focal
- zaza-smoke-test:
nodeset: ubuntu-focal
voting: false
vars:
charm_build_name: keystone-k8s
juju_channel: 3.1/stable

View File

@@ -0,0 +1,518 @@
"""IdentityServiceProvides and Requires module.
This library contains the Requires and Provides classes for handling
the identity_service interface.
Import `IdentityServiceRequires` in your charm, with the charm object and the
relation name:
- self
- "identity_service"
Also provide additional parameters to the charm object:
- service
- internal_url
- public_url
- admin_url
- region
- username
- vhost
Two events are also available to respond to:
- connected
- ready
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
class IdentityServiceClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# IdentityService Requires
self.identity_service = IdentityServiceRequires(
self, "identity_service",
service = "my-service"
internal_url = "http://internal-url"
public_url = "http://public-url"
admin_url = "http://admin-url"
region = "region"
)
self.framework.observe(
self.identity_service.on.connected, self._on_identity_service_connected)
self.framework.observe(
self.identity_service.on.ready, self._on_identity_service_ready)
self.framework.observe(
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
def _on_identity_service_connected(self, event):
'''React to the IdentityService connected event.
This event happens when n IdentityService relation is added to the
model before credentials etc have been provided.
'''
# Do something before the relation is complete
pass
def _on_identity_service_ready(self, event):
'''React to the IdentityService ready event.
The IdentityService interface will use the provided config for the
request to the identity server.
'''
# IdentityService Relation is ready. Do something with the completed relation.
pass
def _on_identity_service_goneaway(self, event):
'''React to the IdentityService goneaway event.
This event happens when an IdentityService relation is removed.
'''
# IdentityService Relation has goneaway. shutdown services or suchlike
pass
```
"""
import json
import logging
from ops.framework import (
StoredState,
EventBase,
ObjectEvents,
EventSource,
Object,
)
from ops.model import (
Relation,
SecretNotFoundError,
)
logger = logging.getLogger(__name__)
# The unique Charmhub library identifier, never change it
LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
# Increment this major API version when introducing breaking changes
LIBAPI = 1
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 0
logger = logging.getLogger(__name__)
class IdentityServiceConnectedEvent(EventBase):
"""IdentityService connected Event."""
pass
class IdentityServiceReadyEvent(EventBase):
"""IdentityService ready for use Event."""
pass
class IdentityServiceGoneAwayEvent(EventBase):
"""IdentityService relation has gone-away Event"""
pass
class IdentityServiceServerEvents(ObjectEvents):
"""Events class for `on`"""
connected = EventSource(IdentityServiceConnectedEvent)
ready = EventSource(IdentityServiceReadyEvent)
goneaway = EventSource(IdentityServiceGoneAwayEvent)
class IdentityServiceRequires(Object):
"""
IdentityServiceRequires class
"""
on = IdentityServiceServerEvents()
_stored = StoredState()
def __init__(self, charm, relation_name: str, service_endpoints: dict,
region: str):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.service_endpoints = service_endpoints
self.region = region
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_service_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_departed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_service_relation_broken,
)
def _on_identity_service_relation_joined(self, event):
"""IdentityService relation joined."""
logging.debug("IdentityService on_joined")
self.on.connected.emit()
self.register_services(
self.service_endpoints,
self.region)
def _on_identity_service_relation_changed(self, event):
"""IdentityService relation changed."""
logging.debug("IdentityService on_changed")
try:
self.service_password
self.on.ready.emit()
except (AttributeError, KeyError):
pass
def _on_identity_service_relation_broken(self, event):
"""IdentityService relation broken."""
logging.debug("IdentityService on_broken")
self.on.goneaway.emit()
@property
def _identity_service_rel(self) -> Relation:
"""The IdentityService relation."""
return self.framework.model.get_relation(self.relation_name)
def get_remote_app_data(self, key: str) -> str:
"""Return the value for the given key from remote app data."""
data = self._identity_service_rel.data[self._identity_service_rel.app]
return data.get(key)
@property
def api_version(self) -> str:
"""Return the api_version."""
return self.get_remote_app_data('api-version')
@property
def auth_host(self) -> str:
"""Return the auth_host."""
return self.get_remote_app_data('auth-host')
@property
def auth_port(self) -> str:
"""Return the auth_port."""
return self.get_remote_app_data('auth-port')
@property
def auth_protocol(self) -> str:
"""Return the auth_protocol."""
return self.get_remote_app_data('auth-protocol')
@property
def internal_host(self) -> str:
"""Return the internal_host."""
return self.get_remote_app_data('internal-host')
@property
def internal_port(self) -> str:
"""Return the internal_port."""
return self.get_remote_app_data('internal-port')
@property
def internal_protocol(self) -> str:
"""Return the internal_protocol."""
return self.get_remote_app_data('internal-protocol')
@property
def admin_domain_name(self) -> str:
"""Return the admin_domain_name."""
return self.get_remote_app_data('admin-domain-name')
@property
def admin_domain_id(self) -> str:
"""Return the admin_domain_id."""
return self.get_remote_app_data('admin-domain-id')
@property
def admin_project_name(self) -> str:
"""Return the admin_project_name."""
return self.get_remote_app_data('admin-project-name')
@property
def admin_project_id(self) -> str:
"""Return the admin_project_id."""
return self.get_remote_app_data('admin-project-id')
@property
def admin_user_name(self) -> str:
"""Return the admin_user_name."""
return self.get_remote_app_data('admin-user-name')
@property
def admin_user_id(self) -> str:
"""Return the admin_user_id."""
return self.get_remote_app_data('admin-user-id')
@property
def service_domain_name(self) -> str:
"""Return the service_domain_name."""
return self.get_remote_app_data('service-domain-name')
@property
def service_domain_id(self) -> str:
"""Return the service_domain_id."""
return self.get_remote_app_data('service-domain-id')
@property
def service_host(self) -> str:
"""Return the service_host."""
return self.get_remote_app_data('service-host')
@property
def service_credentials(self) -> str:
"""Return the service_credentials secret."""
return self.get_remote_app_data('service-credentials')
@property
def service_password(self) -> str:
"""Return the service_password."""
credentials_id = self.get_remote_app_data('service-credentials')
if not credentials_id:
return None
try:
credentials = self.charm.model.get_secret(id=credentials_id)
return credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning(f"Secret {credentials_id} not found")
return None
@property
def service_port(self) -> str:
"""Return the service_port."""
return self.get_remote_app_data('service-port')
@property
def service_protocol(self) -> str:
"""Return the service_protocol."""
return self.get_remote_app_data('service-protocol')
@property
def service_project_name(self) -> str:
"""Return the service_project_name."""
return self.get_remote_app_data('service-project-name')
@property
def service_project_id(self) -> str:
"""Return the service_project_id."""
return self.get_remote_app_data('service-project-id')
@property
def service_user_name(self) -> str:
"""Return the service_user_name."""
credentials_id = self.get_remote_app_data('service-credentials')
if not credentials_id:
return None
try:
credentials = self.charm.model.get_secret(id=credentials_id)
return credentials.get_content().get("username")
except SecretNotFoundError:
logger.warning(f"Secret {credentials_id} not found")
return None
@property
def service_user_id(self) -> str:
"""Return the service_user_id."""
return self.get_remote_app_data('service-user-id')
@property
def internal_auth_url(self) -> str:
"""Return the internal_auth_url."""
return self.get_remote_app_data('internal-auth-url')
@property
def admin_auth_url(self) -> str:
"""Return the admin_auth_url."""
return self.get_remote_app_data('admin-auth-url')
@property
def public_auth_url(self) -> str:
"""Return the public_auth_url."""
return self.get_remote_app_data('public-auth-url')
def register_services(self, service_endpoints: dict,
region: str) -> None:
"""Request access to the IdentityService server."""
if self.model.unit.is_leader():
logging.debug("Requesting service registration")
app_data = self._identity_service_rel.data[self.charm.app]
app_data["service-endpoints"] = json.dumps(
service_endpoints, sort_keys=True
)
app_data["region"] = region
class HasIdentityServiceClientsEvent(EventBase):
"""Has IdentityServiceClients Event."""
pass
class ReadyIdentityServiceClientsEvent(EventBase):
"""IdentityServiceClients Ready Event."""
def __init__(self, handle, relation_id, relation_name, service_endpoints,
region, client_app_name):
super().__init__(handle)
self.relation_id = relation_id
self.relation_name = relation_name
self.service_endpoints = service_endpoints
self.region = region
self.client_app_name = client_app_name
def snapshot(self):
return {
"relation_id": self.relation_id,
"relation_name": self.relation_name,
"service_endpoints": self.service_endpoints,
"client_app_name": self.client_app_name,
"region": self.region}
def restore(self, snapshot):
super().restore(snapshot)
self.relation_id = snapshot["relation_id"]
self.relation_name = snapshot["relation_name"]
self.service_endpoints = snapshot["service_endpoints"]
self.region = snapshot["region"]
self.client_app_name = snapshot["client_app_name"]
class IdentityServiceClientEvents(ObjectEvents):
"""Events class for `on`"""
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
class IdentityServiceProvides(Object):
"""
IdentityServiceProvides class
"""
on = IdentityServiceClientEvents()
_stored = StoredState()
def __init__(self, charm, relation_name):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_service_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_service_relation_broken,
)
def _on_identity_service_relation_joined(self, event):
"""Handle IdentityService joined."""
logging.debug("IdentityService on_joined")
self.on.has_identity_service_clients.emit()
def _on_identity_service_relation_changed(self, event):
"""Handle IdentityService changed."""
logging.debug("IdentityService on_changed")
REQUIRED_KEYS = [
'service-endpoints',
'region']
values = [
event.relation.data[event.relation.app].get(k)
for k in REQUIRED_KEYS
]
# Validate data on the relation
if all(values):
service_eps = json.loads(
event.relation.data[event.relation.app]['service-endpoints'])
self.on.ready_identity_service_clients.emit(
event.relation.id,
event.relation.name,
service_eps,
event.relation.data[event.relation.app]['region'],
event.relation.app.name)
def _on_identity_service_relation_broken(self, event):
"""Handle IdentityService broken."""
logging.debug("IdentityServiceProvides on_departed")
# TODO clear data on the relation
def set_identity_service_credentials(self, relation_name: int,
relation_id: str,
api_version: str,
auth_host: str,
auth_port: str,
auth_protocol: str,
internal_host: str,
internal_port: str,
internal_protocol: str,
service_host: str,
service_port: str,
service_protocol: str,
admin_domain: str,
admin_project: str,
admin_user: str,
service_domain: str,
service_project: str,
service_user: str,
internal_auth_url: str,
admin_auth_url: str,
public_auth_url: str,
service_credentials: str):
logging.debug("Setting identity_service connection information.")
_identity_service_rel = None
for relation in self.framework.model.relations[relation_name]:
if relation.id == relation_id:
_identity_service_rel = relation
if not _identity_service_rel:
# Relation has disappeared so skip send of data
return
app_data = _identity_service_rel.data[self.charm.app]
app_data["api-version"] = api_version
app_data["auth-host"] = auth_host
app_data["auth-port"] = str(auth_port)
app_data["auth-protocol"] = auth_protocol
app_data["internal-host"] = internal_host
app_data["internal-port"] = str(internal_port)
app_data["internal-protocol"] = internal_protocol
app_data["service-host"] = service_host
app_data["service-port"] = str(service_port)
app_data["service-protocol"] = service_protocol
app_data["admin-domain-name"] = admin_domain.name
app_data["admin-domain-id"] = admin_domain.id
app_data["admin-project-name"] = admin_project.name
app_data["admin-project-id"] = admin_project.id
app_data["admin-user-name"] = admin_user.name
app_data["admin-user-id"] = admin_user.id
app_data["service-domain-name"] = service_domain.name
app_data["service-domain-id"] = service_domain.id
app_data["service-project-name"] = service_project.name
app_data["service-project-id"] = service_project.id
app_data["service-user-id"] = service_user.id
app_data["internal-auth-url"] = internal_auth_url
app_data["admin-auth-url"] = admin_auth_url
app_data["public-auth-url"] = public_auth_url
app_data["service-credentials"] = service_credentials

View File

@@ -32,7 +32,7 @@ from typing import (
)
import charms.keystone_k8s.v0.cloud_credentials as sunbeam_cc_svc
import charms.keystone_k8s.v0.identity_service as sunbeam_id_svc
import charms.keystone_k8s.v1.identity_service as sunbeam_id_svc
import ops.charm
import ops.pebble
import ops_sunbeam.charm as sunbeam_charm
@@ -55,6 +55,7 @@ from ops.main import (
)
from ops.model import (
MaintenanceStatus,
SecretNotFoundError,
SecretRotate,
)
from ops_sunbeam.interfaces import (
@@ -424,6 +425,11 @@ export OS_AUTH_VERSION=3
self.keystone_manager.write_keys(
key_repository="/etc/keystone/credential-keys", keys=keys
)
else:
# By default read the latest content of secret
# this will allow juju to trigger secret-remove
# event for old revision
event.secret.get_content(refresh=True)
def _on_secret_rotate(self, event: ops.charm.SecretRotateEvent):
if not self.unit.is_leader():
@@ -462,6 +468,7 @@ export OS_AUTH_VERSION=3
if (
event.secret.label == "fernet-keys"
or event.secret.label == "credential-keys"
or event.secret.label == f"credentials_{self.admin_user}"
):
# TODO: Remove older revisions of the secret
# event.secret.remove_revision(event.revision)
@@ -546,9 +553,21 @@ export OS_AUTH_VERSION=3
service_username = "svc_{}".format(
event.client_app_name.replace("-", "_")
)
service_password = self.password_manager.retrieve_or_set(
service_username
event_relation = self.model.get_relation(
event.relation_name, event.relation_id
)
scope = {"relation": event_relation}
service_credentials = None
service_password = None
try:
service_credentials = self._retrieve_or_set_secret(
service_username, scope
)
credentials = self.model.get_secret(id=service_credentials)
service_password = credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning(f"Secret for {service_username} not found")
service_user = self.keystone_manager.create_user(
name=service_username,
password=service_password,
@@ -592,12 +611,12 @@ export OS_AUTH_VERSION=3
admin_project,
admin_user,
service_domain,
service_password,
service_project,
service_user,
self.internal_endpoint,
self.admin_endpoint,
self.public_endpoint,
service_credentials,
)
def add_credentials(self, event):
@@ -632,7 +651,20 @@ export OS_AUTH_VERSION=3
service_project = self.keystone_manager.get_project(
name=self.service_project, domain=service_domain
)
user_password = self.password_manager.retrieve_or_set(event.username)
event_relation = self.model.get_relation(
event.relation_name, event.relation_id
)
scope = {"relation": event_relation}
user_password = None
try:
credentials_id = self._retrieve_or_set_secret(
event.username, scope
)
credentials = self.model.get_secret(id=credentials_id)
user_password = credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning(f"Secret for {event.username} not found")
service_user = self.keystone_manager.create_user(
name=event.username,
password=user_password,
@@ -691,7 +723,14 @@ export OS_AUTH_VERSION=3
service_project = self.keystone_manager.get_project(
name=self.service_project, domain=service_domain
)
user_password = self.password_manager.retrieve_or_set(username)
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,
@@ -723,6 +762,26 @@ export OS_AUTH_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."""
@@ -746,7 +805,14 @@ export OS_AUTH_VERSION=3
@property
def admin_password(self) -> str:
"""Retrieve the password for the Admin user."""
return self.password_manager.retrieve_or_set(self.admin_user)
try:
credentials_id = self._retrieve_or_set_secret(self.admin_user)
credentials = self.model.get_secret(id=credentials_id)
return credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning("Secret for admin credentials not found")
return None
@property
def admin_user(self):
@@ -770,7 +836,14 @@ export OS_AUTH_VERSION=3
@property
def charm_password(self) -> str:
"""The password for the charm admin user."""
return self.password_manager.retrieve_or_set(self.charm_user)
try:
credentials_id = self._retrieve_or_set_secret(self.charm_user)
credentials = self.model.get_secret(id=credentials_id)
return credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning("Secret for charm credentials not found")
return None
@property
def service_project(self):

View File

@@ -195,6 +195,7 @@ class TestKeystoneOperatorCharm(test_utils.CharmTestCase):
rel_data = self.harness.get_relation_data(
identity_rel_id, self.harness.charm.unit.app.name
)
secret_svc_cinder = self.get_secret_by_label("credentials_svc_cinder")
self.maxDiff = None
self.assertEqual(
rel_data,
@@ -218,13 +219,12 @@ class TestKeystoneOperatorCharm(test_utils.CharmTestCase):
"service-domain-id": "sdomain_id",
"service-domain-name": "sdomain_name",
"service-host": "10.0.0.10",
"service-password": "randonpassword",
"service-credentials": secret_svc_cinder,
"service-port": "5000",
"service-project-id": "aproject_id",
"service-project-name": "aproject_name",
"service-protocol": "http",
"service-user-id": "suser_id",
"service-user-name": "suser_name",
},
)
@@ -239,7 +239,7 @@ class TestKeystoneOperatorCharm(test_utils.CharmTestCase):
"leader_ready": "true",
"fernet-secret-id": fernet_secret_id,
"credential-keys-secret-id": credential_secret_id,
"password_svc_cinder": "randonpassword",
"credentials_svc_cinder": secret_svc_cinder,
},
)