From d2b4d7fbe28b9ed415a6253f60d9d84a2cd44119 Mon Sep 17 00:00:00 2001 From: Alex Zgabur Date: Fri, 10 Nov 2023 18:43:46 +0100 Subject: [PATCH 1/2] Responses section refactor --- testsuite/objects/__init__.py | 70 +++++++++++ .../openshift/objects/auth_config/sections.py | 118 +++++------------- 2 files changed, 104 insertions(+), 84 deletions(-) diff --git a/testsuite/objects/__init__.py b/testsuite/objects/__init__.py index 063e5f68..a9c70a9c 100644 --- a/testsuite/objects/__init__.py +++ b/testsuite/objects/__init__.py @@ -121,6 +121,76 @@ class ValueFrom(ABCValue): selector: str +@dataclass +class JsonResponse: + """Response item as JSON injection.""" + + properties: dict[str, ABCValue] + + def asdict(self): + """Custom asdict due to nested structure.""" + asdict_properties = {} + for key, value in self.properties.items(): + asdict_properties[key] = asdict(value) + return {"json": {"properties": asdict_properties}} + + +@dataclass +class PlainResponse: + """Response item as plain text value.""" + + plain: ABCValue + + +@dataclass +class WristbandSigningKeyRef: + """Name of Kubernetes secret and corresponding signing algorithm.""" + + name: str + algorithm: str = "RS256" + + +@dataclass(kw_only=True) +class WristbandResponse: + """ + Response item as Festival Wristband Token. + + :param issuer: Endpoint to the Authorino service that issues the wristband + :param signingKeyRefs: List of Kubernetes secrets of dataclass `WristbandSigningKeyRef` + :param customClaims: Custom claims added to the wristband token. + :param tokenDuration: Time span of the wristband token, in seconds. + """ + + issuer: str + signingKeyRefs: list[WristbandSigningKeyRef] + customClaims: Optional[list[dict[str, ABCValue]]] = None + tokenDuration: Optional[int] = None + + def asdict(self): + """Custom asdict due to nested structure.""" + + asdict_key_refs = [asdict(i) for i in self.signingKeyRefs] + asdict_custom_claims = [asdict(i) for i in self.customClaims] if self.customClaims else None + return { + "wristband": { + "issuer": self.issuer, + "signingKeyRefs": asdict_key_refs, + "customClaims": asdict_custom_claims, + "tokenDuration": self.tokenDuration, + } + } + + +@dataclass(kw_only=True) +class DenyResponse: + """Dataclass for custom responses deny reason.""" + + code: Optional[int] = None + message: Optional[ABCValue] = None + headers: Optional[dict[str, ABCValue]] = None + body: Optional[ABCValue] = None + + @dataclass class Cache: """Dataclass for specifying Cache in Authorization""" diff --git a/testsuite/openshift/objects/auth_config/sections.py b/testsuite/openshift/objects/auth_config/sections.py index 855c08ab..142bdc64 100644 --- a/testsuite/openshift/objects/auth_config/sections.py +++ b/testsuite/openshift/objects/auth_config/sections.py @@ -1,5 +1,5 @@ """AuthConfig CR object""" -from typing import Literal, Iterable, TYPE_CHECKING +from typing import Literal, Iterable, TYPE_CHECKING, Union from testsuite.objects import ( asdict, @@ -9,6 +9,10 @@ Selector, Credentials, ValueFrom, + JsonResponse, + PlainResponse, + WristbandResponse, + DenyResponse, ) from testsuite.openshift.objects import modify @@ -199,100 +203,46 @@ def add_uma(self, name, endpoint, credentials_secret, **common_features): class ResponseSection(Section): """Section which contains response configuration.""" - @property - def success_headers(self): - """Nested dict for items wrapped as HTTP headers.""" - return self.section.setdefault("success", {}).setdefault("headers", {}) - - @property - def success_dynamic_metadata(self): - """Nested dict for items wrapped as Envoy Dynamic Metadata.""" - return self.section.setdefault("success", {}).setdefault("dynamicMetadata", {}) + SUCCESS_RESPONSE = Union[JsonResponse, PlainResponse, WristbandResponse] - def _add( - self, - name: str, - value: dict, - wrapper: Literal["headers", "dynamicMetadata"] = "headers", - **common_features, - ): + def add_simple(self, auth_json: str, name="simple", key="data", **common_features): + """ + Add simple response to AuthConfig, used for configuring response for debugging purposes, + which can be easily read back using extract_response """ - Add response to AuthConfig. + self.add_success_header(name, JsonResponse({key: ValueFrom(auth_json)}), **common_features) - :param wrapper: This variable configures if the response should be wrapped as HTTP headers or - as Envoy Dynamic Metadata. Default is "headers" + def add_success_header(self, name: str, value: SUCCESS_RESPONSE, **common_features): + """ + Add item to responses.success.headers section. + This section is for items wrapped as HTTP headers. """ - add_common_features(value, **common_features) - if wrapper == "headers": - self.success_headers.update({name: value}) - if wrapper == "dynamicMetadata": - self.success_dynamic_metadata.update({name: value}) - def add_simple(self, auth_json: str, name="simple", key="data", **common_features): + success_headers = self.section.setdefault("success", {}).setdefault("headers", {}) + asdict_value = asdict(value) + add_common_features(asdict_value, **common_features) + success_headers.update({name: asdict_value}) + + def add_success_dynamic(self, name: str, value: SUCCESS_RESPONSE, **common_features): """ - Add simple response to AuthConfig, used for configuring response for debugging purposes, - which can be easily read back using extract_response + Add item to responses.success.dynamicMetadata section. + This section is for items wrapped as Envoy Dynamic Metadata. """ - self.add_json(name, {key: ValueFrom(auth_json)}, **common_features) - @modify - def add_json(self, name: str, properties: dict[str, ABCValue], **common_features): - """Adds json response to AuthConfig""" - asdict_properties = {} - for key, value in properties.items(): - asdict_properties[key] = asdict(value) - self._add(name, {"json": {"properties": asdict_properties}}, **common_features) + success_dynamic_metadata = self.section.setdefault("success", {}).setdefault("dynamicMetadata", {}) + asdict_value = asdict(value) + add_common_features(asdict_value, **common_features) + success_dynamic_metadata.update({name: asdict_value}) - @modify - def add_plain(self, name: str, value: ABCValue, **common_features): - """Adds plain response to AuthConfig""" - self._add(name, {"plain": asdict(value)}, **common_features) + def set_unauthenticated(self, deny_response: DenyResponse): + """Set custom deny response for unauthenticated error.""" - @modify - def add_wristband(self, name: str, issuer: str, secret_name: str, algorithm: str = "RS256", **common_features): - """Adds wristband response to AuthConfig""" - self._add( - name, - { - "wristband": { - "issuer": issuer, - "signingKeyRefs": [ - { - "name": secret_name, - "algorithm": algorithm, - } - ], - }, - }, - **common_features, - ) + self.add_item("unauthenticated", asdict(deny_response)) - @modify - def set_deny_with( - self, - category: Literal["unauthenticated", "unauthorized"], - code: int = None, - message: ABCValue = None, - headers: dict[str, ABCValue] = None, - body: ABCValue = None, - ): - """Set default deny code, message, headers, and body for 'unauthenticated' and 'unauthorized' error.""" - asdict_message = asdict(message) if message else None - asdict_body = asdict(body) if body else None - asdict_headers = None - if headers: - asdict_headers = {} - for key, value in headers.items(): - asdict_headers[key] = asdict(value) - self.add_item( - category, - { - "code": code, - "message": asdict_message, - "headers": asdict_headers, - "body": asdict_body, - }, - ) + def set_unauthorized(self, deny_response: DenyResponse): + """Set custom deny response for unauthorized error.""" + + self.add_item("unauthorized", asdict(deny_response)) class AuthorizationSection(Section): From d252575088d2c6c2becb7e0f8ac38cae43a76fb2 Mon Sep 17 00:00:00 2001 From: Alex Zgabur Date: Fri, 10 Nov 2023 18:43:52 +0100 Subject: [PATCH 2/2] Responses section refactor tests --- .../test_response_condition.py | 6 ++-- .../identity/rhsso/test_rhsso_context.py | 14 ++++++---- .../authorino/metrics/test_deep_metrics.py | 4 +-- .../clusterwide/test_wildcard_collision.py | 6 ++-- .../authorino/operator/http/conftest.py | 4 +-- .../authorino/operator/sharding/conftest.py | 4 +-- .../authorino/response/test_auth_json.py | 4 +-- .../authorino/response/test_base64.py | 6 ++-- .../authorino/response/test_deny_with.py | 28 ++++++++++--------- .../authorino/response/test_headers.py | 4 +-- .../response/test_multiple_responses.py | 6 ++-- .../response/test_simple_response.py | 4 +-- .../tests/kuadrant/authorino/test_redirect.py | 11 ++++---- .../kuadrant/authorino/wristband/conftest.py | 7 ++++- .../tests/kuadrant/test_rate_limit_authz.py | 6 ++-- 15 files changed, 62 insertions(+), 52 deletions(-) diff --git a/testsuite/tests/kuadrant/authorino/conditions/section_conditions/test_response_condition.py b/testsuite/tests/kuadrant/authorino/conditions/section_conditions/test_response_condition.py index 189661f0..5d6a8a45 100644 --- a/testsuite/tests/kuadrant/authorino/conditions/section_conditions/test_response_condition.py +++ b/testsuite/tests/kuadrant/authorino/conditions/section_conditions/test_response_condition.py @@ -1,15 +1,15 @@ """Test condition to skip the response section of AuthConfig""" import pytest -from testsuite.objects import Rule, Value +from testsuite.objects import Rule, Value, JsonResponse from testsuite.utils import extract_response @pytest.fixture(scope="module") def authorization(authorization): """Add to the AuthConfig response, which will only trigger on POST requests""" - authorization.responses.add_json( - "simple", {"data": Value("response")}, when=[Rule("context.request.http.method", "eq", "POST")] + authorization.responses.add_success_header( + "simple", JsonResponse({"data": Value("response")}), when=[Rule("context.request.http.method", "eq", "POST")] ) return authorization diff --git a/testsuite/tests/kuadrant/authorino/identity/rhsso/test_rhsso_context.py b/testsuite/tests/kuadrant/authorino/identity/rhsso/test_rhsso_context.py index f4c095b9..476d5447 100644 --- a/testsuite/tests/kuadrant/authorino/identity/rhsso/test_rhsso_context.py +++ b/testsuite/tests/kuadrant/authorino/identity/rhsso/test_rhsso_context.py @@ -4,18 +4,20 @@ import pytest -from testsuite.objects import ValueFrom +from testsuite.objects import ValueFrom, JsonResponse @pytest.fixture(scope="module") def authorization(authorization): """Setup AuthConfig for test""" - authorization.responses.add_json( + authorization.responses.add_success_header( "auth-json", - { - "auth": ValueFrom("auth.identity"), - "context": ValueFrom("context.request.http.headers.authorization"), - }, + JsonResponse( + { + "auth": ValueFrom("auth.identity"), + "context": ValueFrom("context.request.http.headers.authorization"), + } + ), ) return authorization diff --git a/testsuite/tests/kuadrant/authorino/metrics/test_deep_metrics.py b/testsuite/tests/kuadrant/authorino/metrics/test_deep_metrics.py index d98e2a5d..2d96ed21 100644 --- a/testsuite/tests/kuadrant/authorino/metrics/test_deep_metrics.py +++ b/testsuite/tests/kuadrant/authorino/metrics/test_deep_metrics.py @@ -1,7 +1,7 @@ """Tests for the functionality of the deep-evaluator metric samples""" import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse @pytest.fixture(scope="module") @@ -25,7 +25,7 @@ def authorization(authorization, mockserver_expectation): authorization.identity.add_anonymous("anonymous", metrics=True) authorization.authorization.add_opa_policy("opa", "allow { true }", metrics=True) authorization.metadata.add_http("http", mockserver_expectation, "GET", metrics=True) - authorization.responses.add_json("json", {"auth": Value("response")}, metrics=True) + authorization.responses.add_success_header("json", JsonResponse({"auth": Value("response")}), metrics=True) return authorization diff --git a/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py b/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py index 0be7cc85..f4c669ff 100644 --- a/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py +++ b/testsuite/tests/kuadrant/authorino/operator/clusterwide/test_wildcard_collision.py @@ -4,7 +4,7 @@ import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse from testsuite.openshift.objects.auth_config import AuthConfig @@ -15,7 +15,7 @@ def authorization(authorino, blame, openshift, module_label, proxy, wildcard_dom auth = AuthConfig.create_instance( openshift, blame("ac"), None, hostnames=[wildcard_domain], labels={"testRun": module_label} ) - auth.responses.add_json("header", {"anything": Value("one")}) + auth.responses.add_success_header("header", JsonResponse({"anything": Value("one")})) return auth @@ -26,7 +26,7 @@ def authorization2(authorino, blame, openshift2, module_label, proxy, wildcard_d auth = AuthConfig.create_instance( openshift2, blame("ac"), None, hostnames=[wildcard_domain], labels={"testRun": module_label} ) - auth.responses.add_json("header", {"anything": Value("two")}) + auth.responses.add_success_header("header", JsonResponse({"anything": Value("two")})) return auth diff --git a/testsuite/tests/kuadrant/authorino/operator/http/conftest.py b/testsuite/tests/kuadrant/authorino/operator/http/conftest.py index fc3ccc86..8edd2c09 100644 --- a/testsuite/tests/kuadrant/authorino/operator/http/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/http/conftest.py @@ -1,7 +1,7 @@ """Conftest for all tests requiring custom deployment of Authorino""" import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse from testsuite.httpx import HttpxBackoffClient from testsuite.openshift.objects.auth_config import AuthConfig from testsuite.openshift.objects.route import OpenshiftRoute @@ -13,7 +13,7 @@ def authorization(authorization, wildcard_domain, openshift, module_label) -> Au """In case of Authorino, AuthConfig used for authorization""" authorization.remove_all_hosts() authorization.add_host(wildcard_domain) - authorization.responses.add_json("x-ext-auth-other-json", {"propX": Value("valueX")}) + authorization.responses.add_success_header("x-ext-auth-other-json", JsonResponse({"propX": Value("valueX")})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py b/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py index faf50909..04f9b51f 100644 --- a/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py +++ b/testsuite/tests/kuadrant/authorino/operator/sharding/conftest.py @@ -1,7 +1,7 @@ """Conftest for authorino sharding tests""" import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse from testsuite.openshift.envoy import Envoy from testsuite.openshift.objects.auth_config import AuthConfig @@ -34,7 +34,7 @@ def _authorization(hostname=None, sharding_label=None): hostnames=[hostname], labels={"testRun": module_label, "sharding": sharding_label}, ) - auth.responses.add_json("header", {"anything": Value(sharding_label)}) + auth.responses.add_success_header("header", JsonResponse({"anything": Value(sharding_label)})) request.addfinalizer(auth.delete) auth.commit() return auth diff --git a/testsuite/tests/kuadrant/authorino/response/test_auth_json.py b/testsuite/tests/kuadrant/authorino/response/test_auth_json.py index 33db78ab..7b6eb6e4 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_auth_json.py +++ b/testsuite/tests/kuadrant/authorino/response/test_auth_json.py @@ -4,7 +4,7 @@ import pytest -from testsuite.objects import ValueFrom +from testsuite.objects import ValueFrom, JsonResponse @pytest.fixture(scope="module") @@ -31,7 +31,7 @@ def authorization(authorization, path_and_value): path, _ = path_and_value authorization.responses.clear_all() # delete previous responses due to the parametrization - authorization.responses.add_json("header", {"anything": ValueFrom(path)}) + authorization.responses.add_success_header("header", JsonResponse({"anything": ValueFrom(path)})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/response/test_base64.py b/testsuite/tests/kuadrant/authorino/response/test_base64.py index 10558589..52e94153 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_base64.py +++ b/testsuite/tests/kuadrant/authorino/response/test_base64.py @@ -6,14 +6,14 @@ import pytest -from testsuite.objects import ValueFrom +from testsuite.objects import ValueFrom, JsonResponse @pytest.fixture(scope="module") def authorization(authorization): """Add response to Authorization""" - authorization.responses.add_json( - "header", {"anything": ValueFrom("context.request.http.headers.test|@base64:decode")} + authorization.responses.add_success_header( + "header", JsonResponse({"anything": ValueFrom("context.request.http.headers.test|@base64:decode")}) ) return authorization diff --git a/testsuite/tests/kuadrant/authorino/response/test_deny_with.py b/testsuite/tests/kuadrant/authorino/response/test_deny_with.py index d9c502b8..419b8664 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_deny_with.py +++ b/testsuite/tests/kuadrant/authorino/response/test_deny_with.py @@ -2,7 +2,7 @@ from json import loads import pytest -from testsuite.objects import Value, ValueFrom, Rule +from testsuite.objects import Value, ValueFrom, Rule, DenyResponse HEADERS = { "x-string-header": Value("abc"), @@ -18,19 +18,21 @@ @pytest.fixture(scope="module") def authorization(authorization): """Set custom deny responses and auth rule with only allowed path '/allow'""" - authorization.responses.set_deny_with( - "unauthenticated", - code=333, - headers=HEADERS, - message=Value("Unauthenticated message"), - body=Value("You are unauthenticated."), + authorization.responses.set_unauthenticated( + DenyResponse( + code=333, + headers=HEADERS, + message=Value("Unauthenticated message"), + body=Value("You are unauthenticated."), + ) ) - authorization.responses.set_deny_with( - "unauthorized", - code=444, - headers=HEADERS, - message=ValueFrom("My path is: " + "{context.request.http.path}"), - body=ValueFrom("You are not authorized to access path: " + "{context.request.http.path}"), + authorization.responses.set_unauthorized( + DenyResponse( + code=444, + headers=HEADERS, + message=ValueFrom("My path is: " + "{context.request.http.path}"), + body=ValueFrom("You are not authorized to access path: " + "{context.request.http.path}"), + ) ) # Authorize only when url path is "/allow" authorization.authorization.add_auth_rules("Whitelist", [Rule("context.request.http.path", "eq", "/allow")]) diff --git a/testsuite/tests/kuadrant/authorino/response/test_headers.py b/testsuite/tests/kuadrant/authorino/response/test_headers.py index 2d07859d..fcc9f0c3 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_headers.py +++ b/testsuite/tests/kuadrant/authorino/response/test_headers.py @@ -3,7 +3,7 @@ import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse @pytest.fixture(scope="module", params=["123456789", "standardCharacters", "specialcharacters+*-."]) @@ -16,7 +16,7 @@ def header_name(request): def authorization(authorization, header_name): """Add response to Authorization""" authorization.responses.clear_all() # delete previous responses due to the parametrization - authorization.responses.add_json(header_name, {"anything": Value("one")}) + authorization.responses.add_success_header(header_name, JsonResponse({"anything": Value("one")})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/response/test_multiple_responses.py b/testsuite/tests/kuadrant/authorino/response/test_multiple_responses.py index b9256447..2f76de96 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_multiple_responses.py +++ b/testsuite/tests/kuadrant/authorino/response/test_multiple_responses.py @@ -3,14 +3,14 @@ import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse @pytest.fixture(scope="module") def authorization(authorization): """Add response to Authorization""" - authorization.responses.add_json("header", {"anything": Value("one")}) - authorization.responses.add_json("X-Test", {"anything": Value("two")}) + authorization.responses.add_success_header("header", JsonResponse({"anything": Value("one")})) + authorization.responses.add_success_header("X-Test", JsonResponse({"anything": Value("two")})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/response/test_simple_response.py b/testsuite/tests/kuadrant/authorino/response/test_simple_response.py index fe83e476..4b70ca61 100644 --- a/testsuite/tests/kuadrant/authorino/response/test_simple_response.py +++ b/testsuite/tests/kuadrant/authorino/response/test_simple_response.py @@ -3,13 +3,13 @@ import pytest -from testsuite.objects import Value +from testsuite.objects import Value, JsonResponse @pytest.fixture(scope="module") def authorization(authorization): """Add response to Authorization""" - authorization.responses.add_json("header", {"anything": Value("one")}) + authorization.responses.add_success_header("header", JsonResponse({"anything": Value("one")})) return authorization diff --git a/testsuite/tests/kuadrant/authorino/test_redirect.py b/testsuite/tests/kuadrant/authorino/test_redirect.py index 68280fcc..026c21d6 100644 --- a/testsuite/tests/kuadrant/authorino/test_redirect.py +++ b/testsuite/tests/kuadrant/authorino/test_redirect.py @@ -3,7 +3,7 @@ """ import pytest -from testsuite.objects import ValueFrom +from testsuite.objects import ValueFrom, DenyResponse STATUS_CODE = 302 REDIRECT_URL = "http://anything.inavlid?redirect_to=" @@ -12,10 +12,11 @@ @pytest.fixture(scope="module") def authorization(authorization): """In case of Authorino, AuthConfig used for authorization""" - authorization.responses.set_deny_with( - "unauthenticated", - code=STATUS_CODE, - headers={"Location": ValueFrom(REDIRECT_URL + "{context.request.http.path}")}, + authorization.responses.set_unauthenticated( + DenyResponse( + code=STATUS_CODE, + headers={"Location": ValueFrom(REDIRECT_URL + "{context.request.http.path}")}, + ) ) return authorization diff --git a/testsuite/tests/kuadrant/authorino/wristband/conftest.py b/testsuite/tests/kuadrant/authorino/wristband/conftest.py index c258590c..cc5fb249 100644 --- a/testsuite/tests/kuadrant/authorino/wristband/conftest.py +++ b/testsuite/tests/kuadrant/authorino/wristband/conftest.py @@ -3,6 +3,7 @@ import pytest +from testsuite.objects import WristbandSigningKeyRef, WristbandResponse from testsuite.openshift.objects.auth_config import AuthConfig from testsuite.openshift.envoy import Envoy from testsuite.certificates import CertInfo @@ -70,7 +71,11 @@ def wristband_endpoint(openshift, authorino, authorization_name): @pytest.fixture(scope="module") def authorization(authorization, wristband_secret, wristband_endpoint) -> AuthConfig: """Add wristband response with the signing key to the AuthConfig""" - authorization.responses.add_wristband("wristband", wristband_endpoint, wristband_secret, wrapper="dynamicMetadata") + + authorization.responses.add_success_dynamic( + "wristband", + WristbandResponse(issuer=wristband_endpoint, signingKeyRefs=[WristbandSigningKeyRef(wristband_secret)]), + ) return authorization diff --git a/testsuite/tests/kuadrant/test_rate_limit_authz.py b/testsuite/tests/kuadrant/test_rate_limit_authz.py index 6f90a181..87b3b791 100644 --- a/testsuite/tests/kuadrant/test_rate_limit_authz.py +++ b/testsuite/tests/kuadrant/test_rate_limit_authz.py @@ -3,7 +3,7 @@ import pytest from testsuite.httpx.auth import HttpxOidcClientAuth -from testsuite.objects import ValueFrom +from testsuite.objects import ValueFrom, JsonResponse from testsuite.openshift.objects.rate_limit import Limit @@ -19,8 +19,8 @@ def rate_limit(rate_limit): @pytest.fixture(scope="module") def authorization(authorization): """Adds JSON injection, that wraps the response as Envoy Dynamic Metadata for rate limit""" - authorization.responses.add_json( - "identity", {"user": ValueFrom("auth.identity.preferred_username")}, wrapper="dynamicMetadata" + authorization.responses.add_success_dynamic( + "identity", JsonResponse({"user": ValueFrom("auth.identity.preferred_username")}) ) return authorization