Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update charm libraries #329

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ def _on_certificate_removed(self, event: CertificateRemovedEvent):

"""


import json
import logging
from typing import List, Mapping

from jsonschema import exceptions, validate # type: ignore[import-untyped]
from ops import Relation
from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent
from ops.framework import EventBase, EventSource, Handle, Object

Expand All @@ -113,7 +113,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 = 9

PYDEPS = ["jsonschema"]

Expand Down Expand Up @@ -392,3 +392,11 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
None
"""
self.on.certificate_removed.emit(relation_id=event.relation.id)

def is_ready(self, relation: Relation) -> bool:
"""Check if the relation is ready by checking that it has valid relation data."""
relation_data = _load_relation_data(relation.data[relation.app])
if not self._relation_data_is_valid(relation_data):
logger.warning("Provider relation data did not pass JSON Schema validation: ")
return False
return True
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))
Loading
Loading