Skip to content

Commit

Permalink
Secret groups extendible
Browse files Browse the repository at this point in the history
  • Loading branch information
juditnovak committed Jan 29, 2024
1 parent 3ffd54a commit 876f7ba
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 37 deletions.
116 changes: 85 additions & 31 deletions lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,33 @@ def _on_topic_requested(self, event: TopicRequestedEvent):
REQ_SECRET_FIELDS = "requested-secrets"


class SecretGroup(Enum):
"""Secret groups as constants."""
class SecretGroup(str):
"""Secret groups specific type."""

USER = "user"
TLS = "tls"
EXTRA = "extra"
pass


class SecretGroupsAggregate(str):
"""Secret groups with option to extend with additional constants."""

def __init__(self):
self.USER = SecretGroup("user")
self.TLS = SecretGroup("tls")
self.EXTRA = SecretGroup("extra")

def __setattr__(self, name, value):
"""Setting internal constants."""
if name in self.__dict__:
raise RuntimeError("Can't set constant!")
else:
super().__setattr__(name, SecretGroup(value))

def groups(self) -> list:
"""Return the list of stored SecretGroups."""
return list(self.__dict__.values())


SECRET_GROUPS = SecretGroupsAggregate()


class DataInterfacesError(Exception):
Expand Down Expand Up @@ -471,6 +492,11 @@ class Scope(Enum):
UNIT = "unit"


################################################################################
# Secrets internal caching
################################################################################


