From ad4bb5c3a3c3f8407197b299fad47cdec13c3373 Mon Sep 17 00:00:00 2001 From: Martyn Klassen Date: Tue, 14 Mar 2023 13:28:35 -0400 Subject: [PATCH] feat: Added initial access token support --- src/keycloak/keycloak_admin.py | 18 +++++++++++++++++ src/keycloak/keycloak_openid.py | 22 ++++++++++++++++++++ src/keycloak/urls_patterns.py | 4 ++++ tests/test_keycloak_admin.py | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index af177aa..e6d0d0b 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1848,6 +1848,24 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakDeleteError) + def create_initial_access_token(self, count: int = 1, expiration: int = 1): + """Create an initial access token. + + :param count: Number of clients that can be registered + :type count: int + :param expiration: Days until expireation + :type expiration: int + :return: initial access token + :rtype: str + """ + payload = {"count": count, "expiration": expiration} + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_post( + urls_patterns.URL_ADMIN_CLIENT_INITIAL_ACCESS.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[200]) + def create_client(self, payload, skip_exists=False): """Create a client. diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 56e0315..25610ba 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -47,6 +47,7 @@ from .uma_permissions import AuthStatus, build_permission_param from .urls_patterns import ( URL_AUTH, URL_CERTS, + URL_CLIENT_REGISTRATION, URL_ENTITLEMENT, URL_INTROSPECT, URL_LOGOUT, @@ -679,3 +680,24 @@ class KeycloakOpenID: return AuthStatus( is_logged_in=True, is_authorized=len(needed) == 0, missing_permissions=needed ) + + def register_client(self, token: str, payload: dict): + """Create a client. + + ClientRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + + :param token: Initial access token + :type token: str + :param payload: ClientRepresentation + :type payload: dict + :return: Client Representation + :rtype: dict + """ + params_path = {"realm-name": self.realm_name} + self.connection.add_param_headers("Authorization", "Bearer " + token) + self.connection.add_param_headers("Content-Type", "application/json") + data_raw = self.connection.raw_post( + URL_CLIENT_REGISTRATION.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakPostError) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 7a0bf28..e5c54ab 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -38,6 +38,9 @@ URL_AUTH = ( "&scope={scope}&state={state}" ) +URL_CLIENT_REGISTRATION = URL_REALM + "/clients-registrations/default" +URL_CLIENT_UPDATE = URL_CLIENT_REGISTRATION + "/{client-id}" + # ADMIN URLS URL_ADMIN_USERS = "admin/realms/{realm-name}/users" URL_ADMIN_USERS_COUNT = "admin/realms/{realm-name}/users/count" @@ -83,6 +86,7 @@ URL_ADMIN_GROUP_CHILD = "admin/realms/{realm-name}/groups/{id}/children" URL_ADMIN_GROUP_PERMISSIONS = "admin/realms/{realm-name}/groups/{id}/management/permissions" URL_ADMIN_GROUP_MEMBERS = "admin/realms/{realm-name}/groups/{id}/members" +URL_ADMIN_CLIENT_INITIAL_ACCESS = "admin/realms/{realm-name}/clients-initial-access" URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" URL_ADMIN_CLIENT_ALL_SESSIONS = URL_ADMIN_CLIENT + "/user-sessions" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index c458b97..c6957d0 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1,6 +1,7 @@ """Test the keycloak admin object.""" import copy +import uuid from typing import Tuple import freezegun @@ -2588,3 +2589,38 @@ def test_clear_user_cache(realm: str, admin: KeycloakAdmin) -> None: admin.realm_name = realm res = admin.clear_user_cache() assert res == {} + + +def test_initial_access_token( + admin: KeycloakAdmin, oid_with_credentials: Tuple[KeycloakOpenID, str, str] +) -> None: + """Test initial access token and client creation. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + """ + res = admin.create_initial_access_token(2, 3) + assert "token" in res + assert res["count"] == 2 + assert res["expiration"] == 3 + + oid, username, password = oid_with_credentials + + client = str(uuid.uuid4()) + secret = str(uuid.uuid4()) + + res = oid.register_client( + token=res["token"], + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "secret": secret, + "clientAuthenticatorType": "client-secret", + }, + ) + assert res["clientId"] == client