Skip to content

Commit

Permalink
chore: update charm libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
observability-noctua-bot committed Oct 16, 2024
1 parent d122274 commit 38f5c50
Show file tree
Hide file tree
Showing 8 changed files with 499 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ def _on_certificate_removed(self, event: CertificateRemovedEvent):
"""


import json
import logging
from typing import List, Mapping
Expand All @@ -113,7 +112,7 @@ def _on_certificate_removed(self, event: CertificateRemovedEvent):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 7
LIBPATCH = 8

PYDEPS = ["jsonschema"]

Expand Down
87 changes: 65 additions & 22 deletions lib/charms/hydra/v0/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,16 @@ def _set_client_config(self):
```
"""

import inspect
import json
import logging
import re
from dataclasses import asdict, dataclass, field
from dataclasses import asdict, dataclass, field, fields
from typing import Dict, List, Mapping, Optional

import jsonschema
from ops.charm import (
CharmBase,
RelationBrokenEvent,
RelationChangedEvent,
RelationCreatedEvent,
RelationDepartedEvent,
)
from ops.charm import CharmBase, RelationBrokenEvent, RelationChangedEvent, RelationCreatedEvent
from ops.framework import EventBase, EventSource, Handle, Object, ObjectEvents
from ops.model import Relation, Secret, TooManyRelatedAppsError
from ops.model import Relation, Secret, SecretNotFoundError, TooManyRelatedAppsError

# The unique Charmhub library identifier, never change it
LIBID = "a3a301e325e34aac80a2d633ef61fe97"
Expand All @@ -74,12 +67,20 @@ def _set_client_config(self):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 6
LIBPATCH = 10

PYDEPS = ["jsonschema"]


logger = logging.getLogger(__name__)

DEFAULT_RELATION_NAME = "oauth"
ALLOWED_GRANT_TYPES = ["authorization_code", "refresh_token", "client_credentials"]
ALLOWED_GRANT_TYPES = [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
]
ALLOWED_CLIENT_AUTHN_METHODS = ["client_secret_basic", "client_secret_post"]
CLIENT_SECRET_FIELD = "secret"

Expand Down Expand Up @@ -127,6 +128,7 @@ def _set_client_config(self):
},
"groups": {"type": "string", "default": None},
"ca_chain": {"type": "array", "items": {"type": "string"}, "default": []},
"jwt_access_token": {"type": "string", "default": "False"},
},
"required": [
"issuer_url",
Expand All @@ -153,13 +155,13 @@ def _set_client_config(self):
"type": "array",
"default": None,
"items": {
"enum": ["authorization_code", "client_credentials", "refresh_token"],
"enum": ALLOWED_GRANT_TYPES,
"type": "string",
},
},
"token_endpoint_auth_method": {
"type": "string",
"enum": ["client_secret_basic", "client_secret_post"],
"enum": ALLOWED_CLIENT_AUTHN_METHODS,
"default": "client_secret_basic",
},
},
Expand Down Expand Up @@ -200,11 +202,32 @@ def _dump_data(data: Dict, schema: Optional[Dict] = None) -> Dict:
ret[k] = json.dumps(v)
except json.JSONDecodeError as e:
raise DataValidationError(f"Failed to encode relation json: {e}")
elif isinstance(v, bool):
ret[k] = str(v)
else:
ret[k] = v
return ret