class CachedSecret:
"""Locally cache a secret.
Expand Down Expand Up @@ -586,6 +612,11 @@ def add(self, label: str, content: Dict[str, str], relation: Relation) -> Cached
return self._secrets[label]


################################################################################
# Relation Data base/abstract ancestors (i.e. parent classes)
################################################################################


# Base DataRelation


Expand All @@ -596,11 +627,11 @@ class DataRelation(Object, ABC):

# Local map to associate mappings with secrets potentially as a group
SECRET_LABEL_MAP = {
"username": SecretGroup.USER,
"password": SecretGroup.USER,
"uris": SecretGroup.USER,
"tls": SecretGroup.TLS,
"tls-ca": SecretGroup.TLS,
"username": SECRET_GROUPS.USER,
"password": SECRET_GROUPS.USER,
"uris": SECRET_GROUPS.USER,
"tls": SECRET_GROUPS.TLS,
"tls-ca": SECRET_GROUPS.TLS,
}

def __init__(self, charm: CharmBase, relation_name: str) -> None:
Expand Down Expand Up @@ -633,6 +664,11 @@ def secrets_enabled(self):
self._jujuversion = JujuVersion.from_environ()
return self._jujuversion.has_secrets

@property
def secret_label_map(self):
"""Exposing secret-label map via a property -- could be overridden in descendants!"""
return self.SECRET_LABEL_MAP

# Mandatory overrides for internal/helper methods

@abstractmethod
Expand Down Expand Up @@ -692,11 +728,11 @@ def _generate_secret_label(
relation_name: str, relation_id: int, group_mapping: SecretGroup
) -> str:
"""Generate unique group_mappings for secrets within a relation context."""
return f"{relation_name}.{relation_id}.{group_mapping.value}.secret"
return f"{relation_name}.{relation_id}.{group_mapping}.secret"

def _generate_secret_field_name(self, group_mapping: SecretGroup) -> str:
"""Generate unique group_mappings for secrets within a relation context."""
return f"{PROV_SECRET_PREFIX}{group_mapping.value}"
return f"{PROV_SECRET_PREFIX}{group_mapping}"

def _relation_from_secret_label(self, secret_label: str) -> Optional[Relation]:
"""Retrieve the relation that belongs to a secret label."""
Expand All @@ -721,19 +757,18 @@ def _relation_from_secret_label(self, secret_label: str) -> Optional[Relation]:
except ModelError:
return

@classmethod
def _group_secret_fields(cls, secret_fields: List[str]) -> Dict[SecretGroup, List[str]]:
def _group_secret_fields(self, secret_fields: List[str]) -> Dict[SecretGroup, List[str]]:
"""Helper function to arrange secret mappings under their group.
NOTE: All unrecognized items end up in the 'extra' secret bucket.
Make sure only secret fields are passed!
"""
secret_fieldnames_grouped = {}
for key in secret_fields:
if group := cls.SECRET_LABEL_MAP.get(key):
if group := self.secret_label_map.get(key):
secret_fieldnames_grouped.setdefault(group, []).append(key)
else:
secret_fieldnames_grouped.setdefault(SecretGroup.EXTRA, []).append(key)
secret_fieldnames_grouped.setdefault(SECRET_GROUPS.EXTRA, []).append(key)
return secret_fieldnames_grouped

def _get_group_secret_contents(
Expand All @@ -752,22 +787,21 @@ def _get_group_secret_contents(
return {k: v for k, v in secret_data.items() if k in secret_fields}
return {}

@classmethod
def _content_for_secret_group(
cls, content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup
self, content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup
) -> Dict[str, str]:
"""Select <field>: <value> pairs from input, that belong to this particular Secret group."""
if group_mapping == SecretGroup.EXTRA:
if group_mapping == SECRET_GROUPS.EXTRA:
return {
k: v
for k, v in content.items()
if k in secret_fields and k not in cls.SECRET_LABEL_MAP.keys()
if k in secret_fields and k not in self.secret_label_map.keys()
}

return {
k: v
for k, v in content.items()
if k in secret_fields and cls.SECRET_LABEL_MAP.get(k) == group_mapping
if k in secret_fields and self.secret_label_map.get(k) == group_mapping
}

@juju_secrets_only
Expand Down Expand Up @@ -1338,7 +1372,7 @@ def _register_secrets_to_relation(self, relation: Relation, params_name_list: Li
if not relation.app:
return

for group in SecretGroup:
for group in SECRET_GROUPS.groups():
secret_field = self._generate_secret_field_name(group)
if secret_field in params_name_list:
if secret_uri := relation.data[relation.app].get(secret_field):
Expand Down Expand Up @@ -1463,7 +1497,9 @@ def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None:
fetch_my_relation_field = leader_only(DataRelation.fetch_my_relation_field)


# Base DataPeer
################################################################################
# Peer Relation Data
################################################################################


class DataPeer(DataRequires, DataProvides):
Expand Down Expand Up @@ -1497,6 +1533,16 @@ def scope(self) -> Optional[Scope]:
if isinstance(self.component, Unit):
return Scope.UNIT

def add_secret_field(
self, field: List, individual: bool = False, group_mapping: Optional[SecretGroup] = None
) -> None:
"""Local access to secrets field, in case they are being used."""
if self.secrets_enabled:
self._secret_fields += field
if group_mapping:
pass
# HERE

def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the relation has changed."""
pass
Expand All @@ -1513,15 +1559,15 @@ def _generate_secret_label(
members.append(self.scope.value)
return f"{'.'.join(members)}"

def _generate_secret_field_name(self, group_mapping: SecretGroup = SecretGroup.EXTRA) -> str:
def _generate_secret_field_name(self, group_mapping: SecretGroup = SECRET_GROUPS.EXTRA) -> str:
"""Generate unique group_mappings for secrets within a relation context."""
return f"{self.secret_field_name}"

@juju_secrets_only
def _get_relation_secret(
self,
relation_id: int,
group_mapping: SecretGroup = SecretGroup.EXTRA,
group_mapping: SecretGroup = SECRET_GROUPS.EXTRA,
relation_name: Optional[str] = None,
) -> Optional[CachedSecret]:
"""Retrieve a Juju Secret specifically for peer relations.
Expand Down Expand Up @@ -1672,7 +1718,11 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


# General events
################################################################################
# Cross-charm Relatoins Data Handling and Evenets
################################################################################

# Generic events


class ExtraRoleEvent(RelationEvent):
Expand Down Expand Up @@ -2191,7 +2241,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:

# Check if the database is created
# (the database charm shared the credentials).
secret_field_user = self._generate_secret_field_name(SecretGroup.USER)
secret_field_user = self._generate_secret_field_name(SECRET_GROUPS.USER)
if (
"username" in diff.added and "password" in diff.added
) or secret_field_user in diff.added:
Expand Down Expand Up @@ -2237,7 +2287,11 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
self._emit_aliased_event(event, "read_only_endpoints_changed")


# Kafka related events
################################################################################
# Charm-specific Relations Data and Events
################################################################################

# Kafka Events


class KafkaProvidesEvent(RelationEvent):
Expand Down Expand Up @@ -2455,7 +2509,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
if any(newval for newval in diff.added if self._is_secret_field(newval)):
self._register_secrets_to_relation(event.relation, diff.added)

secret_field_user = self._generate_secret_field_name(SecretGroup.USER)
secret_field_user = self._generate_secret_field_name(SECRET_GROUPS.USER)
if (
"username" in diff.added and "password" in diff.added
) or secret_field_user in diff.added:
Expand Down Expand Up @@ -2651,8 +2705,8 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
if any(newval for newval in diff.added if self._is_secret_field(newval)):
self._register_secrets_to_relation(event.relation, diff.added)

secret_field_user = self._generate_secret_field_name(SecretGroup.USER)
secret_field_tls = self._generate_secret_field_name(SecretGroup.TLS)
secret_field_user = self._generate_secret_field_name(SECRET_GROUPS.USER)
secret_field_tls = self._generate_secret_field_name(SECRET_GROUPS.TLS)
updates = {"username", "password", "tls", "tls-ca", secret_field_user, secret_field_tls}
if len(set(diff._asdict().keys()) - updates) < len(diff):
logger.info("authentication updated at: %s", datetime.now())
Expand Down
2 changes: 2 additions & 0 deletions lib/charms/data_platform_libs/v0/data_secrets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Secrets related helper classes/functions."""

# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.


from typing import Dict, Literal, Optional

from ops import Secret, SecretInfo
Expand Down
12 changes: 6 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ def juju_has_secrets(mocker: MockerFixture):
juju_version = version("juju")

if juju_version < "3":
mocker.patch.object(
JujuVersion, "has_secrets", new_callable=PropertyMock
).return_value = False
mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = (
False
)
return False
else:
mocker.patch.object(
JujuVersion, "has_secrets", new_callable=PropertyMock
).return_value = True
mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = (
True
)
return True


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Secrets related helper classes/functions."""

# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down

0 comments on commit 876f7ba

Please sign in to comment.