Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ea12f5
add support for management of keycloak localizations
danekja Aug 26, 2025
3ab6856
unit test for keycloak localization support
danekja Sep 19, 2025
a5f2785
keycloak_realm_localization botmeta record
danekja Sep 19, 2025
c144001
update implementation to latest version of community.general
danekja Sep 19, 2025
07ba82a
changelog for module_utils/identity/keycloak - KeycloakAPI._request s…
danekja Sep 19, 2025
8936806
fix indentation, documentation and other sanity test findings
danekja Sep 25, 2025
1a433be
fix test_keycloak_realm_localization.py for run with newer ansible
danekja Sep 25, 2025
803c526
fix closure of _request optional http headers parameter
danekja Sep 25, 2025
e919484
add copyright and license to test_keycloak_realm_localization.py
danekja Sep 25, 2025
367c957
fix documentation indentation
danekja Sep 25, 2025
7607ac8
replace list.copy with 2.x compatible syntax
danekja Sep 25, 2025
3d43d2b
replace list.copy with deepcopy
danekja Sep 25, 2025
188bfd8
Update plugins/modules/keycloak_realm_localization.py
danekja Sep 26, 2025
194b03b
rev: remove uncessary changelog fragment
danekja Sep 26, 2025
1cc06a0
rev: remove uncessary code remnants
danekja Sep 26, 2025
7d9767a
rev: update file header with copyright according to guidelines
danekja Sep 26, 2025
40a75d2
rev: update documentation according to guidelines
danekja Sep 26, 2025
a93417a
rev: fix intermodule doc reference
danekja Sep 26, 2025
0d52bc7
rev: fix copyright
danekja Sep 26, 2025
c00c7b9
rev: list reference in docs
danekja Sep 26, 2025
538cd52
rev: line too long in docs
danekja Sep 26, 2025
37c815c
rev: remove year from copyright per guidelines
danekja Sep 26, 2025
e1c726d
rev: maybe this is valid copyright line?
danekja Sep 26, 2025
e340bfb
Update plugins/modules/keycloak_realm_localization.py
danekja Oct 3, 2025
72eb31f
rev: cleaner sorting in _normalize_overrides_from_api
danekja Oct 3, 2025
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
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ files:
maintainers: fynncfchen
$modules/keycloak_realm_key.py:
maintainers: mattock
$modules/keycloak_realm_localization.py:
maintainers: danekja
$modules/keycloak_role.py:
maintainers: laurpaum
$modules/keycloak_user.py:
Expand Down
1 change: 1 addition & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ action_groups:
- keycloak_realm
- keycloak_realm_key
- keycloak_realm_keys_metadata_info
- keycloak_realm_localization
- keycloak_realm_rolemapping
- keycloak_role
- keycloak_user
Expand Down
87 changes: 83 additions & 4 deletions plugins/module_utils/identity/keycloak/keycloak.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
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"
Expand Down Expand Up @@ -367,7 +370,7 @@ def __init__(self, module, connection_header):
self.restheaders = connection_header
self.http_agent = self.module.params.get('http_agent')

def _request(self, url, method, data=None):
def _request(self, url, method, data=None, headers=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)
Expand All @@ -378,20 +381,24 @@ def _request(self, url, method, data=None):
:param url: request path
:param method: request method (e.g., 'GET', 'POST', etc.)
:param data: (optional) data for request
:param headers headers to be sent with request, defaults to self.restheaders
:return: raw API response
"""
def make_request_catching_401():
def make_request_catching_401(headers):
try:
return open_url(url, method=method, data=data,
http_agent=self.http_agent, headers=self.restheaders,
http_agent=self.http_agent, headers=headers,
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except HTTPError as e:
if e.code != 401:
raise e
return e

r = make_request_catching_401()
if headers is None:
headers = self.restheaders

r = make_request_catching_401(headers)

if isinstance(r, Exception):
# Try to refresh token and retry, if available
Expand Down Expand Up @@ -568,6 +575,78 @@ def delete_realm(self, realm="master"):
self.fail_request(e, msg='Could not delete realm %s: %s' % (realm, str(e)),
exception=traceback.format_exc())

def get_localization_values(self, locale, realm="master"):
"""
Get all localization overrides for a given realm and locale.

Parameters:
locale (str): Locale code (for example, 'en', 'fi', 'de').
realm (str): Realm name. Defaults to 'master'.

Returns:
dict[str, str]: Mapping of localization keys to override values.

Raises:
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='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)),
exception=traceback.format_exc())

def set_localization_value(self, locale, key, value, realm="master"):
"""
Create or update a single localization override for the given key.

Parameters:
locale (str): Locale code (for example, 'en').
key (str): Localization message key to set.
value (str): Override value to set.
realm (str): Realm name. Defaults to 'master'.

Returns:
HTTPResponse: Response object on success.

Raises:
KeycloakError: Wrapped HTTP error with context
"""
realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key)

headers = self.restheaders.copy()
headers['Content-Type'] = 'text/plain; charset=utf-8'

try:
return self._request(realm_url, method='PUT', data=to_native(value), headers=headers)
except Exception as e:
self.fail_request(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)),
exception=traceback.format_exc())

def delete_localization_value(self, locale, key, realm="master"):
"""
Delete a single localization override key for the given locale.

Parameters:
locale (str): Locale code (for example, 'en').
key (str): Localization message key to delete.
realm (str): Realm name. Defaults to 'master'.

Returns:
HTTPResponse: Response object on success.

Raises:
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='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)),
exception=traceback.format_exc())

def get_clients(self, realm='master', filter=None):
""" Obtains client representations for clients in a realm

Expand Down
Loading
Loading