def strtobool(val: str) -> bool:
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
"""
if not isinstance(val, str):
raise ValueError(f"invalid value type {type(val)}")

val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError(f"invalid truth value {val}")


class OAuthRelation(Object):
"""A class containing helper methods for oauth relation."""

Expand Down Expand Up @@ -291,11 +314,22 @@ class OauthProviderConfig:
client_secret: Optional[str] = None
groups: Optional[str] = None
ca_chain: Optional[str] = None
jwt_access_token: Optional[bool] = False

@classmethod
def from_dict(cls, dic: Dict) -> "OauthProviderConfig":
"""Generate OauthProviderConfig instance from dict."""
return cls(**{k: v for k, v in dic.items() if k in inspect.signature(cls).parameters})
jwt_access_token = False
if "jwt_access_token" in dic:
jwt_access_token = strtobool(dic["jwt_access_token"])
return cls(
jwt_access_token=jwt_access_token,
**{
k: v
for k, v in dic.items()
if k in [f.name for f in fields(cls)] and k != "jwt_access_token"
},
)


class OAuthInfoChangedEvent(EventBase):
Expand All @@ -315,6 +349,7 @@ def snapshot(self) -> Dict:

def restore(self, snapshot: Dict) -> None:
"""Restore event."""
super().restore(snapshot)
self.client_id = snapshot["client_id"]
self.client_secret_id = snapshot["client_secret_id"]

Expand Down Expand Up @@ -454,7 +489,9 @@ def is_client_created(self, relation_id: Optional[int] = None) -> bool:
and "client_secret_id" in relation.data[relation.app]
)

def get_provider_info(self, relation_id: Optional[int] = None) -> OauthProviderConfig:
def get_provider_info(
self, relation_id: Optional[int] = None
) -> Optional[OauthProviderConfig]:
"""Get the provider information from the databag."""
if len(self.model.relations) == 0:
return None
Expand Down Expand Up @@ -647,8 +684,8 @@ def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME)
self._get_client_config_from_relation_data,
)
self.framework.observe(
events.relation_departed,
self._on_relation_departed,
events.relation_broken,
self._on_relation_broken,
)

def _get_client_config_from_relation_data(self, event: RelationChangedEvent) -> None:
Expand Down Expand Up @@ -696,7 +733,7 @@ def _get_client_config_from_relation_data(self, event: RelationChangedEvent) ->
def _get_secret_label(self, relation: Relation) -> str:
return f"client_secret_{relation.id}"

def _on_relation_departed(self, event: RelationDepartedEvent) -> None:
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
# Workaround for https://github.com/canonical/operator/issues/888
self._pop_relation_data(event.relation.id)

Expand All @@ -711,8 +748,12 @@ def _create_juju_secret(self, client_secret: str, relation: Relation) -> Secret:
return juju_secret

def _delete_juju_secret(self, relation: Relation) -> None:
secret = self.model.get_secret(label=self._get_secret_label(relation))
secret.remove_all_revisions()
try:
secret = self.model.get_secret(label=self._get_secret_label(relation))
except SecretNotFoundError:
return
else:
secret.remove_all_revisions()

def set_provider_info_in_relation_data(
self,
Expand All @@ -725,6 +766,7 @@ def set_provider_info_in_relation_data(
scope: str,
groups: Optional[str] = None,
ca_chain: Optional[str] = None,
jwt_access_token: Optional[bool] = False,
) -> None:
"""Put the provider information in the databag."""
if not self.model.unit.is_leader():
Expand All @@ -738,6 +780,7 @@ def set_provider_info_in_relation_data(
"userinfo_endpoint": userinfo_endpoint,
"jwks_endpoint": jwks_endpoint,
"scope": scope,
"jwt_access_token": jwt_access_token,
}
if groups:
data["groups"] = groups
Expand All @@ -760,5 +803,5 @@ def set_client_credentials_in_relation_data(
# TODO: What if we are refreshing the client_secret? We need to add a
# new revision for that
secret = self._create_juju_secret(client_secret, relation)
data = dict(client_id=client_id, client_secret_id=secret.id)
data = {"client_id": client_id, "client_secret_id": secret.id}
relation.data[self.model.app].update(_dump_data(data))
51 changes: 19 additions & 32 deletions lib/charms/observability_libs/v0/cert_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,25 @@
from typing import List, Optional, Union, cast

try:
from charms.tls_certificates_interface.v3.tls_certificates import ( # type: ignore
from charms.tls_certificates_interface.v2.tls_certificates import ( # type: ignore
AllCertificatesInvalidatedEvent,
CertificateAvailableEvent,
CertificateExpiringEvent,
CertificateInvalidatedEvent,
TLSCertificatesRequiresV3,
TLSCertificatesRequiresV2,
generate_csr,
generate_private_key,
)
except ImportError as e:
raise ImportError(
"failed to import charms.tls_certificates_interface.v3.tls_certificates; "
"failed to import charms.tls_certificates_interface.v2.tls_certificates; "
"Either the library itself is missing (please get it through charmcraft fetch-lib) "
"or one of its dependencies is unmet."
) from e

import logging

from ops.charm import CharmBase, RelationBrokenEvent
from ops.charm import CharmBase
from ops.framework import EventBase, EventSource, Object, ObjectEvents
from ops.model import Relation

Expand All @@ -67,7 +67,7 @@

LIBID = "b5cd5cd580f3428fa5f59a8876dcbe6a"
LIBAPI = 0
LIBPATCH = 12
LIBPATCH = 14


def is_ip_address(value: str) -> bool:
Expand Down Expand Up @@ -132,7 +132,7 @@ def __init__(
self.peer_relation_name = peer_relation_name
self.certificates_relation_name = certificates_relation_name

self.certificates = TLSCertificatesRequiresV3(self.charm, self.certificates_relation_name)
self.certificates = TLSCertificatesRequiresV2(self.charm, self.certificates_relation_name)

self.framework.observe(
self.charm.on.config_changed,
Expand All @@ -158,10 +158,6 @@ def __init__(
self.certificates.on.all_certificates_invalidated, # pyright: ignore
self._on_all_certificates_invalidated,
)
self.framework.observe(
self.charm.on[self.certificates_relation_name].relation_broken, # pyright: ignore
self._on_certificates_relation_broken,
)

# Peer relation events
self.framework.observe(
Expand Down Expand Up @@ -289,7 +285,7 @@ def _generate_csr(
if clear_cert:
self._ca_cert = ""
self._server_cert = ""
self._chain = ""
self._chain = []

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""Get the certificate from the event and store it in a peer relation.
Expand All @@ -311,7 +307,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
if event_csr == self._csr:
self._ca_cert = event.ca
self._server_cert = event.certificate
self._chain = event.chain_as_pem()
self._chain = event.chain
self.on.cert_changed.emit() # pyright: ignore

@property
Expand Down Expand Up @@ -382,29 +378,21 @@ def _server_cert(self, value: str):
rel.data[self.charm.unit].update({"certificate": value})

@property
def _chain(self) -> str:
def _chain(self) -> List[str]:
if self._peer_relation:
if chain := self._peer_relation.data[self.charm.unit].get("chain", ""):
chain = json.loads(chain)

# In a previous version of this lib, chain used to be a list.
# Convert the List[str] to str, per
# https://github.com/canonical/tls-certificates-interface/pull/141
if isinstance(chain, list):
chain = "\n\n".join(reversed(chain))

return cast(str, chain)
return ""
if chain := self._peer_relation.data[self.charm.unit].get("chain", []):
return cast(list, json.loads(cast(str, chain)))
return []

@_chain.setter
def _chain(self, value: str):
def _chain(self, value: List[str]):
# Caller must guard. We want the setter to fail loudly. Failure must have a side effect.
rel = self._peer_relation
assert rel is not None # For type checker
rel.data[self.charm.unit].update({"chain": json.dumps(value)})

@property
def chain(self) -> str:
def chain(self) -> List[str]:
"""Return the ca chain."""
return self._chain

Expand Down Expand Up @@ -433,18 +421,17 @@ def _on_certificate_invalidated(self, event: CertificateInvalidatedEvent) -> Non
self.on.cert_changed.emit() # pyright: ignore

def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEvent) -> None:
# Do what you want with this information, probably remove all certificates
# Note: assuming "limit: 1" in metadata
self._generate_csr(overwrite=True, clear_cert=True)
self.on.cert_changed.emit() # pyright: ignore

def _on_certificates_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Clear the certificates data when removing the relation."""
# Note: assuming "limit: 1" in metadata
# The "certificates_relation_broken" event is converted to "all invalidated" custom
# event by the tls-certificates library. Per convention, we let the lib manage the
# relation and we do not observe "certificates_relation_broken" directly.
if self._peer_relation:
private_key = self._private_key
# This is a workaround for https://bugs.launchpad.net/juju/+bug/2024583
self._peer_relation.data[self.charm.unit].clear()
if private_key:
self._peer_relation.data[self.charm.unit].update({"private_key": private_key})

# We do not generate a CSR here because the relation is gone.
self.on.cert_changed.emit() # pyright: ignore
Loading

0 comments on commit 38f5c50

Please sign in to comment.