mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-03-22 05:09:12 +00:00
keycloak_realm_localization: new module - realm localization control (#10841)
* add support for management of keycloak localizations * unit test for keycloak localization support * keycloak_realm_localization botmeta record * rev: improvements after code review
This commit is contained in:
parent
4bbedfd7df
commit
986118c0af
5 changed files with 845 additions and 7 deletions
|
|
@ -25,6 +25,9 @@ URL_REALMS = "{url}/admin/realms"
|
|||
URL_REALM = "{url}/admin/realms/{realm}"
|
||||
URL_REALM_KEYS_METADATA = "{url}/admin/realms/{realm}/keys"
|
||||
|
||||
URL_LOCALIZATIONS = "{url}/admin/realms/{realm}/localization/{locale}"
|
||||
URL_LOCALIZATION = "{url}/admin/realms/{realm}/localization/{locale}/{key}"
|
||||
|
||||
URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token"
|
||||
URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}"
|
||||
URL_CLIENTS = "{url}/admin/realms/{realm}/clients"
|
||||
|
|
@ -386,7 +389,9 @@ class KeycloakAPI:
|
|||
self.restheaders = connection_header
|
||||
self.http_agent = self.module.params.get("http_agent")
|
||||
|
||||
def _request(self, url: str, method: str, data: str | bytes | None = None):
|
||||
def _request(
|
||||
self, url: str, method: str, data: str | bytes | None = None, *, extra_headers: dict[str, str] | None = None
|
||||
):
|
||||
"""Makes a request to Keycloak and returns the raw response.
|
||||
If a 401 is returned, attempts to re-authenticate
|
||||
using first the module's refresh_token (if provided)
|
||||
|
|
@ -397,17 +402,18 @@ class KeycloakAPI:
|
|||
:param url: request path
|
||||
:param method: request method (e.g., 'GET', 'POST', etc.)
|
||||
:param data: (optional) data for request
|
||||
:param extra_headers headers to be sent with request, defaults to self.restheaders
|
||||
:return: raw API response
|
||||
"""
|
||||
|
||||
def make_request_catching_401() -> object | HTTPError:
|
||||
def make_request_catching_401(headers: dict[str, str]) -> object | HTTPError:
|
||||
try:
|
||||
return open_url(
|
||||
url,
|
||||
method=method,
|
||||
data=data,
|
||||
http_agent=self.http_agent,
|
||||
headers=self.restheaders,
|
||||
headers=headers,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs,
|
||||
)
|
||||
|
|
@ -416,7 +422,12 @@ class KeycloakAPI:
|
|||
raise e
|
||||
return e
|
||||
|
||||
r = make_request_catching_401()
|
||||
headers = self.restheaders
|
||||
if extra_headers is not None:
|
||||
headers = headers.copy()
|
||||
headers.update(extra_headers)
|
||||
|
||||
r = make_request_catching_401(headers)
|
||||
|
||||
if isinstance(r, Exception):
|
||||
# Try to refresh token and retry, if available
|
||||
|
|
@ -426,7 +437,7 @@ class KeycloakAPI:
|
|||
token = _request_token_using_refresh_token(self.module.params)
|
||||
self.restheaders["Authorization"] = f"Bearer {token}"
|
||||
|
||||
r = make_request_catching_401()
|
||||
r = make_request_catching_401(headers)
|
||||
except KeycloakError as e:
|
||||
# Token refresh returns 400 if token is expired/invalid, so continue on if we get a 400
|
||||
if e.authError is not None and e.authError.code != 400: # type: ignore # TODO!
|
||||
|
|
@ -440,7 +451,7 @@ class KeycloakAPI:
|
|||
token = _request_token_using_credentials(self.module.params)
|
||||
self.restheaders["Authorization"] = f"Bearer {token}"
|
||||
|
||||
r = make_request_catching_401()
|
||||
r = make_request_catching_401(headers)
|
||||
|
||||
if isinstance(r, Exception):
|
||||
# Try to re-auth with client_id and client_secret, if available
|
||||
|
|
@ -451,7 +462,7 @@ class KeycloakAPI:
|
|||
token = _request_token_using_client_credentials(self.module.params)
|
||||
self.restheaders["Authorization"] = f"Bearer {token}"
|
||||
|
||||
r = make_request_catching_401()
|
||||
r = make_request_catching_401(headers)
|
||||
except KeycloakError as e:
|
||||
# Token refresh returns 400 if token is expired/invalid, so continue on if we get a 400
|
||||
if e.authError is not None and e.authError.code != 400: # type: ignore # TODO!
|
||||
|
|
@ -590,6 +601,78 @@ class KeycloakAPI:
|
|||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Could not delete realm {realm}: {e}", exception=traceback.format_exc())
|
||||
|
||||
def get_localization_values(self, locale: str, realm: str = "master") -> dict[str, str]:
|
||||
"""
|
||||
Get all localization overrides for a given realm and locale.
|
||||
|
||||
:param locale: Locale code (for example, 'en', 'fi', 'de').
|
||||
:param realm: Realm name. Defaults to 'master'.
|
||||
|
||||
:return: Mapping of localization keys to override values.
|
||||
|
||||
:raise KeycloakError: Wrapped HTTP/JSON error with context
|
||||
"""
|
||||
realm_url = URL_LOCALIZATIONS.format(url=self.baseurl, realm=realm, locale=locale)
|
||||
|
||||
try:
|
||||
return self._request_and_deserialize(realm_url, method="GET")
|
||||
except Exception as e:
|
||||
self.fail_request(
|
||||
e,
|
||||
msg=f"Could not read localization overrides for realm {realm}, locale {locale}: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
def set_localization_value(self, locale: str, key: str, value: str, realm: str = "master"):
|
||||
"""
|
||||
Create or update a single localization override for the given key.
|
||||
|
||||
:param locale: Locale code (for example, 'en').
|
||||
:param key: Localization message key to set.
|
||||
:param value: Override value to set.
|
||||
:param realm: Realm name. Defaults to 'master'.
|
||||
|
||||
:return: HTTPResponse: Response object on success.
|
||||
|
||||
:raise KeycloakError: Wrapped HTTP error with context
|
||||
"""
|
||||
realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key)
|
||||
|
||||
headers = {}
|
||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
|
||||
try:
|
||||
return self._request(realm_url, method="PUT", data=to_native(value), extra_headers=headers)
|
||||
except Exception as e:
|
||||
self.fail_request(
|
||||
e,
|
||||
msg=f"Could not set localization value in realm {realm}, locale {locale}: {key}={value}: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
def delete_localization_value(self, locale: str, key: str, realm: str = "master"):
|
||||
"""
|
||||
Delete a single localization override key for the given locale.
|
||||
|
||||
:param locale: Locale code (for example, 'en').
|
||||
:param key: Localization message key to delete.
|
||||
:param realm: Realm name. Defaults to 'master'.
|
||||
|
||||
:return: HTTPResponse: Response object on success.
|
||||
|
||||
:raise KeycloakError: Wrapped HTTP error with context
|
||||
"""
|
||||
realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key)
|
||||
|
||||
try:
|
||||
return self._request(realm_url, method="DELETE")
|
||||
except Exception as e:
|
||||
self.fail_request(
|
||||
e,
|
||||
msg=f"Could not delete localization value in realm {realm}, locale {locale}, key {key}: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
def get_clients(self, realm: str = "master", filter=None):
|
||||
"""Obtains client representations for clients in a realm
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue