diff --git a/CHANGELOG.md b/CHANGELOG.md index f5fe572..8db709e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v5.2.0 (2025-01-30) + +### Feat + +- Add functions to get/update realm users profile (#634) + ## v5.1.2 (2025-01-26) ### Fix diff --git a/docs/source/conf.py b/docs/source/conf.py index 5e237f2..caaec76 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,6 +22,8 @@ """Sphinx documentation configuration.""" +import os + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -68,9 +70,9 @@ author = "Marcos Pereira" # built documents. # # The short X.Y version. -version = "0.0.0" +version = os.getenv("READTHEDOCS_VERSION", "latest") # The full version, including alpha/beta/rc tags. -release = "0.0.0" +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 9f3e87e..02a163a 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2297,6 +2297,24 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK]) + def get_realm_users_profile(self) -> dict: + """ + Get list of attributes and group for given realm. + + Related documentation: + https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_get_adminrealmsrealmusersprofile + + Return https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#UPConfig + :returns: UPConfig + + """ + params_path = {"realm-name": self.connection.realm_name} + + data_raw = self.connection.raw_get( + urls_patterns.URL_ADMIN_REALM_USER_PROFILE.format(**params_path), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK]) + def get_realm_roles(self, brief_representation: bool = True, search_text: str = "") -> list: """ Get all roles for the realm or client. @@ -2911,6 +2929,26 @@ class KeycloakAdmin: expected_codes=[HTTP_NO_CONTENT], ) + def update_realm_users_profile(self, payload: dict) -> dict: + """ + Update realm users profile for the current realm. + + :param up_config: List of attributes, groups, unmamagedAttributePolicy + + Related documentation: + https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#UPConfig + """ + params_path = {"realm-name": self.connection.realm_name} + data_raw = self.connection.raw_put( + urls_patterns.URL_ADMIN_REALM_USER_PROFILE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, + KeycloakPutError, + expected_codes=[HTTP_OK], + ) + def delete_realm_role(self, role_name: str) -> bytes: """ Delete a role for the realm by name. @@ -5577,6 +5615,26 @@ class KeycloakAdmin: expected_codes=[HTTP_NO_CONTENT], ) + async def a_update_realm_users_profile(self, payload: dict) -> dict: + """ + Update realm users profile for the current realm. + + :param up_config: List of attributes, groups, unmamagedAttributePolicy + + Related documentation: + https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#UPConfig + """ + params_path = {"realm-name": self.connection.realm_name} + data_raw = await self.connection.a_raw_put( + urls_patterns.URL_ADMIN_REALM_USER_PROFILE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, + KeycloakPutError, + expected_codes=[HTTP_OK], + ) + async def a_delete_realm(self, realm_name: str) -> bytes: """ Delete a realm asynchronously. @@ -7478,6 +7536,24 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK]) + async def a_get_realm_users_profile(self) -> dict: + """ + Get list of attributes and group for given realm. + + Related documentation: + https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_get_adminrealmsrealmusersprofile + + Return https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#UPConfig + :returns: UPConfig + + """ + params_path = {"realm-name": self.connection.realm_name} + + data_raw = await self.connection.a_raw_get( + urls_patterns.URL_ADMIN_REALM_USER_PROFILE.format(**params_path), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK]) + async def a_get_realm_roles( self, brief_representation: bool = True, diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3508eb3..79af57b 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -153,6 +153,7 @@ URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT = ( URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALM_ROLES_MEMBERS = URL_ADMIN_REALM_ROLES + "/{role-name}/users" URL_ADMIN_REALM_ROLES_GROUPS = URL_ADMIN_REALM_ROLES + "/{role-name}/groups" +URL_ADMIN_REALM_USER_PROFILE = "admin/realms/{realm-name}/users/profile" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index cca2c55..51c45b9 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -204,6 +204,28 @@ def test_realms(admin: KeycloakAdmin) -> None: assert "master" in realm_names, realm_names assert "test" in realm_names, realm_names + + if os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"] == "latest" or Version( + os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"], + ) >= Version("24"): + # Get users profile, add an attribute + user_profile = admin.get_realm_users_profile() + assert "attributes" in user_profile + + new_attribute = { + "name": "surname", + "displayName": "", + "validations": {}, + "annotations": {}, + "permissions": {"view": [], "edit": ["admin"]}, + "multivalued": False, + } + user_profile["attributes"].append(new_attribute) + + res = admin.update_realm_users_profile(user_profile) + # Check for new attribute in result + assert "surname" in [x["name"] for x in res["attributes"]] + # Delete the realm res = admin.delete_realm(realm_name="test") assert res == {}, res @@ -3427,6 +3449,29 @@ async def test_a_realms(admin: KeycloakAdmin) -> None: assert "master" in realm_names, realm_names assert "test" in realm_names, realm_names + # Get users profile, add an attribute and check + user_profile = await admin.a_get_realm_users_profile() + assert "attributes" in user_profile + + + if os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"] == "latest" or Version( + os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"], + ) >= Version("24"): + new_attribute = { + "name": "nickname", + "displayName": "", + "validations": {}, + "annotations": {}, + "permissions": {"view": [], "edit": ["admin"]}, + "multivalued": False, + } + + user_profile["attributes"].append(new_attribute) + + res = await admin.a_update_realm_users_profile(user_profile) + # Check for new attribute in result + assert "nickname" in [x["name"] for x in res["attributes"]] + # Delete the realm res = await admin.a_delete_realm(realm_name="test") assert res == {}, res