diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index aaed2e52..fce78609 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 39 +LIBPATCH = 41 PYDEPS = ["ops>=2.0.0"] @@ -391,6 +391,10 @@ class IllegalOperationError(DataInterfacesError): """To be used when an operation is not allowed to be performed.""" +class PrematureDataAccessError(DataInterfacesError): + """To be raised when the Relation Data may be accessed (written) before protocol init complete.""" + + ############################################################################## # Global helpers / utilities ############################################################################## @@ -1277,7 +1281,6 @@ def _delete_relation_data_without_secrets( str(field), str(relation.id), ) - pass # Public interface methods # Handling Relation Fields seamlessly, regardless if in databag or a Juju Secret @@ -1453,6 +1456,8 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: class ProviderData(Data): """Base provides-side of the data products relation.""" + RESOURCE_FIELD = "database" + def __init__( self, model: Model, @@ -1536,21 +1541,26 @@ def _delete_relation_secret( secret = self._get_relation_secret(relation.id, group) if not secret: - logging.error("Can't delete secret for relation %s", str(relation.id)) + logging.debug( + "Can't delete secrets from group '%s' (relation ID: %s)", + str(group), + str(relation.id), + ) return False old_content = secret.get_content() new_content = copy.deepcopy(old_content) - for field in fields: + for field in set(fields) & set(secret_fields): try: new_content.pop(field) except KeyError: logging.debug( - "Non-existing secret was attempted to be removed %s, %s", - str(relation.id), + "Non-existing secret '%s' was attempted to be removed (relation ID: %s)", str(field), + str(relation.id), ) - return False + if old_content == new_content: + return False # Remove secret from the relation if it's fully gone if not new_content: @@ -1618,6 +1628,15 @@ def _fetch_my_specific_relation_data( def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: """Set values for fields not caring whether it's a secret or not.""" req_secret_fields = [] + + keys = set(data.keys()) + if self.fetch_relation_field(relation.id, self.RESOURCE_FIELD) is None and ( + keys - {"endpoints", "read-only-endpoints", "replset"} + ): + raise PrematureDataAccessError( + "Premature access to relation data, update is forbidden before the connection is initialized." + ) + if relation.app: req_secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) @@ -3290,6 +3309,8 @@ class KafkaRequiresEvents(CharmEvents): class KafkaProviderData(ProviderData): """Provider-side of the Kafka relation.""" + RESOURCE_FIELD = "topic" + def __init__(self, model: Model, relation_name: str) -> None: super().__init__(model, relation_name) @@ -3539,6 +3560,8 @@ class OpenSearchRequiresEvents(CharmEvents): class OpenSearchProvidesData(ProviderData): """Provider-side of the OpenSearch relation.""" + RESOURCE_FIELD = "index" + def __init__(self, model: Model, relation_name: str) -> None: super().__init__(model, relation_name) diff --git a/lib/charms/mongodb/v0/config_server_interface.py b/lib/charms/mongodb/v0/config_server_interface.py index 44e485bb..fd5d46c8 100644 --- a/lib/charms/mongodb/v0/config_server_interface.py +++ b/lib/charms/mongodb/v0/config_server_interface.py @@ -13,6 +13,7 @@ DatabaseProvides, DatabaseRequestedEvent, DatabaseRequires, + PrematureDataAccessError, ) from charms.mongodb.v0.mongo import MongoConnection from charms.mongodb.v1.mongos import MongosConnection @@ -158,15 +159,12 @@ def _on_database_requested(self, event: DatabaseRequestedEvent | RelationChanged def _on_relation_changed(self, event: RelationChangedEvent) -> None: """Handles providing mongos with KeyFile and hosts.""" - # First we need to ensure that the database requested event has run - # otherwise we risk the chance of writing secrets in plain sight. - if not self.database_provides.fetch_relation_field(event.relation.id, "database"): + try: + self._on_database_requested(event) + except PrematureDataAccessError: logger.info("Database Requested has not run yet, skipping.") event.defer() - return - # TODO : This workflow is a fix until we have time for a better and complete fix (DPE-5513) - self._on_database_requested(event) def _on_relation_broken(self, event) -> None: if self.charm.upgrade_in_progress: diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index ab21e75c..b2ce994d 100644 --- a/lib/charms/mongodb/v1/mongodb_provider.py +++ b/lib/charms/mongodb/v1/mongodb_provider.py @@ -539,18 +539,6 @@ def remove_all_relational_users(self): fields = self.database_provides.fetch_my_relation_data([relation.id])[relation.id] self.database_provides.delete_relation_data(relation.id, fields=list(fields)) - # unforatunately the above doesn't work to remove secrets, so we forcibly remove the - # rest manually remove the secret before clearing the databag - for unit in relation.units: - secret_id = json.loads(relation.data[unit]["data"])["secret-user"] - # secret id is the same on all units for `secret-user` - break - - user_secrets = self.charm.model.get_secret(id=secret_id) - user_secrets.remove_all_revisions() - user_secrets.get_content(refresh=True) - relation.data[self.charm.app].clear() - @staticmethod def _get_database_from_relation(relation: Relation) -> Optional[str]: """Return database name from relation."""