Skip to content

Commit c4d705d

Browse files
authored
Merge pull request #240 from canonical/IAM-1418
Update login_ui_endpoints lib
2 parents 787b96e + e764aa1 commit c4d705d

9 files changed

Lines changed: 409 additions & 184 deletions

File tree

charmcraft.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,6 @@ parts:
8787
charm-binary-python-packages:
8888
- jsonschema
8989
- "setuptools>=70.0.0"
90+
build-packages:
91+
- rustc
92+
- cargo

integration-requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
-r requirements.txt
22
juju
3-
pydantic<2.0
43
pytest
54
pytest-asyncio
65
pytest-operator==0.41.0

lib/charms/identity_platform_login_ui_operator/v0/login_ui_endpoints.py

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ def some_event_function():
3737
```
3838
"""
3939

40+
from typing import List
4041
import logging
41-
from typing import Dict
42+
from typing import Dict, Optional
4243

4344
from ops.charm import CharmBase, RelationCreatedEvent
4445
from ops.framework import EventBase, EventSource, Object, ObjectEvents
45-
from ops.model import TooManyRelatedAppsError
46+
from ops import Relation, ModelError
47+
from pydantic import BaseModel
4648

4749
# The unique Charmhub library identifier, never change it
4850
LIBID = "f59057701b5840849d3cea756af404c6"
@@ -52,23 +54,24 @@ def some_event_function():
5254

5355
# Increment this PATCH version before using `charmcraft publish-lib` or reset
5456
# to 0 if you are raising the major API version
55-
LIBPATCH = 2
57+
LIBPATCH = 3
5658

5759
RELATION_NAME = "ui-endpoint-info"
5860
INTERFACE_NAME = "login_ui_endpoints"
5961
logger = logging.getLogger(__name__)
6062

61-
RELATION_KEYS = [
62-
"consent_url",
63-
"error_url",
64-
"login_url",
65-
"oidc_error_url",
66-
"device_verification_url",
67-
"post_device_done_url",
68-
"recovery_url",
69-
"settings_url",
70-
"webauthn_settings_url",
71-
]
63+
64+
class LoginUIProviderData(BaseModel):
65+
consent_url: Optional[str] = None
66+
error_url: Optional[str] = None
67+
login_url: Optional[str] = None
68+
oidc_error_url: Optional[str] = None
69+
device_verification_url: Optional[str] = None
70+
post_device_done_url: Optional[str] = None
71+
recovery_url: Optional[str] = None
72+
registration_url: Optional[str] = None
73+
settings_url: Optional[str] = None
74+
webauthn_settings_url: Optional[str] = None
7275

7376

7477
class LoginUIEndpointsRelationReadyEvent(EventBase):
@@ -100,29 +103,15 @@ def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME):
100103
def _on_provider_endpoints_relation_created(self, event: RelationCreatedEvent) -> None:
101104
self.on.ready.emit()
102105

103-
def send_endpoints_relation_data(self, endpoint: str) -> None:
106+
def send_endpoints_relation_data(self, data: LoginUIProviderData) -> None:
104107
"""Updates relation with endpoint info."""
105108
if not self._charm.unit.is_leader():
106-
raise LoginUINonLeaderOperationError()
109+
return None
107110

108111
relations = self.model.relations[self._relation_name]
109112

110-
if not endpoint:
111-
endpoint_databag = {k: "" for k in RELATION_KEYS}
112-
else:
113-
endpoint_databag = {
114-
"consent_url": f"{endpoint}/ui/consent",
115-
"error_url": f"{endpoint}/ui/error",
116-
"login_url": f"{endpoint}/ui/login",
117-
"oidc_error_url": f"{endpoint}/ui/oidc_error",
118-
"device_verification_url": f"{endpoint}/ui/device_code",
119-
"post_device_done_url": f"{endpoint}/ui/device_complete",
120-
"recovery_url": f"{endpoint}/ui/reset_email",
121-
"settings_url": f"{endpoint}/ui/reset_password",
122-
"webauthn_settings_url": f"{endpoint}/ui/setup_passkey",
123-
}
124113
for relation in relations:
125-
relation.data[self._charm.app].update(endpoint_databag)
114+
relation.data[self._charm.app].update(data.model_dump(exclude_none=True))
126115

127116

128117
class LoginUIEndpointsRelationError(Exception):
@@ -131,27 +120,11 @@ class LoginUIEndpointsRelationError(Exception):
131120
pass
132121

133122

134-
class LoginUITooManyRelatedAppsError(LoginUIEndpointsRelationError):
135-
"""Raised when there are more than one ui-endpoints-info relations between Identity Platform Login UI and another component."""
136-
137-
def __init__(self) -> None:
138-
self.message = f"Too many applications on {RELATION_NAME}"
139-
super().__init__(self.message)
140-
141-
142-
class LoginUINonLeaderOperationError(LoginUIEndpointsRelationError):
143-
"""Raised when a non-leader unit wants to call a function intended for leader units."""
144-
145-
def __init__(self) -> None:
146-
self.message = "Calling Identity Platform Login UI unit is not leader"
147-
super().__init__(self.message)
148-
149-
150-
class LoginUIEndpointsRelationMissingError(LoginUIEndpointsRelationError):
151-
"""Raised when the relation is missing."""
123+
class LoginUIEndpointsConflictError(LoginUIEndpointsRelationError):
124+
"""Raised when we got the same uri multiple times."""
152125

153126
def __init__(self) -> None:
154-
self.message = "Missing ui-endpoint-info relation with Identity Platform Login UI"
127+
self.message = "Got the same uri from multiple relations"
155128
super().__init__(self.message)
156129

157130

@@ -163,20 +136,35 @@ def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME):
163136
self.charm = charm
164137
self._relation_name = relation_name
165138

166-
def get_login_ui_endpoints(self, relation_id=None) -> Dict:
167-
"""Get the Identity Platform Login UI endpoints."""
139+
@property
140+
def relations(self) -> List[Relation]:
141+
"""The list of Relation instances associated with this relation_name."""
142+
return [
143+
relation
144+
for relation in self.charm.model.relations[self._relation_name]
145+
if relation.active
146+
]
168147

169-
try:
170-
ui_endpoint_relation = self.model.get_relation(self._relation_name, relation_id=relation_id)
171-
except TooManyRelatedAppsError:
172-
raise LoginUITooManyRelatedAppsError()
148+
def _get_login_ui_endpoints_data(self, relation: Relation) -> Optional[Dict]:
149+
return relation.data[relation.app] if relation.app else None
173150

174-
if ui_endpoint_relation is None:
175-
raise LoginUIEndpointsRelationMissingError()
151+
def get_login_ui_endpoints(self, relation_id=None) -> Optional[Dict]:
152+
"""Get the Identity Platform Login UI endpoints."""
153+
if relation_id:
154+
relations = [self.model.get_relation(self._relation_name, relation_id=relation_id)]
155+
else:
156+
relations = self.relations
176157

177-
if not ui_endpoint_relation.app:
178-
raise LoginUIEndpointsRelationMissingError()
158+
if not relations:
159+
return None
179160

180-
ui_endpoint_relation_data = ui_endpoint_relation.data[ui_endpoint_relation.app]
161+
data = {}
162+
for r in relations:
163+
d = self._get_login_ui_endpoints_data(r)
164+
if not d:
165+
continue
166+
if set(d.keys()).intersection(data.keys()):
167+
raise LoginUIEndpointsConflictError()
168+
data.update(d)
181169

182-
return ui_endpoint_relation_data
170+
return data

0 commit comments

Comments
 (0)