From 2b66f668f7bf4e8b299656b9b18a0ae2ffaa75ec Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 15:06:34 -0600 Subject: [PATCH 01/22] basic user login and testing --- tests/tests_deployment/test_keycloak_api.py | 124 ++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/tests_deployment/test_keycloak_api.py diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py new file mode 100644 index 0000000000..62da194159 --- /dev/null +++ b/tests/tests_deployment/test_keycloak_api.py @@ -0,0 +1,124 @@ +"""Keycloak API endpoint tests.""" + +import os + +import pytest +import requests + + +@pytest.fixture(scope="session") +def keycloak_base_url() -> str: + """Get the base URL for Keycloak.""" + return os.getenv("KEYCLOAK_BASE_URL", "https://tylertesting42.io/auth/") + + +@pytest.fixture(scope="session") +def keycloak_username() -> str: + """Get the Keycloak admin username.""" + return os.getenv("KEYCLOAK_USERNAME", "root") + + +@pytest.fixture(scope="session") +def keycloak_password() -> str: + """Get the Keycloak admin password.""" + return os.getenv("KEYCLOAK_PASSWORD", "e7kjiszh9ykuzhkopnnw7vmgixyl5vto") + + +@pytest.fixture(scope="session") +def keycloak_realm() -> str: + """Get the Keycloak realm name.""" + return os.getenv("KEYCLOAK_REALM", "master") + + +@pytest.fixture(scope="session") +def keycloak_client_id() -> str: + """Get the Keycloak client ID for authentication.""" + return os.getenv("KEYCLOAK_CLIENT_ID", "admin-cli") + + +@pytest.fixture(scope="session") +def verify_ssl() -> bool: + """Determine if SSL verification should be enabled. + + If KEYCLOAK_VERIFY_SSL is 1 or true, SSL verification is enabled. + For all other inputs, SSL verification is disabled (default). + """ + verify = os.environ.get("KEYCLOAK_VERIFY_SSL", "false") + return verify.lower() in ("1", "true") + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_keycloak_login_with_credentials( + keycloak_base_url: str, + keycloak_username: str, + keycloak_password: str, + keycloak_realm: str, + keycloak_client_id: str, + verify_ssl: bool, +) -> None: + """Test that we can authenticate with Keycloak using username/password.""" + # Construct the token endpoint URL + token_url = f"{keycloak_base_url}realms/{keycloak_realm}/protocol/openid-connect/token" + + # Prepare the authentication request + payload = { + "grant_type": "password", + "client_id": keycloak_client_id, + "username": keycloak_username, + "password": keycloak_password, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + # Make the authentication request + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=verify_ssl, + ) + + # Assert successful authentication + assert response.status_code == 200, f"Authentication failed: {response.text}" + + # Verify the response contains expected fields + token_data = response.json() + assert "access_token" in token_data + assert "refresh_token" in token_data + assert "token_type" in token_data + assert token_data["token_type"].lower() == "bearer" + assert "expires_in" in token_data + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_keycloak_login_invalid_credentials( + keycloak_base_url: str, + keycloak_realm: str, + keycloak_client_id: str, + verify_ssl: bool, +) -> None: + """Test that authentication fails with invalid credentials.""" + token_url = f"{keycloak_base_url}realms/{keycloak_realm}/protocol/openid-connect/token" + + payload = { + "grant_type": "password", + "client_id": keycloak_client_id, + "username": "invalid_user", + "password": "invalid_password", + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=verify_ssl, + ) + + # Assert authentication fails + assert response.status_code == 401 From daa723861b980faeca121662b6db4a32fd8dc1a7 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 16:08:50 -0600 Subject: [PATCH 02/22] add helper function --- tests/tests_deployment/keycloak_api_utils.py | 107 +++++++++++++++++++ tests/tests_deployment/test_keycloak_api.py | 88 ++++++--------- 2 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 tests/tests_deployment/keycloak_api_utils.py diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py new file mode 100644 index 0000000000..985633b096 --- /dev/null +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -0,0 +1,107 @@ +"""Helper class for Keycloak API interactions.""" + +import requests + +TIMEOUT = 10 + + +class KeycloakAPI: + """ + Helper class for making requests to Keycloak. + Handles OAuth2 authentication flows. + """ + + def __init__( + self, + base_url: str, + realm: str, + client_id: str, + username: str = None, + password: str = None, + verify_ssl: bool = True, + ) -> None: + """Initialize Keycloak API client. + + Parameters + ---------- + base_url : str + Base URL for Keycloak (e.g., "https://example.com/auth") + realm : str + Keycloak realm name + client_id : str + OAuth2 client ID + username : str, optional + Username for authentication + password : str, optional + Password for authentication + verify_ssl : bool + Whether to verify SSL certificates + """ + self.verify_ssl = verify_ssl + self.base_url = base_url.rstrip('/') + self.realm = realm + self.client_id = client_id + self.username = username + self.password = password + self.access_token = None + self.refresh_token = None + self.token_type = None + + # Authenticate if credentials provided + if username and password: + self.authenticate() + + def _get_token_url(self) -> str: + """Construct the token endpoint URL.""" + return f"{self.base_url}/realms/{self.realm}/protocol/openid-connect/token" + + def authenticate(self, username: str = None, password: str = None) -> dict: + """Authenticate with Keycloak using username and password. + + Parameters + ---------- + username : str, optional + Username to authenticate with (uses instance username if not provided) + password : str, optional + Password to authenticate with (uses instance password if not provided) + + Returns + ------- + dict + Token response containing access_token, refresh_token, etc. + """ + username = username or self.username + password = password or self.password + + if not username or not password: + raise ValueError("Username and password are required for authentication") + + token_url = self._get_token_url() + + payload = { + "grant_type": "password", + "client_id": self.client_id, + "username": username, + "password": password, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=self.verify_ssl, + timeout=TIMEOUT, + ) + + response.raise_for_status() + + token_data = response.json() + self.access_token = token_data.get("access_token") + self.refresh_token = token_data.get("refresh_token") + self.token_type = token_data.get("token_type") + + return token_data diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 62da194159..5b8068d343 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -4,12 +4,13 @@ import pytest import requests +from .keycloak_api_utils import KeycloakAPI @pytest.fixture(scope="session") def keycloak_base_url() -> str: """Get the base URL for Keycloak.""" - return os.getenv("KEYCLOAK_BASE_URL", "https://tylertesting42.io/auth/") + return os.getenv("KEYCLOAK_BASE_URL", "https://tylertesting42.io/auth") @pytest.fixture(scope="session") @@ -47,78 +48,53 @@ def verify_ssl() -> bool: return verify.lower() in ("1", "true") -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_keycloak_login_with_credentials( +@pytest.fixture(scope="session") +def keycloak_api( keycloak_base_url: str, - keycloak_username: str, - keycloak_password: str, keycloak_realm: str, keycloak_client_id: str, verify_ssl: bool, -) -> None: - """Test that we can authenticate with Keycloak using username/password.""" - # Construct the token endpoint URL - token_url = f"{keycloak_base_url}realms/{keycloak_realm}/protocol/openid-connect/token" - - # Prepare the authentication request - payload = { - "grant_type": "password", - "client_id": keycloak_client_id, - "username": keycloak_username, - "password": keycloak_password, - } - - headers = { - "Content-Type": "application/x-www-form-urlencoded", - } - - # Make the authentication request - response = requests.post( - token_url, - data=payload, - headers=headers, - verify=verify_ssl, +) -> KeycloakAPI: + """Create a KeycloakAPI instance without authentication.""" + return KeycloakAPI( + base_url=keycloak_base_url, + realm=keycloak_realm, + client_id=keycloak_client_id, + verify_ssl=verify_ssl, ) - # Assert successful authentication - assert response.status_code == 200, f"Authentication failed: {response.text}" + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_keycloak_login_with_credentials( + keycloak_api: KeycloakAPI, + keycloak_username: str, + keycloak_password: str, +) -> None: + """Test that we can authenticate with Keycloak using username/password.""" + # Authenticate using KeycloakAPI + token_data = keycloak_api.authenticate(keycloak_username, keycloak_password) # Verify the response contains expected fields - token_data = response.json() assert "access_token" in token_data assert "refresh_token" in token_data assert "token_type" in token_data assert token_data["token_type"].lower() == "bearer" assert "expires_in" in token_data + # Verify tokens are stored in the instance + assert keycloak_api.access_token is not None + assert keycloak_api.refresh_token is not None + assert keycloak_api.token_type == "Bearer" + @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_invalid_credentials( - keycloak_base_url: str, - keycloak_realm: str, - keycloak_client_id: str, - verify_ssl: bool, + keycloak_api: KeycloakAPI, ) -> None: """Test that authentication fails with invalid credentials.""" - token_url = f"{keycloak_base_url}realms/{keycloak_realm}/protocol/openid-connect/token" - - payload = { - "grant_type": "password", - "client_id": keycloak_client_id, - "username": "invalid_user", - "password": "invalid_password", - } - - headers = { - "Content-Type": "application/x-www-form-urlencoded", - } - - response = requests.post( - token_url, - data=payload, - headers=headers, - verify=verify_ssl, - ) + # Authenticate with invalid credentials should raise an HTTPError + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + keycloak_api.authenticate("invalid_user", "invalid_password") - # Assert authentication fails - assert response.status_code == 401 + # Verify it's a 401 Unauthorized error + assert exc_info.value.response.status_code == 401 From d7315ad5e2208ed22c50cf1545d216fcdb8575b9 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 16:14:34 -0600 Subject: [PATCH 03/22] added tests for user CRUD --- tests/tests_deployment/keycloak_api_utils.py | 144 ++++++++++++++++ tests/tests_deployment/test_keycloak_api.py | 167 +++++++++++++++++++ 2 files changed, 311 insertions(+) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 985633b096..6250174621 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -105,3 +105,147 @@ def authenticate(self, username: str = None, password: str = None) -> dict: self.token_type = token_data.get("token_type") return token_data + + def _get_admin_url(self, endpoint: str) -> str: + """Construct an admin API endpoint URL. + + Parameters + ---------- + endpoint : str + The admin endpoint path (e.g., "users", "users/{id}") + + Returns + ------- + str + Full URL for the admin endpoint + """ + return f"{self.base_url}/admin/realms/{self.realm}/{endpoint}" + + def _make_admin_request( + self, + endpoint: str, + method: str = "GET", + json_data: dict = None, + timeout: int = TIMEOUT, + ) -> requests.Response: + """Make an authenticated request to the Keycloak admin API. + + Parameters + ---------- + endpoint : str + The admin endpoint path + method : str + HTTP method (GET, POST, PUT, DELETE) + json_data : dict, optional + JSON data to send in the request body + timeout : int + Request timeout in seconds + + Returns + ------- + requests.Response + Response from the admin API + """ + if not self.access_token: + raise ValueError("Not authenticated. Call authenticate() first.") + + url = self._get_admin_url(endpoint) + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + } + + response = requests.request( + method, + url, + json=json_data, + headers=headers, + verify=self.verify_ssl, + timeout=timeout, + ) + + return response + + def create_user(self, user_data: dict) -> requests.Response: + """Create a new user in Keycloak. + + Parameters + ---------- + user_data : dict + User data including username, email, firstName, lastName, etc. + Example: {"username": "testuser", "email": "test@example.com", + "enabled": True, "firstName": "Test", "lastName": "User"} + + Returns + ------- + requests.Response + Response from the create user request + """ + return self._make_admin_request("users", method="POST", json_data=user_data) + + def get_users(self, username: str = None) -> requests.Response: + """Get users from Keycloak. + + Parameters + ---------- + username : str, optional + Filter users by exact username match + + Returns + ------- + requests.Response + Response containing list of users + """ + endpoint = "users" + if username: + endpoint = f"users?username={username}" + return self._make_admin_request(endpoint) + + def get_user_by_id(self, user_id: str) -> requests.Response: + """Get a specific user by ID. + + Parameters + ---------- + user_id : str + The Keycloak user ID + + Returns + ------- + requests.Response + Response containing user data + """ + return self._make_admin_request(f"users/{user_id}") + + def update_user(self, user_id: str, user_data: dict) -> requests.Response: + """Update a user in Keycloak. + + Parameters + ---------- + user_id : str + The Keycloak user ID + user_data : dict + User data to update (partial updates supported) + + Returns + ------- + requests.Response + Response from the update request + """ + return self._make_admin_request( + f"users/{user_id}", method="PUT", json_data=user_data + ) + + def delete_user(self, user_id: str) -> requests.Response: + """Delete a user from Keycloak. + + Parameters + ---------- + user_id : str + The Keycloak user ID to delete + + Returns + ------- + requests.Response + Response from the delete request + """ + return self._make_admin_request(f"users/{user_id}", method="DELETE") diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 5b8068d343..ea9cc4a094 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -64,6 +64,34 @@ def keycloak_api( ) +@pytest.fixture(scope="session") +def authenticated_keycloak_api( + keycloak_base_url: str, + keycloak_username: str, + keycloak_password: str, + keycloak_realm: str, + keycloak_client_id: str, + verify_ssl: bool, +) -> KeycloakAPI: + """Create an authenticated KeycloakAPI instance for admin operations.""" + api = KeycloakAPI( + base_url=keycloak_base_url, + realm=keycloak_realm, + client_id=keycloak_client_id, + username=keycloak_username, + password=keycloak_password, + verify_ssl=verify_ssl, + ) + return api + + +@pytest.fixture +def test_username() -> str: + """Generate a unique test username.""" + import uuid + return f"testuser_{uuid.uuid4().hex[:8]}" + + @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_with_credentials( keycloak_api: KeycloakAPI, @@ -98,3 +126,142 @@ def test_keycloak_login_invalid_credentials( # Verify it's a 401 Unauthorized error assert exc_info.value.response.status_code == 401 + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test creating a new user in Keycloak.""" + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Test", + "lastName": "User", + "enabled": True, + } + + response = authenticated_keycloak_api.create_user(user_data) + + # Keycloak returns 201 Created for successful user creation + assert response.status_code == 201, f"Failed to create user: {response.text}" + + # Cleanup: Delete the created user + # Get the user to retrieve the ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + assert get_response.status_code == 200 + users = get_response.json() + if users: + user_id = users[0]["id"] + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_users( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test getting users from Keycloak.""" + response = authenticated_keycloak_api.get_users() + + assert response.status_code == 200, f"Failed to get users: {response.text}" + users = response.json() + assert isinstance(users, list), "Expected a list of users" + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_user_by_username( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test getting a specific user by username.""" + # First, create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user by username + response = authenticated_keycloak_api.get_users(username=test_username) + assert response.status_code == 200, f"Failed to get user: {response.text}" + + users = response.json() + assert len(users) > 0, "User not found" + assert users[0]["username"] == test_username + + # Cleanup: Delete the test user + user_id = users[0]["id"] + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_update_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test updating a user in Keycloak.""" + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Original", + "lastName": "Name", + "enabled": True, + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] + + # Update the user + update_data = { + "firstName": "Updated", + "lastName": "User", + } + update_response = authenticated_keycloak_api.update_user(user_id, update_data) + assert update_response.status_code == 204, f"Failed to update user: {update_response.text}" + + # Verify the update + verify_response = authenticated_keycloak_api.get_user_by_id(user_id) + assert verify_response.status_code == 200 + updated_user = verify_response.json() + assert updated_user["firstName"] == "Updated" + assert updated_user["lastName"] == "User" + + # Cleanup: Delete the test user + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test deleting a user from Keycloak.""" + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] + + # Delete the user + delete_response = authenticated_keycloak_api.delete_user(user_id) + assert delete_response.status_code == 204, f"Failed to delete user: {delete_response.text}" + + # Verify the user is deleted + verify_response = authenticated_keycloak_api.get_user_by_id(user_id) + assert verify_response.status_code == 404, "User should not exist after deletion" From 4d4ce09961a639d52f6202ab0a7d61e53821642e Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 16:21:37 -0600 Subject: [PATCH 04/22] client CRUD working --- tests/tests_deployment/keycloak_api_utils.py | 84 +++++++++++ tests/tests_deployment/test_keycloak_api.py | 150 +++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 6250174621..fff74b0df6 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -249,3 +249,87 @@ def delete_user(self, user_id: str) -> requests.Response: Response from the delete request """ return self._make_admin_request(f"users/{user_id}", method="DELETE") + + def create_client(self, client_data: dict) -> requests.Response: + """Create a new client in Keycloak. + + Parameters + ---------- + client_data : dict + Client data including clientId, protocol, publicClient, etc. + Example: {"clientId": "test-client", "enabled": True, + "publicClient": False, "protocol": "openid-connect"} + + Returns + ------- + requests.Response + Response from the create client request + """ + return self._make_admin_request("clients", method="POST", json_data=client_data) + + def get_clients(self, client_id: str = None) -> requests.Response: + """Get clients from Keycloak. + + Parameters + ---------- + client_id : str, optional + Filter clients by clientId (not the internal ID) + + Returns + ------- + requests.Response + Response containing list of clients + """ + endpoint = "clients" + if client_id: + endpoint = f"clients?clientId={client_id}" + return self._make_admin_request(endpoint) + + def get_client_by_id(self, id: str) -> requests.Response: + """Get a specific client by internal ID. + + Parameters + ---------- + id : str + The Keycloak client internal ID (not clientId) + + Returns + ------- + requests.Response + Response containing client data + """ + return self._make_admin_request(f"clients/{id}") + + def update_client(self, id: str, client_data: dict) -> requests.Response: + """Update a client in Keycloak. + + Parameters + ---------- + id : str + The Keycloak client internal ID + client_data : dict + Client data to update (partial updates supported) + + Returns + ------- + requests.Response + Response from the update request + """ + return self._make_admin_request( + f"clients/{id}", method="PUT", json_data=client_data + ) + + def delete_client(self, id: str) -> requests.Response: + """Delete a client from Keycloak. + + Parameters + ---------- + id : str + The Keycloak client internal ID to delete + + Returns + ------- + requests.Response + Response from the delete request + """ + return self._make_admin_request(f"clients/{id}", method="DELETE") diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index ea9cc4a094..75f308756d 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -92,6 +92,13 @@ def test_username() -> str: return f"testuser_{uuid.uuid4().hex[:8]}" +@pytest.fixture +def test_client_id() -> str: + """Generate a unique test client ID.""" + import uuid + return f"test-client-{uuid.uuid4().hex[:8]}" + + @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_with_credentials( keycloak_api: KeycloakAPI, @@ -265,3 +272,146 @@ def test_delete_user( # Verify the user is deleted verify_response = authenticated_keycloak_api.get_user_by_id(user_id) assert verify_response.status_code == 404, "User should not exist after deletion" + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_client( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test creating a new client in Keycloak.""" + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "redirectUris": ["http://localhost:8080/*"], + } + + response = authenticated_keycloak_api.create_client(client_data) + + # Keycloak returns 201 Created for successful client creation + assert response.status_code == 201, f"Failed to create client: {response.text}" + + # Cleanup: Delete the created client + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + assert get_response.status_code == 200 + clients = get_response.json() + if clients: + client_internal_id = clients[0]["id"] + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_clients( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test getting clients from Keycloak.""" + response = authenticated_keycloak_api.get_clients() + + assert response.status_code == 200, f"Failed to get clients: {response.text}" + clients = response.json() + assert isinstance(clients, list), "Expected a list of clients" + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_client_by_client_id( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test getting a specific client by clientId.""" + # First, create a test client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client by clientId + response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + assert response.status_code == 200, f"Failed to get client: {response.text}" + + clients = response.json() + assert len(clients) > 0, "Client not found" + assert clients[0]["clientId"] == test_client_id + + # Cleanup: Delete the test client + client_internal_id = clients[0]["id"] + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_update_client( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test updating a client in Keycloak.""" + # Create a test client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + "description": "Original description", + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] + + # Update the client + update_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "description": "Updated description", + } + update_response = authenticated_keycloak_api.update_client(client_internal_id, update_data) + assert update_response.status_code == 204, f"Failed to update client: {update_response.text}" + + # Verify the update + verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) + assert verify_response.status_code == 200 + updated_client = verify_response.json() + assert updated_client["description"] == "Updated description" + assert updated_client["publicClient"] is False + + # Cleanup: Delete the test client + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_client( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test deleting a client from Keycloak.""" + # Create a test client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] + + # Delete the client + delete_response = authenticated_keycloak_api.delete_client(client_internal_id) + assert delete_response.status_code == 204, f"Failed to delete client: {delete_response.text}" + + # Verify the client is deleted + verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) + assert verify_response.status_code == 404, "Client should not exist after deletion" From 4ac96e0b593889bd0932e7158d22a0d5be833e66 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 16:48:21 -0600 Subject: [PATCH 05/22] add the rest of the required tests --- pytest.ini | 6 + tests/tests_deployment/keycloak_api_utils.py | 598 +++++++++++++++++++ tests/tests_deployment/test_keycloak_api.py | 412 ++++++++++++- 3 files changed, 1013 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index d299f154a8..78fe8c5d62 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,6 +9,12 @@ addopts = markers = gpu: test gpu working properly preemptible: test preemptible instances + keycloak: all Keycloak API tests + keycloak_users: Keycloak user CRUD tests + keycloak_clients: Keycloak client CRUD tests + keycloak_roles: Keycloak role CRUD tests + keycloak_groups: Keycloak group CRUD tests + keycloak_integration: Keycloak end-to-end integration tests testpaths = tests xfail_strict = True diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index fff74b0df6..51be501c7b 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -1,10 +1,41 @@ """Helper class for Keycloak API interactions.""" +import base64 +import json import requests TIMEOUT = 10 +def decode_jwt_token(token: str) -> dict: + """Decode a JWT token without verification (for testing purposes). + + Parameters + ---------- + token : str + The JWT token to decode + + Returns + ------- + dict + The decoded token payload + """ + # Split the token into parts + parts = token.split('.') + if len(parts) != 3: + raise ValueError("Invalid JWT token format") + + # Decode the payload (second part) + payload = parts[1] + # Add padding if needed + padding = len(payload) % 4 + if padding: + payload += '=' * (4 - padding) + + decoded = base64.urlsafe_b64decode(payload) + return json.loads(decoded) + + class KeycloakAPI: """ Helper class for making requests to Keycloak. @@ -333,3 +364,570 @@ def delete_client(self, id: str) -> requests.Response: Response from the delete request """ return self._make_admin_request(f"clients/{id}", method="DELETE") + + def reset_user_password(self, user_id: str, password: str, temporary: bool = False) -> requests.Response: + """Reset a user's password. + + Parameters + ---------- + user_id : str + The Keycloak user ID + password : str + The new password + temporary : bool + Whether the password is temporary (user must change on next login) + + Returns + ------- + requests.Response + Response from the password reset request + """ + password_data = { + "type": "password", + "value": password, + "temporary": temporary + } + return self._make_admin_request( + f"users/{user_id}/reset-password", method="PUT", json_data=password_data + ) + + def get_client_secret(self, id: str) -> requests.Response: + """Get the secret for a confidential client. + + Parameters + ---------- + id : str + The Keycloak client internal ID + + Returns + ------- + requests.Response + Response containing the client secret + """ + return self._make_admin_request(f"clients/{id}/client-secret") + + def regenerate_client_secret(self, id: str) -> requests.Response: + """Regenerate the secret for a confidential client. + + Parameters + ---------- + id : str + The Keycloak client internal ID + + Returns + ------- + requests.Response + Response containing the new client secret + """ + return self._make_admin_request(f"clients/{id}/client-secret", method="POST") + + def create_realm_role(self, role_data: dict) -> requests.Response: + """Create a new realm role. + + Parameters + ---------- + role_data : dict + Role data including name and description + Example: {"name": "test-role", "description": "Test role"} + + Returns + ------- + requests.Response + Response from the create role request + """ + return self._make_admin_request("roles", method="POST", json_data=role_data) + + def get_realm_roles(self) -> requests.Response: + """Get all realm roles. + + Returns + ------- + requests.Response + Response containing list of realm roles + """ + return self._make_admin_request("roles") + + def get_realm_role_by_name(self, role_name: str) -> requests.Response: + """Get a specific realm role by name. + + Parameters + ---------- + role_name : str + The role name + + Returns + ------- + requests.Response + Response containing role data + """ + return self._make_admin_request(f"roles/{role_name}") + + def delete_realm_role(self, role_name: str) -> requests.Response: + """Delete a realm role. + + Parameters + ---------- + role_name : str + The role name to delete + + Returns + ------- + requests.Response + Response from the delete request + """ + return self._make_admin_request(f"roles/{role_name}", method="DELETE") + + def create_client_role(self, client_id: str, role_data: dict) -> requests.Response: + """Create a new client role. + + Parameters + ---------- + client_id : str + The Keycloak client internal ID + role_data : dict + Role data including name and description + + Returns + ------- + requests.Response + Response from the create role request + """ + return self._make_admin_request( + f"clients/{client_id}/roles", method="POST", json_data=role_data + ) + + def get_client_roles(self, client_id: str) -> requests.Response: + """Get all roles for a specific client. + + Parameters + ---------- + client_id : str + The Keycloak client internal ID + + Returns + ------- + requests.Response + Response containing list of client roles + """ + return self._make_admin_request(f"clients/{client_id}/roles") + + def get_client_role_by_name(self, client_id: str, role_name: str) -> requests.Response: + """Get a specific client role by name. + + Parameters + ---------- + client_id : str + The Keycloak client internal ID + role_name : str + The role name + + Returns + ------- + requests.Response + Response containing role data + """ + return self._make_admin_request(f"clients/{client_id}/roles/{role_name}") + + def delete_client_role(self, client_id: str, role_name: str) -> requests.Response: + """Delete a client role. + + Parameters + ---------- + client_id : str + The Keycloak client internal ID + role_name : str + The role name to delete + + Returns + ------- + requests.Response + Response from the delete request + """ + return self._make_admin_request( + f"clients/{client_id}/roles/{role_name}", method="DELETE" + ) + + def assign_realm_roles_to_user(self, user_id: str, roles: list) -> requests.Response: + """Assign realm roles to a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + roles : list + List of role representations (must include 'id' and 'name') + + Returns + ------- + requests.Response + Response from the assignment request + """ + return self._make_admin_request( + f"users/{user_id}/role-mappings/realm", method="POST", json_data=roles + ) + + def get_user_realm_roles(self, user_id: str) -> requests.Response: + """Get realm roles assigned to a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + + Returns + ------- + requests.Response + Response containing list of user's realm roles + """ + return self._make_admin_request(f"users/{user_id}/role-mappings/realm") + + def remove_realm_roles_from_user(self, user_id: str, roles: list) -> requests.Response: + """Remove realm roles from a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + roles : list + List of role representations to remove + + Returns + ------- + requests.Response + Response from the removal request + """ + return self._make_admin_request( + f"users/{user_id}/role-mappings/realm", method="DELETE", json_data=roles + ) + + def assign_client_roles_to_user( + self, user_id: str, client_id: str, roles: list + ) -> requests.Response: + """Assign client roles to a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + client_id : str + The Keycloak client internal ID + roles : list + List of role representations + + Returns + ------- + requests.Response + Response from the assignment request + """ + return self._make_admin_request( + f"users/{user_id}/role-mappings/clients/{client_id}", + method="POST", + json_data=roles, + ) + + def get_user_client_roles(self, user_id: str, client_id: str) -> requests.Response: + """Get client roles assigned to a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + client_id : str + The Keycloak client internal ID + + Returns + ------- + requests.Response + Response containing list of user's client roles + """ + return self._make_admin_request( + f"users/{user_id}/role-mappings/clients/{client_id}" + ) + + def remove_client_roles_from_user( + self, user_id: str, client_id: str, roles: list + ) -> requests.Response: + """Remove client roles from a user. + + Parameters + ---------- + user_id : str + The Keycloak user ID + client_id : str + The Keycloak client internal ID + roles : list + List of role representations to remove + + Returns + ------- + requests.Response + Response from the removal request + """ + return self._make_admin_request( + f"users/{user_id}/role-mappings/clients/{client_id}", + method="DELETE", + json_data=roles, + ) + + def create_group(self, group_data: dict) -> requests.Response: + """Create a new group. + + Parameters + ---------- + group_data : dict + Group data including name + Example: {"name": "test-group"} + + Returns + ------- + requests.Response + Response from the create group request + """ + return self._make_admin_request("groups", method="POST", json_data=group_data) + + def get_groups(self) -> requests.Response: + """Get all groups. + + Returns + ------- + requests.Response + Response containing list of groups + """ + return self._make_admin_request("groups") + + def get_group_by_id(self, group_id: str) -> requests.Response: + """Get a specific group by ID. + + Parameters + ---------- + group_id : str + The Keycloak group ID + + Returns + ------- + requests.Response + Response containing group data + """ + return self._make_admin_request(f"groups/{group_id}") + + def delete_group(self, group_id: str) -> requests.Response: + """Delete a group. + + Parameters + ---------- + group_id : str + The Keycloak group ID to delete + + Returns + ------- + requests.Response + Response from the delete request + """ + return self._make_admin_request(f"groups/{group_id}", method="DELETE") + + def add_user_to_group(self, user_id: str, group_id: str) -> requests.Response: + """Add a user to a group. + + Parameters + ---------- + user_id : str + The Keycloak user ID + group_id : str + The Keycloak group ID + + Returns + ------- + requests.Response + Response from the add user request + """ + return self._make_admin_request( + f"users/{user_id}/groups/{group_id}", method="PUT" + ) + + def remove_user_from_group(self, user_id: str, group_id: str) -> requests.Response: + """Remove a user from a group. + + Parameters + ---------- + user_id : str + The Keycloak user ID + group_id : str + The Keycloak group ID + + Returns + ------- + requests.Response + Response from the remove user request + """ + return self._make_admin_request( + f"users/{user_id}/groups/{group_id}", method="DELETE" + ) + + def get_user_groups(self, user_id: str) -> requests.Response: + """Get groups that a user is a member of. + + Parameters + ---------- + user_id : str + The Keycloak user ID + + Returns + ------- + requests.Response + Response containing list of user's groups + """ + return self._make_admin_request(f"users/{user_id}/groups") + + def get_group_members(self, group_id: str) -> requests.Response: + """Get members of a group. + + Parameters + ---------- + group_id : str + The Keycloak group ID + + Returns + ------- + requests.Response + Response containing list of group members + """ + return self._make_admin_request(f"groups/{group_id}/members") + + def assign_realm_roles_to_group(self, group_id: str, roles: list) -> requests.Response: + """Assign realm roles to a group. + + Parameters + ---------- + group_id : str + The Keycloak group ID + roles : list + List of role representations + + Returns + ------- + requests.Response + Response from the assignment request + """ + return self._make_admin_request( + f"groups/{group_id}/role-mappings/realm", method="POST", json_data=roles + ) + + def get_group_realm_roles(self, group_id: str) -> requests.Response: + """Get realm roles assigned to a group. + + Parameters + ---------- + group_id : str + The Keycloak group ID + + Returns + ------- + requests.Response + Response containing list of group's realm roles + """ + return self._make_admin_request(f"groups/{group_id}/role-mappings/realm") + + def create_subgroup(self, parent_group_id: str, group_data: dict) -> requests.Response: + """Create a subgroup under a parent group. + + Parameters + ---------- + parent_group_id : str + The Keycloak parent group ID + group_data : dict + Group data including name + + Returns + ------- + requests.Response + Response from the create subgroup request + """ + return self._make_admin_request( + f"groups/{parent_group_id}/children", method="POST", json_data=group_data + ) + + def oauth2_client_credentials_flow(self, client_id: str, client_secret: str) -> dict: + """Perform OAuth2 client credentials flow. + + Parameters + ---------- + client_id : str + The OAuth2 client ID + client_secret : str + The OAuth2 client secret + + Returns + ------- + dict + Token response containing access_token + """ + token_url = self._get_token_url() + + payload = { + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=self.verify_ssl, + timeout=TIMEOUT, + ) + + response.raise_for_status() + return response.json() + + def oauth2_password_flow( + self, client_id: str, username: str, password: str, client_secret: str = None + ) -> dict: + """Perform OAuth2 resource owner password flow. + + Parameters + ---------- + client_id : str + The OAuth2 client ID + username : str + Username for authentication + password : str + Password for authentication + client_secret : str, optional + The OAuth2 client secret (for confidential clients) + + Returns + ------- + dict + Token response containing access_token + """ + token_url = self._get_token_url() + + payload = { + "grant_type": "password", + "client_id": client_id, + "username": username, + "password": password, + } + + if client_secret: + payload["client_secret"] = client_secret + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=self.verify_ssl, + timeout=TIMEOUT, + ) + + response.raise_for_status() + return response.json() diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 75f308756d..8f0fec3d0f 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -1,10 +1,11 @@ """Keycloak API endpoint tests.""" import os +import uuid import pytest import requests -from .keycloak_api_utils import KeycloakAPI +from .keycloak_api_utils import KeycloakAPI, decode_jwt_token @pytest.fixture(scope="session") @@ -88,17 +89,28 @@ def authenticated_keycloak_api( @pytest.fixture def test_username() -> str: """Generate a unique test username.""" - import uuid return f"testuser_{uuid.uuid4().hex[:8]}" @pytest.fixture def test_client_id() -> str: """Generate a unique test client ID.""" - import uuid return f"test-client-{uuid.uuid4().hex[:8]}" +@pytest.fixture +def test_role_name() -> str: + """Generate a unique test role name.""" + return f"test-role-{uuid.uuid4().hex[:8]}" + + +@pytest.fixture +def test_group_name() -> str: + """Generate a unique test group name.""" + return f"test-group-{uuid.uuid4().hex[:8]}" + + +@pytest.mark.keycloak @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_with_credentials( keycloak_api: KeycloakAPI, @@ -122,6 +134,7 @@ def test_keycloak_login_with_credentials( assert keycloak_api.token_type == "Bearer" +@pytest.mark.keycloak @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_invalid_credentials( keycloak_api: KeycloakAPI, @@ -135,6 +148,8 @@ def test_keycloak_login_invalid_credentials( assert exc_info.value.response.status_code == 401 +@pytest.mark.keycloak +@pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_create_user( authenticated_keycloak_api: KeycloakAPI, @@ -164,6 +179,8 @@ def test_create_user( authenticated_keycloak_api.delete_user(user_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_users( authenticated_keycloak_api: KeycloakAPI, @@ -176,6 +193,8 @@ def test_get_users( assert isinstance(users, list), "Expected a list of users" +@pytest.mark.keycloak +@pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_user_by_username( authenticated_keycloak_api: KeycloakAPI, @@ -204,6 +223,8 @@ def test_get_user_by_username( authenticated_keycloak_api.delete_user(user_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_update_user( authenticated_keycloak_api: KeycloakAPI, @@ -245,6 +266,8 @@ def test_update_user( authenticated_keycloak_api.delete_user(user_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_user( authenticated_keycloak_api: KeycloakAPI, @@ -274,6 +297,197 @@ def test_delete_user( assert verify_response.status_code == 404, "User should not exist after deletion" +@pytest.mark.keycloak +@pytest.mark.keycloak_users +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_users( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all users from Keycloak.""" + response = authenticated_keycloak_api.get_users() + + assert response.status_code == 200, f"Failed to list users: {response.text}" + users = response.json() + assert isinstance(users, list), "Expected a list of users" + # Verify each user has expected fields + if len(users) > 0: + assert "id" in users[0] + assert "username" in users[0] + + +@pytest.mark.keycloak +@pytest.mark.keycloak_users +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_user_by_id( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test getting a specific user by ID.""" + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Test", + "lastName": "User", + "enabled": True, + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] + + # Get user by ID + response = authenticated_keycloak_api.get_user_by_id(user_id) + assert response.status_code == 200, f"Failed to get user by ID: {response.text}" + + user = response.json() + assert user["id"] == user_id + assert user["username"] == test_username + assert user["email"] == f"{test_username}@example.com" + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_users +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_update_user_password( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test updating a user's password and verifying authentication.""" + old_password = "OldPassword123!" + new_password = "NewPassword456!" + + # Create a test user with initial password + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": old_password, + "temporary": False, + } + ], + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] + + # Verify user can authenticate with old password + try: + token_data = keycloak_api.authenticate(test_username, old_password) + assert "access_token" in token_data + except requests.exceptions.HTTPError: + # Some Keycloak configurations may not allow password auth immediately + pass + + # Update the password + update_response = authenticated_keycloak_api.reset_user_password( + user_id, new_password, temporary=False + ) + assert update_response.status_code == 204, f"Failed to update password: {update_response.text}" + + # Verify user can authenticate with new password + token_data = keycloak_api.authenticate(test_username, new_password) + assert "access_token" in token_data + + # Verify old password no longer works + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + keycloak_api.authenticate(test_username, old_password) + assert exc_info.value.response.status_code == 401 + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_users +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_enable_disable_user( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test disabling and re-enabling a user account.""" + password = "TestPassword123!" + + # Create an enabled test user with password + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_response = authenticated_keycloak_api.create_user(user_data) + assert create_response.status_code == 201 + + # Get the user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] + + # Verify user can authenticate while enabled + try: + token_data = keycloak_api.authenticate(test_username, password) + assert "access_token" in token_data + user_can_auth = True + except requests.exceptions.HTTPError: + # Some configurations may not allow immediate auth + user_can_auth = False + + # Disable the user + disable_response = authenticated_keycloak_api.update_user(user_id, {"enabled": False}) + assert disable_response.status_code == 204 + + # Verify user is disabled + verify_response = authenticated_keycloak_api.get_user_by_id(user_id) + disabled_user = verify_response.json() + assert disabled_user["enabled"] is False + + # Verify user cannot authenticate when disabled + # Keycloak may return 400 (Bad Request) or 401 (Unauthorized) for disabled users + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + keycloak_api.authenticate(test_username, password) + assert exc_info.value.response.status_code in (400, 401) + + # Re-enable the user + enable_response = authenticated_keycloak_api.update_user(user_id, {"enabled": True}) + assert enable_response.status_code == 204 + + # Verify user is enabled + verify_response = authenticated_keycloak_api.get_user_by_id(user_id) + enabled_user = verify_response.json() + assert enabled_user["enabled"] is True + + # Verify user can authenticate again (if they could before) + if user_can_auth: + token_data = keycloak_api.authenticate(test_username, password) + assert "access_token" in token_data + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_create_client( authenticated_keycloak_api: KeycloakAPI, @@ -302,6 +516,8 @@ def test_create_client( authenticated_keycloak_api.delete_client(client_internal_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_clients( authenticated_keycloak_api: KeycloakAPI, @@ -314,6 +530,8 @@ def test_get_clients( assert isinstance(clients, list), "Expected a list of clients" +@pytest.mark.keycloak +@pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_client_by_client_id( authenticated_keycloak_api: KeycloakAPI, @@ -343,6 +561,8 @@ def test_get_client_by_client_id( authenticated_keycloak_api.delete_client(client_internal_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_update_client( authenticated_keycloak_api: KeycloakAPI, @@ -387,6 +607,8 @@ def test_update_client( authenticated_keycloak_api.delete_client(client_internal_id) +@pytest.mark.keycloak +@pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_client( authenticated_keycloak_api: KeycloakAPI, @@ -415,3 +637,187 @@ def test_delete_client( # Verify the client is deleted verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) assert verify_response.status_code == 404, "Client should not exist after deletion" + + +@pytest.mark.keycloak +@pytest.mark.keycloak_clients +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_clients( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all clients from Keycloak.""" + response = authenticated_keycloak_api.get_clients() + + assert response.status_code == 200, f"Failed to list clients: {response.text}" + clients = response.json() + assert isinstance(clients, list), "Expected a list of clients" + # Verify each client has expected fields + if len(clients) > 0: + assert "id" in clients[0] + assert "clientId" in clients[0] + + +@pytest.mark.keycloak +@pytest.mark.keycloak_clients +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_client_secret_regeneration( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test regenerating a client secret.""" + # Create a confidential client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] + + # Get the initial secret + secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) + assert secret_response.status_code == 200 + old_secret = secret_response.json()["value"] + + # Regenerate the secret + regen_response = authenticated_keycloak_api.regenerate_client_secret(client_internal_id) + assert regen_response.status_code == 200 + new_secret = regen_response.json()["value"] + + # Verify the secrets are different + assert old_secret != new_secret + + # Verify old secret no longer works for client credentials flow + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + authenticated_keycloak_api.oauth2_client_credentials_flow(test_client_id, old_secret) + assert exc_info.value.response.status_code == 401 + + # Verify new secret works + token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, new_secret + ) + assert "access_token" in token_data + + # Cleanup + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_clients +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_client_oauth2_flow( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, + test_username: str, +) -> None: + """Test OAuth2 client credentials and password flows.""" + password = "TestPassword123!" + + # Create a confidential client with service account enabled + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + "directAccessGrantsEnabled": True, + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client internal ID and secret + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] + + secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) + client_secret = secret_response.json()["value"] + + # Test client credentials flow + token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, client_secret + ) + assert "access_token" in token_data + assert "token_type" in token_data + + # Verify token is valid by decoding it + access_token = token_data["access_token"] + token_payload = decode_jwt_token(access_token) + assert "exp" in token_payload # Has expiration + + # Create a test user for password flow + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + user_create_response = authenticated_keycloak_api.create_user(user_data) + assert user_create_response.status_code == 201 + + # Get user ID for cleanup + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Test password flow + password_token_data = authenticated_keycloak_api.oauth2_password_flow( + test_client_id, test_username, password, client_secret + ) + assert "access_token" in password_token_data + + # Verify user token contains username + user_token_payload = decode_jwt_token(password_token_data["access_token"]) + assert "preferred_username" in user_token_payload + assert user_token_payload["preferred_username"] == test_username + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_clients +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_client_with_invalid_credentials( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test OAuth2 flow with invalid client credentials.""" + # Create a confidential client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get the client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] + + # Attempt OAuth2 flow with invalid secret + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, "invalid-secret" + ) + assert exc_info.value.response.status_code == 401 + + # Cleanup + authenticated_keycloak_api.delete_client(client_internal_id) From 23a7214b2e4223c2e379092351d5dd6e01af6f94 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Mon, 20 Oct 2025 17:02:33 -0600 Subject: [PATCH 06/22] add separate file for roles and groups --- tests/tests_deployment/test_keycloak_api.py | 956 +++++++++++++++++ .../test_keycloak_roles_groups.py | 968 ++++++++++++++++++ 2 files changed, 1924 insertions(+) create mode 100644 tests/tests_deployment/test_keycloak_roles_groups.py diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 8f0fec3d0f..8234840fbf 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -821,3 +821,959 @@ def test_client_with_invalid_credentials( # Cleanup authenticated_keycloak_api.delete_client(client_internal_id) + + +# Role Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_realm_role( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test creating a new realm role.""" + role_data = { + "name": test_role_name, + "description": "Test realm role", + } + + response = authenticated_keycloak_api.create_realm_role(role_data) + assert response.status_code == 201, f"Failed to create realm role: {response.text}" + + # Verify role was created + get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert get_response.status_code == 200 + role = get_response.json() + assert role["name"] == test_role_name + + # Cleanup + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_realm_roles( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all realm roles.""" + response = authenticated_keycloak_api.get_realm_roles() + + assert response.status_code == 200, f"Failed to list realm roles: {response.text}" + roles = response.json() + assert isinstance(roles, list), "Expected a list of roles" + # Verify we have at least some default roles + assert len(roles) > 0 + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_realm_role_by_name( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test getting a specific realm role by name.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role", + } + create_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_response.status_code == 201 + + # Get the role by name + response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert response.status_code == 200 + + role = response.json() + assert role["name"] == test_role_name + assert role["description"] == "Test role" + + # Cleanup + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_realm_role( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test deleting a realm role.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role to delete", + } + create_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_response.status_code == 201 + + # Delete the role + delete_response = authenticated_keycloak_api.delete_realm_role(test_role_name) + assert delete_response.status_code == 204 + + # Verify role is deleted + get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert get_response.status_code == 404 + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_client_role( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, + test_role_name: str, +) -> None: + """Test creating a client role.""" + # First create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_client_response = authenticated_keycloak_api.create_client(client_data) + assert create_client_response.status_code == 201 + + # Get client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_response.json()[0]["id"] + + # Create a client role + role_data = { + "name": test_role_name, + "description": "Test client role", + } + role_response = authenticated_keycloak_api.create_client_role(client_internal_id, role_data) + assert role_response.status_code == 201 + + # Verify role was created + get_role_response = authenticated_keycloak_api.get_client_role_by_name( + client_internal_id, test_role_name + ) + assert get_role_response.status_code == 200 + role = get_role_response.json() + assert role["name"] == test_role_name + + # Cleanup + authenticated_keycloak_api.delete_client_role(client_internal_id, test_role_name) + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_client_roles( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test listing all roles for a client.""" + # Create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_response.json()[0]["id"] + + # Get client roles + roles_response = authenticated_keycloak_api.get_client_roles(client_internal_id) + assert roles_response.status_code == 200 + roles = roles_response.json() + assert isinstance(roles, list) + + # Cleanup + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_assign_realm_role_to_user( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, + test_role_name: str, +) -> None: + """Test assigning a realm role to a user.""" + password = "TestPassword123!" + + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role for assignment", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign role to user + assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( + user_id, [role] + ) + assert assign_response.status_code == 204 + + # Verify user has role + user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) + assert user_roles_response.status_code == 200 + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name in role_names + + # Verify role appears in token + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert test_role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_assign_client_role_to_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_client_id: str, + test_role_name: str, +) -> None: + """Test assigning a client role to a user.""" + # Create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_client_response = authenticated_keycloak_api.create_client(client_data) + assert create_client_response.status_code == 201 + + # Get client internal ID + get_client_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_client_response.json()[0]["id"] + + # Create a client role + role_data = { + "name": test_role_name, + "description": "Test client role for assignment", + } + create_role_response = authenticated_keycloak_api.create_client_role( + client_internal_id, role_data + ) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_client_role_by_name( + client_internal_id, test_role_name + ) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign client role to user + assign_response = authenticated_keycloak_api.assign_client_roles_to_user( + user_id, client_internal_id, [role] + ) + assert assign_response.status_code == 204 + + # Verify user has client role + user_roles_response = authenticated_keycloak_api.get_user_client_roles( + user_id, client_internal_id + ) + assert user_roles_response.status_code == 200 + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name in role_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_remove_role_from_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_role_name: str, +) -> None: + """Test removing a role from a user.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role for removal", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign role to user + assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( + user_id, [role] + ) + assert assign_response.status_code == 204 + + # Remove role from user + remove_response = authenticated_keycloak_api.remove_realm_roles_from_user( + user_id, [role] + ) + assert remove_response.status_code == 204 + + # Verify role is removed + user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name not in role_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +# Group Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_group( + authenticated_keycloak_api: KeycloakAPI, + test_group_name: str, +) -> None: + """Test creating a new group.""" + group_data = { + "name": test_group_name, + } + + response = authenticated_keycloak_api.create_group(group_data) + assert response.status_code == 201, f"Failed to create group: {response.text}" + + # Verify group was created + groups_response = authenticated_keycloak_api.get_groups() + assert groups_response.status_code == 200 + groups = groups_response.json() + group_names = [g["name"] for g in groups] + assert test_group_name in group_names + + # Get group ID for cleanup + group = [g for g in groups if g["name"] == test_group_name][0] + authenticated_keycloak_api.delete_group(group["id"]) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_groups( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all groups.""" + response = authenticated_keycloak_api.get_groups() + + assert response.status_code == 200, f"Failed to list groups: {response.text}" + groups = response.json() + assert isinstance(groups, list), "Expected a list of groups" + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_add_user_to_group( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, +) -> None: + """Test adding a user to a group.""" + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_response.status_code == 204 + + # Verify user is in group + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + assert user_groups_response.status_code == 200 + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert test_group_name in user_group_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_remove_user_from_group( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, +) -> None: + """Test removing a user from a group.""" + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_response.status_code == 204 + + # Remove user from group + remove_response = authenticated_keycloak_api.remove_user_from_group(user_id, group_id) + assert remove_response.status_code == 204 + + # Verify user is removed from group + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert test_group_name not in user_group_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_group( + authenticated_keycloak_api: KeycloakAPI, + test_group_name: str, +) -> None: + """Test deleting a group.""" + # Create a group + group_data = {"name": test_group_name} + create_response = authenticated_keycloak_api.create_group(group_data) + assert create_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Delete the group + delete_response = authenticated_keycloak_api.delete_group(group_id) + assert delete_response.status_code == 204 + + # Verify group is deleted + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group_names = [g["name"] for g in groups] + assert test_group_name not in group_names + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_group_role_assignment( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, + test_role_name: str, +) -> None: + """Test that user inherits roles from group.""" + password = "TestPassword123!" + + # Create a role + role_data = { + "name": test_role_name, + "description": "Test role for group", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Assign role to group + assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( + group_id, [role] + ) + assert assign_role_response.status_code == 204 + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_user_response.status_code == 204 + + # Verify user has role from group in token + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert test_role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_nested_groups( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test nested group hierarchy and role inheritance.""" + parent_group_name = f"parent-group-{uuid.uuid4().hex[:8]}" + child_group_name = f"child-group-{uuid.uuid4().hex[:8]}" + role_name = f"parent-role-{uuid.uuid4().hex[:8]}" + password = "TestPassword123!" + + # Create a role + role_data = { + "name": role_name, + "description": "Test role for parent group", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) + role = role_response.json() + + # Create parent group + parent_group_data = {"name": parent_group_name} + create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) + assert create_parent_response.status_code == 201 + + # Get parent group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + parent_group = [g for g in groups if g["name"] == parent_group_name][0] + parent_group_id = parent_group["id"] + + # Assign role to parent group + assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( + parent_group_id, [role] + ) + assert assign_role_response.status_code == 204 + + # Create child group under parent + child_group_data = {"name": child_group_name} + create_child_response = authenticated_keycloak_api.create_subgroup( + parent_group_id, child_group_data + ) + assert create_child_response.status_code == 201 + + # Get child group ID + parent_details_response = authenticated_keycloak_api.get_group_by_id(parent_group_id) + parent_details = parent_details_response.json() + child_group = parent_details["subGroups"][0] + child_group_id = child_group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to child group + add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, child_group_id) + assert add_user_response.status_code == 204 + + # Verify user inherits role from parent group + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too + authenticated_keycloak_api.delete_realm_role(role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_group_scope_propagation( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test that user gets scopes from multiple groups.""" + group1_name = f"test-group-1-{uuid.uuid4().hex[:8]}" + group2_name = f"test-group-2-{uuid.uuid4().hex[:8]}" + role1_name = f"test-role-1-{uuid.uuid4().hex[:8]}" + role2_name = f"test-role-2-{uuid.uuid4().hex[:8]}" + password = "TestPassword123!" + + # Create two roles + role1_data = {"name": role1_name, "description": "Role 1"} + role2_data = {"name": role2_name, "description": "Role 2"} + authenticated_keycloak_api.create_realm_role(role1_data) + authenticated_keycloak_api.create_realm_role(role2_data) + + # Get roles + role1 = authenticated_keycloak_api.get_realm_role_by_name(role1_name).json() + role2 = authenticated_keycloak_api.get_realm_role_by_name(role2_name).json() + + # Create two groups + authenticated_keycloak_api.create_group({"name": group1_name}) + authenticated_keycloak_api.create_group({"name": group2_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + group1 = [g for g in groups if g["name"] == group1_name][0] + group2 = [g for g in groups if g["name"] == group2_name][0] + group1_id = group1["id"] + group2_id = group2["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(group1_id, [role1]) + authenticated_keycloak_api.assign_realm_roles_to_group(group2_id, [role2]) + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + + # Add user to both groups + authenticated_keycloak_api.add_user_to_group(user_id, group1_id) + authenticated_keycloak_api.add_user_to_group(user_id, group2_id) + + # Verify user has roles from both groups + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + + # Check user is in both groups + user_groups = authenticated_keycloak_api.get_user_groups(user_id).json() + user_group_names = [g["name"] for g in user_groups] + assert group1_name in user_group_names + assert group2_name in user_group_names + + # Check token contains both roles + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role1_name in token_payload["realm_access"]["roles"] + assert role2_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group1_id) + authenticated_keycloak_api.delete_group(group2_id) + authenticated_keycloak_api.delete_realm_role(role1_name) + authenticated_keycloak_api.delete_realm_role(role2_name) + + +# Integration Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_integration +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_admin_user_workflow( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, +) -> None: + """End-to-end test covering complete workflow from admin perspective. + + This test covers: + 1. Admin creates infrastructure (user, groups, roles, OAuth client) + 2. User authenticates + 3. Group associations are verified + 4. OAuth client login with correct permissions + 5. Scope verification from token + 6. Cleanup + """ + test_username = f"workflow-user-{uuid.uuid4().hex[:8]}" + test_password = "WorkflowPassword123!" + test_client_id = f"workflow-client-{uuid.uuid4().hex[:8]}" + admin_group_name = f"workflow-admin-{uuid.uuid4().hex[:8]}" + user_group_name = f"workflow-user-{uuid.uuid4().hex[:8]}" + admin_role_name = f"workflow-admin-role-{uuid.uuid4().hex[:8]}" + user_role_name = f"workflow-user-role-{uuid.uuid4().hex[:8]}" + + # Step 1: Admin Setup + # Create roles + admin_role_data = {"name": admin_role_name, "description": "Admin role"} + user_role_data = {"name": user_role_name, "description": "User role"} + authenticated_keycloak_api.create_realm_role(admin_role_data) + authenticated_keycloak_api.create_realm_role(user_role_data) + + # Get roles + admin_role = authenticated_keycloak_api.get_realm_role_by_name(admin_role_name).json() + user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() + + # Create groups + authenticated_keycloak_api.create_group({"name": admin_group_name}) + authenticated_keycloak_api.create_group({"name": user_group_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + admin_group = [g for g in groups if g["name"] == admin_group_name][0] + user_group = [g for g in groups if g["name"] == user_group_name][0] + admin_group_id = admin_group["id"] + user_group_id = user_group["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) + authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) + + # Create OAuth2 client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + "directAccessGrantsEnabled": True, + "standardFlowEnabled": False, + "implicitFlowEnabled": False, + } + authenticated_keycloak_api.create_client(client_data) + + # Get client internal ID and secret + client_internal_id = authenticated_keycloak_api.get_clients( + client_id=test_client_id + ).json()[0]["id"] + client_secret = authenticated_keycloak_api.get_client_secret( + client_internal_id + ).json()["value"] + + # Create test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Workflow", + "lastName": "User", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": test_password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + + # Assign user to groups + authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) + authenticated_keycloak_api.add_user_to_group(user_id, user_group_id) + + # Step 2: User Authentication + token_data = keycloak_api.authenticate(test_username, test_password) + assert "access_token" in token_data + assert "refresh_token" in token_data + + # Step 3: Group Association Verification + user_details = authenticated_keycloak_api.get_user_by_id(user_id).json() + assert user_details["username"] == test_username + + # Verify user is member of expected groups + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert admin_group_name in user_group_names + assert user_group_name in user_group_names + + # Parse user's access token + user_token_payload = decode_jwt_token(token_data["access_token"]) + assert "preferred_username" in user_token_payload + assert user_token_payload["preferred_username"] == test_username + + # Step 4: OAuth Client Login + # Authenticate as test user through OAuth2 flow + oauth_token_data = authenticated_keycloak_api.oauth2_password_flow( + test_client_id, test_username, test_password, client_secret + ) + assert "access_token" in oauth_token_data + + # Also test client credentials flow + client_token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, client_secret + ) + assert "access_token" in client_token_data + + # Step 5: Scope Verification + oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) + + # Verify token contains expected group memberships and roles + if "realm_access" in oauth_token_payload and "roles" in oauth_token_payload["realm_access"]: + token_roles = oauth_token_payload["realm_access"]["roles"] + assert admin_role_name in token_roles, f"Admin role not found in token roles: {token_roles}" + assert user_role_name in token_roles, f"User role not found in token roles: {token_roles}" + + # Verify user identity in token + assert oauth_token_payload["preferred_username"] == test_username + + # Step 6: Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_client(client_internal_id) + authenticated_keycloak_api.delete_group(admin_group_id) + authenticated_keycloak_api.delete_group(user_group_id) + authenticated_keycloak_api.delete_realm_role(admin_role_name) + authenticated_keycloak_api.delete_realm_role(user_role_name) diff --git a/tests/tests_deployment/test_keycloak_roles_groups.py b/tests/tests_deployment/test_keycloak_roles_groups.py new file mode 100644 index 0000000000..3ef6afd8bf --- /dev/null +++ b/tests/tests_deployment/test_keycloak_roles_groups.py @@ -0,0 +1,968 @@ +"""Keycloak API tests for roles, groups, and integration workflows.""" + +import os +import uuid + +import pytest +import requests +from .keycloak_api_utils import KeycloakAPI, decode_jwt_token + + +# Import fixtures from test_keycloak_api +pytest_plugins = ["tests.tests_deployment.test_keycloak_api"] + + +# Role Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_realm_role( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test creating a new realm role.""" + role_data = { + "name": test_role_name, + "description": "Test realm role", + } + + response = authenticated_keycloak_api.create_realm_role(role_data) + assert response.status_code == 201, f"Failed to create realm role: {response.text}" + + # Verify role was created + get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert get_response.status_code == 200 + role = get_response.json() + assert role["name"] == test_role_name + + # Cleanup + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_realm_roles( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all realm roles.""" + response = authenticated_keycloak_api.get_realm_roles() + + assert response.status_code == 200, f"Failed to list realm roles: {response.text}" + roles = response.json() + assert isinstance(roles, list), "Expected a list of roles" + # Verify we have at least some default roles + assert len(roles) > 0 + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_get_realm_role_by_name( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test getting a specific realm role by name.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role", + } + create_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_response.status_code == 201 + + # Get the role by name + response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert response.status_code == 200 + + role = response.json() + assert role["name"] == test_role_name + assert role["description"] == "Test role" + + # Cleanup + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_realm_role( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> None: + """Test deleting a realm role.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role to delete", + } + create_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_response.status_code == 201 + + # Delete the role + delete_response = authenticated_keycloak_api.delete_realm_role(test_role_name) + assert delete_response.status_code == 204 + + # Verify role is deleted + get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + assert get_response.status_code == 404 + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_client_role( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, + test_role_name: str, +) -> None: + """Test creating a client role.""" + # First create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_client_response = authenticated_keycloak_api.create_client(client_data) + assert create_client_response.status_code == 201 + + # Get client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_response.json()[0]["id"] + + # Create a client role + role_data = { + "name": test_role_name, + "description": "Test client role", + } + role_response = authenticated_keycloak_api.create_client_role(client_internal_id, role_data) + assert role_response.status_code == 201 + + # Verify role was created + get_role_response = authenticated_keycloak_api.get_client_role_by_name( + client_internal_id, test_role_name + ) + assert get_role_response.status_code == 200 + role = get_role_response.json() + assert role["name"] == test_role_name + + # Cleanup + authenticated_keycloak_api.delete_client_role(client_internal_id, test_role_name) + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_client_roles( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> None: + """Test listing all roles for a client.""" + # Create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_response = authenticated_keycloak_api.create_client(client_data) + assert create_response.status_code == 201 + + # Get client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_response.json()[0]["id"] + + # Get client roles + roles_response = authenticated_keycloak_api.get_client_roles(client_internal_id) + assert roles_response.status_code == 200 + roles = roles_response.json() + assert isinstance(roles, list) + + # Cleanup + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_assign_realm_role_to_user( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, + test_role_name: str, +) -> None: + """Test assigning a realm role to a user.""" + password = "TestPassword123!" + + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role for assignment", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign role to user + assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( + user_id, [role] + ) + assert assign_response.status_code == 204 + + # Verify user has role + user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) + assert user_roles_response.status_code == 200 + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name in role_names + + # Verify role appears in token + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert test_role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_assign_client_role_to_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_client_id: str, + test_role_name: str, +) -> None: + """Test assigning a client role to a user.""" + # Create a client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + create_client_response = authenticated_keycloak_api.create_client(client_data) + assert create_client_response.status_code == 201 + + # Get client internal ID + get_client_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + client_internal_id = get_client_response.json()[0]["id"] + + # Create a client role + role_data = { + "name": test_role_name, + "description": "Test client role for assignment", + } + create_role_response = authenticated_keycloak_api.create_client_role( + client_internal_id, role_data + ) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_client_role_by_name( + client_internal_id, test_role_name + ) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign client role to user + assign_response = authenticated_keycloak_api.assign_client_roles_to_user( + user_id, client_internal_id, [role] + ) + assert assign_response.status_code == 204 + + # Verify user has client role + user_roles_response = authenticated_keycloak_api.get_user_client_roles( + user_id, client_internal_id + ) + assert user_roles_response.status_code == 200 + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name in role_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_client(client_internal_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_roles +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_remove_role_from_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_role_name: str, +) -> None: + """Test removing a role from a user.""" + # Create a test role + role_data = { + "name": test_role_name, + "description": "Test role for removal", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Assign role to user + assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( + user_id, [role] + ) + assert assign_response.status_code == 204 + + # Remove role from user + remove_response = authenticated_keycloak_api.remove_realm_roles_from_user( + user_id, [role] + ) + assert remove_response.status_code == 204 + + # Verify role is removed + user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name not in role_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +# Group Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_create_group( + authenticated_keycloak_api: KeycloakAPI, + test_group_name: str, +) -> None: + """Test creating a new group.""" + group_data = { + "name": test_group_name, + } + + response = authenticated_keycloak_api.create_group(group_data) + assert response.status_code == 201, f"Failed to create group: {response.text}" + + # Verify group was created + groups_response = authenticated_keycloak_api.get_groups() + assert groups_response.status_code == 200 + groups = groups_response.json() + group_names = [g["name"] for g in groups] + assert test_group_name in group_names + + # Get group ID for cleanup + group = [g for g in groups if g["name"] == test_group_name][0] + authenticated_keycloak_api.delete_group(group["id"]) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_list_groups( + authenticated_keycloak_api: KeycloakAPI, +) -> None: + """Test listing all groups.""" + response = authenticated_keycloak_api.get_groups() + + assert response.status_code == 200, f"Failed to list groups: {response.text}" + groups = response.json() + assert isinstance(groups, list), "Expected a list of groups" + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_add_user_to_group( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, +) -> None: + """Test adding a user to a group.""" + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_response.status_code == 204 + + # Verify user is in group + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + assert user_groups_response.status_code == 200 + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert test_group_name in user_group_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_remove_user_from_group( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, +) -> None: + """Test removing a user from a group.""" + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_response.status_code == 204 + + # Remove user from group + remove_response = authenticated_keycloak_api.remove_user_from_group(user_id, group_id) + assert remove_response.status_code == 204 + + # Verify user is removed from group + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert test_group_name not in user_group_names + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_delete_group( + authenticated_keycloak_api: KeycloakAPI, + test_group_name: str, +) -> None: + """Test deleting a group.""" + # Create a group + group_data = {"name": test_group_name} + create_response = authenticated_keycloak_api.create_group(group_data) + assert create_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Delete the group + delete_response = authenticated_keycloak_api.delete_group(group_id) + assert delete_response.status_code == 204 + + # Verify group is deleted + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group_names = [g["name"] for g in groups] + assert test_group_name not in group_names + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_group_role_assignment( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, + test_group_name: str, + test_role_name: str, +) -> None: + """Test that user inherits roles from group.""" + password = "TestPassword123!" + + # Create a role + role_data = { + "name": test_role_name, + "description": "Test role for group", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = role_response.json() + + # Create a group + group_data = {"name": test_group_name} + create_group_response = authenticated_keycloak_api.create_group(group_data) + assert create_group_response.status_code == 201 + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = [g for g in groups if g["name"] == test_group_name][0] + group_id = group["id"] + + # Assign role to group + assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( + group_id, [role] + ) + assert assign_role_response.status_code == 204 + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to group + add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) + assert add_user_response.status_code == 204 + + # Verify user has role from group in token + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert test_role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group_id) + authenticated_keycloak_api.delete_realm_role(test_role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_nested_groups( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test nested group hierarchy and role inheritance.""" + parent_group_name = f"parent-group-{uuid.uuid4().hex[:8]}" + child_group_name = f"child-group-{uuid.uuid4().hex[:8]}" + role_name = f"parent-role-{uuid.uuid4().hex[:8]}" + password = "TestPassword123!" + + # Create a role + role_data = { + "name": role_name, + "description": "Test role for parent group", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == 201 + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) + role = role_response.json() + + # Create parent group + parent_group_data = {"name": parent_group_name} + create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) + assert create_parent_response.status_code == 201 + + # Get parent group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + parent_group = [g for g in groups if g["name"] == parent_group_name][0] + parent_group_id = parent_group["id"] + + # Assign role to parent group + assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( + parent_group_id, [role] + ) + assert assign_role_response.status_code == 204 + + # Create child group under parent + child_group_data = {"name": child_group_name} + create_child_response = authenticated_keycloak_api.create_subgroup( + parent_group_id, child_group_data + ) + assert create_child_response.status_code == 201 + + # Get child group ID + parent_details_response = authenticated_keycloak_api.get_group_by_id(parent_group_id) + parent_details = parent_details_response.json() + child_group = parent_details["subGroups"][0] + child_group_id = child_group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == 201 + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to child group + add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, child_group_id) + assert add_user_response.status_code == 204 + + # Verify user inherits role from parent group + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too + authenticated_keycloak_api.delete_realm_role(role_name) + + +@pytest.mark.keycloak +@pytest.mark.keycloak_groups +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_group_scope_propagation( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, + test_username: str, +) -> None: + """Test that user gets scopes from multiple groups.""" + group1_name = f"test-group-1-{uuid.uuid4().hex[:8]}" + group2_name = f"test-group-2-{uuid.uuid4().hex[:8]}" + role1_name = f"test-role-1-{uuid.uuid4().hex[:8]}" + role2_name = f"test-role-2-{uuid.uuid4().hex[:8]}" + password = "TestPassword123!" + + # Create two roles + role1_data = {"name": role1_name, "description": "Role 1"} + role2_data = {"name": role2_name, "description": "Role 2"} + authenticated_keycloak_api.create_realm_role(role1_data) + authenticated_keycloak_api.create_realm_role(role2_data) + + # Get roles + role1 = authenticated_keycloak_api.get_realm_role_by_name(role1_name).json() + role2 = authenticated_keycloak_api.get_realm_role_by_name(role2_name).json() + + # Create two groups + authenticated_keycloak_api.create_group({"name": group1_name}) + authenticated_keycloak_api.create_group({"name": group2_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + group1 = [g for g in groups if g["name"] == group1_name][0] + group2 = [g for g in groups if g["name"] == group2_name][0] + group1_id = group1["id"] + group2_id = group2["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(group1_id, [role1]) + authenticated_keycloak_api.assign_realm_roles_to_group(group2_id, [role2]) + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + + # Add user to both groups + authenticated_keycloak_api.add_user_to_group(user_id, group1_id) + authenticated_keycloak_api.add_user_to_group(user_id, group2_id) + + # Verify user has roles from both groups + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + + # Check user is in both groups + user_groups = authenticated_keycloak_api.get_user_groups(user_id).json() + user_group_names = [g["name"] for g in user_groups] + assert group1_name in user_group_names + assert group2_name in user_group_names + + # Check token contains both roles + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role1_name in token_payload["realm_access"]["roles"] + assert role2_name in token_payload["realm_access"]["roles"] + + # Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_group(group1_id) + authenticated_keycloak_api.delete_group(group2_id) + authenticated_keycloak_api.delete_realm_role(role1_name) + authenticated_keycloak_api.delete_realm_role(role2_name) + + +# Integration Tests + + +@pytest.mark.keycloak +@pytest.mark.keycloak_integration +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_admin_user_workflow( + authenticated_keycloak_api: KeycloakAPI, + keycloak_api: KeycloakAPI, +) -> None: + """End-to-end test covering complete workflow from admin perspective. + + This test covers: + 1. Admin creates infrastructure (user, groups, roles, OAuth client) + 2. User authenticates + 3. Group associations are verified + 4. OAuth client login with correct permissions + 5. Scope verification from token + 6. Cleanup + """ + test_username = f"workflow-user-{uuid.uuid4().hex[:8]}" + test_password = "WorkflowPassword123!" + test_client_id = f"workflow-client-{uuid.uuid4().hex[:8]}" + admin_group_name = f"workflow-admin-{uuid.uuid4().hex[:8]}" + user_group_name = f"workflow-user-{uuid.uuid4().hex[:8]}" + admin_role_name = f"workflow-admin-role-{uuid.uuid4().hex[:8]}" + user_role_name = f"workflow-user-role-{uuid.uuid4().hex[:8]}" + + # Step 1: Admin Setup + # Create roles + admin_role_data = {"name": admin_role_name, "description": "Admin role"} + user_role_data = {"name": user_role_name, "description": "User role"} + authenticated_keycloak_api.create_realm_role(admin_role_data) + authenticated_keycloak_api.create_realm_role(user_role_data) + + # Get roles + admin_role = authenticated_keycloak_api.get_realm_role_by_name(admin_role_name).json() + user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() + + # Create groups + authenticated_keycloak_api.create_group({"name": admin_group_name}) + authenticated_keycloak_api.create_group({"name": user_group_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + admin_group = [g for g in groups if g["name"] == admin_group_name][0] + user_group = [g for g in groups if g["name"] == user_group_name][0] + admin_group_id = admin_group["id"] + user_group_id = user_group["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) + authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) + + # Create OAuth2 client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + "directAccessGrantsEnabled": True, + "standardFlowEnabled": False, + "implicitFlowEnabled": False, + } + authenticated_keycloak_api.create_client(client_data) + + # Get client internal ID and secret + client_internal_id = authenticated_keycloak_api.get_clients( + client_id=test_client_id + ).json()[0]["id"] + client_secret = authenticated_keycloak_api.get_client_secret( + client_internal_id + ).json()["value"] + + # Create test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Workflow", + "lastName": "User", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": test_password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + + # Assign user to groups + authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) + authenticated_keycloak_api.add_user_to_group(user_id, user_group_id) + + # Step 2: User Authentication + token_data = keycloak_api.authenticate(test_username, test_password) + assert "access_token" in token_data + assert "refresh_token" in token_data + + # Step 3: Group Association Verification + user_details = authenticated_keycloak_api.get_user_by_id(user_id).json() + assert user_details["username"] == test_username + + # Verify user is member of expected groups + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert admin_group_name in user_group_names + assert user_group_name in user_group_names + + # Parse user's access token + user_token_payload = decode_jwt_token(token_data["access_token"]) + assert "preferred_username" in user_token_payload + assert user_token_payload["preferred_username"] == test_username + + # Step 4: OAuth Client Login + # Authenticate as test user through OAuth2 flow + oauth_token_data = authenticated_keycloak_api.oauth2_password_flow( + test_client_id, test_username, test_password, client_secret + ) + assert "access_token" in oauth_token_data + + # Also test client credentials flow + client_token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, client_secret + ) + assert "access_token" in client_token_data + + # Step 5: Scope Verification + oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) + + # Verify token contains expected group memberships and roles + if "realm_access" in oauth_token_payload and "roles" in oauth_token_payload["realm_access"]: + token_roles = oauth_token_payload["realm_access"]["roles"] + assert admin_role_name in token_roles, f"Admin role not found in token roles: {token_roles}" + assert user_role_name in token_roles, f"User role not found in token roles: {token_roles}" + + # Verify user identity in token + assert oauth_token_payload["preferred_username"] == test_username + + # Step 6: Cleanup + authenticated_keycloak_api.delete_user(user_id) + authenticated_keycloak_api.delete_client(client_internal_id) + authenticated_keycloak_api.delete_group(admin_group_id) + authenticated_keycloak_api.delete_group(user_group_id) + authenticated_keycloak_api.delete_realm_role(admin_role_name) + authenticated_keycloak_api.delete_realm_role(user_role_name) From e273fe28f6deb1433ce2e55735335d0489477eab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:05:29 +0000 Subject: [PATCH 07/22] [pre-commit.ci] Apply automatic pre-commit fixes --- tests/tests_deployment/keycloak_api_utils.py | 41 ++++++---- tests/tests_deployment/test_keycloak_api.py | 82 ++++++++++++++----- .../test_keycloak_roles_groups.py | 49 +++++++---- 3 files changed, 123 insertions(+), 49 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 51be501c7b..67912ae0ef 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -2,6 +2,7 @@ import base64 import json + import requests TIMEOUT = 10 @@ -21,7 +22,7 @@ def decode_jwt_token(token: str) -> dict: The decoded token payload """ # Split the token into parts - parts = token.split('.') + parts = token.split(".") if len(parts) != 3: raise ValueError("Invalid JWT token format") @@ -30,7 +31,7 @@ def decode_jwt_token(token: str) -> dict: # Add padding if needed padding = len(payload) % 4 if padding: - payload += '=' * (4 - padding) + payload += "=" * (4 - padding) decoded = base64.urlsafe_b64decode(payload) return json.loads(decoded) @@ -69,7 +70,7 @@ def __init__( Whether to verify SSL certificates """ self.verify_ssl = verify_ssl - self.base_url = base_url.rstrip('/') + self.base_url = base_url.rstrip("/") self.realm = realm self.client_id = client_id self.username = username @@ -365,7 +366,9 @@ def delete_client(self, id: str) -> requests.Response: """ return self._make_admin_request(f"clients/{id}", method="DELETE") - def reset_user_password(self, user_id: str, password: str, temporary: bool = False) -> requests.Response: + def reset_user_password( + self, user_id: str, password: str, temporary: bool = False + ) -> requests.Response: """Reset a user's password. Parameters @@ -382,11 +385,7 @@ def reset_user_password(self, user_id: str, password: str, temporary: bool = Fal requests.Response Response from the password reset request """ - password_data = { - "type": "password", - "value": password, - "temporary": temporary - } + password_data = {"type": "password", "value": password, "temporary": temporary} return self._make_admin_request( f"users/{user_id}/reset-password", method="PUT", json_data=password_data ) @@ -511,7 +510,9 @@ def get_client_roles(self, client_id: str) -> requests.Response: """ return self._make_admin_request(f"clients/{client_id}/roles") - def get_client_role_by_name(self, client_id: str, role_name: str) -> requests.Response: + def get_client_role_by_name( + self, client_id: str, role_name: str + ) -> requests.Response: """Get a specific client role by name. Parameters @@ -547,7 +548,9 @@ def delete_client_role(self, client_id: str, role_name: str) -> requests.Respons f"clients/{client_id}/roles/{role_name}", method="DELETE" ) - def assign_realm_roles_to_user(self, user_id: str, roles: list) -> requests.Response: + def assign_realm_roles_to_user( + self, user_id: str, roles: list + ) -> requests.Response: """Assign realm roles to a user. Parameters @@ -581,7 +584,9 @@ def get_user_realm_roles(self, user_id: str) -> requests.Response: """ return self._make_admin_request(f"users/{user_id}/role-mappings/realm") - def remove_realm_roles_from_user(self, user_id: str, roles: list) -> requests.Response: + def remove_realm_roles_from_user( + self, user_id: str, roles: list + ) -> requests.Response: """Remove realm roles from a user. Parameters @@ -793,7 +798,9 @@ def get_group_members(self, group_id: str) -> requests.Response: """ return self._make_admin_request(f"groups/{group_id}/members") - def assign_realm_roles_to_group(self, group_id: str, roles: list) -> requests.Response: + def assign_realm_roles_to_group( + self, group_id: str, roles: list + ) -> requests.Response: """Assign realm roles to a group. Parameters @@ -827,7 +834,9 @@ def get_group_realm_roles(self, group_id: str) -> requests.Response: """ return self._make_admin_request(f"groups/{group_id}/role-mappings/realm") - def create_subgroup(self, parent_group_id: str, group_data: dict) -> requests.Response: + def create_subgroup( + self, parent_group_id: str, group_data: dict + ) -> requests.Response: """Create a subgroup under a parent group. Parameters @@ -846,7 +855,9 @@ def create_subgroup(self, parent_group_id: str, group_data: dict) -> requests.Re f"groups/{parent_group_id}/children", method="POST", json_data=group_data ) - def oauth2_client_credentials_flow(self, client_id: str, client_secret: str) -> dict: + def oauth2_client_credentials_flow( + self, client_id: str, client_secret: str + ) -> dict: """Perform OAuth2 client credentials flow. Parameters diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 8234840fbf..2038357993 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -5,6 +5,7 @@ import pytest import requests + from .keycloak_api_utils import KeycloakAPI, decode_jwt_token @@ -253,7 +254,9 @@ def test_update_user( "lastName": "User", } update_response = authenticated_keycloak_api.update_user(user_id, update_data) - assert update_response.status_code == 204, f"Failed to update user: {update_response.text}" + assert ( + update_response.status_code == 204 + ), f"Failed to update user: {update_response.text}" # Verify the update verify_response = authenticated_keycloak_api.get_user_by_id(user_id) @@ -290,7 +293,9 @@ def test_delete_user( # Delete the user delete_response = authenticated_keycloak_api.delete_user(user_id) - assert delete_response.status_code == 204, f"Failed to delete user: {delete_response.text}" + assert ( + delete_response.status_code == 204 + ), f"Failed to delete user: {delete_response.text}" # Verify the user is deleted verify_response = authenticated_keycloak_api.get_user_by_id(user_id) @@ -397,7 +402,9 @@ def test_update_user_password( update_response = authenticated_keycloak_api.reset_user_password( user_id, new_password, temporary=False ) - assert update_response.status_code == 204, f"Failed to update password: {update_response.text}" + assert ( + update_response.status_code == 204 + ), f"Failed to update password: {update_response.text}" # Verify user can authenticate with new password token_data = keycloak_api.authenticate(test_username, new_password) @@ -454,7 +461,9 @@ def test_enable_disable_user( user_can_auth = False # Disable the user - disable_response = authenticated_keycloak_api.update_user(user_id, {"enabled": False}) + disable_response = authenticated_keycloak_api.update_user( + user_id, {"enabled": False} + ) assert disable_response.status_code == 204 # Verify user is disabled @@ -593,8 +602,12 @@ def test_update_client( "protocol": "openid-connect", "description": "Updated description", } - update_response = authenticated_keycloak_api.update_client(client_internal_id, update_data) - assert update_response.status_code == 204, f"Failed to update client: {update_response.text}" + update_response = authenticated_keycloak_api.update_client( + client_internal_id, update_data + ) + assert ( + update_response.status_code == 204 + ), f"Failed to update client: {update_response.text}" # Verify the update verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) @@ -632,7 +645,9 @@ def test_delete_client( # Delete the client delete_response = authenticated_keycloak_api.delete_client(client_internal_id) - assert delete_response.status_code == 204, f"Failed to delete client: {delete_response.text}" + assert ( + delete_response.status_code == 204 + ), f"Failed to delete client: {delete_response.text}" # Verify the client is deleted verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) @@ -687,7 +702,9 @@ def test_client_secret_regeneration( old_secret = secret_response.json()["value"] # Regenerate the secret - regen_response = authenticated_keycloak_api.regenerate_client_secret(client_internal_id) + regen_response = authenticated_keycloak_api.regenerate_client_secret( + client_internal_id + ) assert regen_response.status_code == 200 new_secret = regen_response.json()["value"] @@ -696,7 +713,9 @@ def test_client_secret_regeneration( # Verify old secret no longer works for client credentials flow with pytest.raises(requests.exceptions.HTTPError) as exc_info: - authenticated_keycloak_api.oauth2_client_credentials_flow(test_client_id, old_secret) + authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, old_secret + ) assert exc_info.value.response.status_code == 401 # Verify new secret works @@ -949,7 +968,9 @@ def test_create_client_role( "name": test_role_name, "description": "Test client role", } - role_response = authenticated_keycloak_api.create_client_role(client_internal_id, role_data) + role_response = authenticated_keycloak_api.create_client_role( + client_internal_id, role_data + ) assert role_response.status_code == 201 # Verify role was created @@ -1086,7 +1107,9 @@ def test_assign_client_role_to_user( assert create_client_response.status_code == 201 # Get client internal ID - get_client_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + get_client_response = authenticated_keycloak_api.get_clients( + client_id=test_client_id + ) client_internal_id = get_client_response.json()[0]["id"] # Create a client role @@ -1326,7 +1349,9 @@ def test_remove_user_from_group( assert add_response.status_code == 204 # Remove user from group - remove_response = authenticated_keycloak_api.remove_user_from_group(user_id, group_id) + remove_response = authenticated_keycloak_api.remove_user_from_group( + user_id, group_id + ) assert remove_response.status_code == 204 # Verify user is removed from group @@ -1499,7 +1524,9 @@ def test_nested_groups( assert create_child_response.status_code == 201 # Get child group ID - parent_details_response = authenticated_keycloak_api.get_group_by_id(parent_group_id) + parent_details_response = authenticated_keycloak_api.get_group_by_id( + parent_group_id + ) parent_details = parent_details_response.json() child_group = parent_details["subGroups"][0] child_group_id = child_group["id"] @@ -1525,7 +1552,9 @@ def test_nested_groups( user_id = user_get_response.json()[0]["id"] # Add user to child group - add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, child_group_id) + add_user_response = authenticated_keycloak_api.add_user_to_group( + user_id, child_group_id + ) assert add_user_response.status_code == 204 # Verify user inherits role from parent group @@ -1596,7 +1625,9 @@ def test_group_scope_propagation( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] # Add user to both groups authenticated_keycloak_api.add_user_to_group(user_id, group1_id) @@ -1661,7 +1692,9 @@ def test_admin_user_workflow( authenticated_keycloak_api.create_realm_role(user_role_data) # Get roles - admin_role = authenticated_keycloak_api.get_realm_role_by_name(admin_role_name).json() + admin_role = authenticated_keycloak_api.get_realm_role_by_name( + admin_role_name + ).json() user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() # Create groups @@ -1718,7 +1751,9 @@ def test_admin_user_workflow( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] # Assign user to groups authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) @@ -1762,10 +1797,17 @@ def test_admin_user_workflow( oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) # Verify token contains expected group memberships and roles - if "realm_access" in oauth_token_payload and "roles" in oauth_token_payload["realm_access"]: + if ( + "realm_access" in oauth_token_payload + and "roles" in oauth_token_payload["realm_access"] + ): token_roles = oauth_token_payload["realm_access"]["roles"] - assert admin_role_name in token_roles, f"Admin role not found in token roles: {token_roles}" - assert user_role_name in token_roles, f"User role not found in token roles: {token_roles}" + assert ( + admin_role_name in token_roles + ), f"Admin role not found in token roles: {token_roles}" + assert ( + user_role_name in token_roles + ), f"User role not found in token roles: {token_roles}" # Verify user identity in token assert oauth_token_payload["preferred_username"] == test_username diff --git a/tests/tests_deployment/test_keycloak_roles_groups.py b/tests/tests_deployment/test_keycloak_roles_groups.py index 3ef6afd8bf..5c0d6c6667 100644 --- a/tests/tests_deployment/test_keycloak_roles_groups.py +++ b/tests/tests_deployment/test_keycloak_roles_groups.py @@ -1,12 +1,10 @@ """Keycloak API tests for roles, groups, and integration workflows.""" -import os import uuid import pytest -import requests -from .keycloak_api_utils import KeycloakAPI, decode_jwt_token +from .keycloak_api_utils import KeycloakAPI, decode_jwt_token # Import fixtures from test_keycloak_api pytest_plugins = ["tests.tests_deployment.test_keycloak_api"] @@ -138,7 +136,9 @@ def test_create_client_role( "name": test_role_name, "description": "Test client role", } - role_response = authenticated_keycloak_api.create_client_role(client_internal_id, role_data) + role_response = authenticated_keycloak_api.create_client_role( + client_internal_id, role_data + ) assert role_response.status_code == 201 # Verify role was created @@ -275,7 +275,9 @@ def test_assign_client_role_to_user( assert create_client_response.status_code == 201 # Get client internal ID - get_client_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + get_client_response = authenticated_keycloak_api.get_clients( + client_id=test_client_id + ) client_internal_id = get_client_response.json()[0]["id"] # Create a client role @@ -515,7 +517,9 @@ def test_remove_user_from_group( assert add_response.status_code == 204 # Remove user from group - remove_response = authenticated_keycloak_api.remove_user_from_group(user_id, group_id) + remove_response = authenticated_keycloak_api.remove_user_from_group( + user_id, group_id + ) assert remove_response.status_code == 204 # Verify user is removed from group @@ -688,7 +692,9 @@ def test_nested_groups( assert create_child_response.status_code == 201 # Get child group ID - parent_details_response = authenticated_keycloak_api.get_group_by_id(parent_group_id) + parent_details_response = authenticated_keycloak_api.get_group_by_id( + parent_group_id + ) parent_details = parent_details_response.json() child_group = parent_details["subGroups"][0] child_group_id = child_group["id"] @@ -714,7 +720,9 @@ def test_nested_groups( user_id = user_get_response.json()[0]["id"] # Add user to child group - add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, child_group_id) + add_user_response = authenticated_keycloak_api.add_user_to_group( + user_id, child_group_id + ) assert add_user_response.status_code == 204 # Verify user inherits role from parent group @@ -785,7 +793,9 @@ def test_group_scope_propagation( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] # Add user to both groups authenticated_keycloak_api.add_user_to_group(user_id, group1_id) @@ -850,7 +860,9 @@ def test_admin_user_workflow( authenticated_keycloak_api.create_realm_role(user_role_data) # Get roles - admin_role = authenticated_keycloak_api.get_realm_role_by_name(admin_role_name).json() + admin_role = authenticated_keycloak_api.get_realm_role_by_name( + admin_role_name + ).json() user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() # Create groups @@ -907,7 +919,9 @@ def test_admin_user_workflow( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0]["id"] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] # Assign user to groups authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) @@ -951,10 +965,17 @@ def test_admin_user_workflow( oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) # Verify token contains expected group memberships and roles - if "realm_access" in oauth_token_payload and "roles" in oauth_token_payload["realm_access"]: + if ( + "realm_access" in oauth_token_payload + and "roles" in oauth_token_payload["realm_access"] + ): token_roles = oauth_token_payload["realm_access"]["roles"] - assert admin_role_name in token_roles, f"Admin role not found in token roles: {token_roles}" - assert user_role_name in token_roles, f"User role not found in token roles: {token_roles}" + assert ( + admin_role_name in token_roles + ), f"Admin role not found in token roles: {token_roles}" + assert ( + user_role_name in token_roles + ), f"User role not found in token roles: {token_roles}" # Verify user identity in token assert oauth_token_payload["preferred_username"] == test_username From b5e513b61c812524a2041f00259fae1063edbe8c Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Tue, 21 Oct 2025 15:24:57 -0600 Subject: [PATCH 08/22] clean up tests --- .../test_keycloak_roles_groups.py | 989 ------------------ 1 file changed, 989 deletions(-) delete mode 100644 tests/tests_deployment/test_keycloak_roles_groups.py diff --git a/tests/tests_deployment/test_keycloak_roles_groups.py b/tests/tests_deployment/test_keycloak_roles_groups.py deleted file mode 100644 index 5c0d6c6667..0000000000 --- a/tests/tests_deployment/test_keycloak_roles_groups.py +++ /dev/null @@ -1,989 +0,0 @@ -"""Keycloak API tests for roles, groups, and integration workflows.""" - -import uuid - -import pytest - -from .keycloak_api_utils import KeycloakAPI, decode_jwt_token - -# Import fixtures from test_keycloak_api -pytest_plugins = ["tests.tests_deployment.test_keycloak_api"] - - -# Role Tests - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_create_realm_role( - authenticated_keycloak_api: KeycloakAPI, - test_role_name: str, -) -> None: - """Test creating a new realm role.""" - role_data = { - "name": test_role_name, - "description": "Test realm role", - } - - response = authenticated_keycloak_api.create_realm_role(role_data) - assert response.status_code == 201, f"Failed to create realm role: {response.text}" - - # Verify role was created - get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert get_response.status_code == 200 - role = get_response.json() - assert role["name"] == test_role_name - - # Cleanup - authenticated_keycloak_api.delete_realm_role(test_role_name) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_list_realm_roles( - authenticated_keycloak_api: KeycloakAPI, -) -> None: - """Test listing all realm roles.""" - response = authenticated_keycloak_api.get_realm_roles() - - assert response.status_code == 200, f"Failed to list realm roles: {response.text}" - roles = response.json() - assert isinstance(roles, list), "Expected a list of roles" - # Verify we have at least some default roles - assert len(roles) > 0 - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_get_realm_role_by_name( - authenticated_keycloak_api: KeycloakAPI, - test_role_name: str, -) -> None: - """Test getting a specific realm role by name.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role", - } - create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == 201 - - # Get the role by name - response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert response.status_code == 200 - - role = response.json() - assert role["name"] == test_role_name - assert role["description"] == "Test role" - - # Cleanup - authenticated_keycloak_api.delete_realm_role(test_role_name) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_delete_realm_role( - authenticated_keycloak_api: KeycloakAPI, - test_role_name: str, -) -> None: - """Test deleting a realm role.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role to delete", - } - create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == 201 - - # Delete the role - delete_response = authenticated_keycloak_api.delete_realm_role(test_role_name) - assert delete_response.status_code == 204 - - # Verify role is deleted - get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert get_response.status_code == 404 - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_create_client_role( - authenticated_keycloak_api: KeycloakAPI, - test_client_id: str, - test_role_name: str, -) -> None: - """Test creating a client role.""" - # First create a client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": False, - "protocol": "openid-connect", - } - create_client_response = authenticated_keycloak_api.create_client(client_data) - assert create_client_response.status_code == 201 - - # Get client internal ID - get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - client_internal_id = get_response.json()[0]["id"] - - # Create a client role - role_data = { - "name": test_role_name, - "description": "Test client role", - } - role_response = authenticated_keycloak_api.create_client_role( - client_internal_id, role_data - ) - assert role_response.status_code == 201 - - # Verify role was created - get_role_response = authenticated_keycloak_api.get_client_role_by_name( - client_internal_id, test_role_name - ) - assert get_role_response.status_code == 200 - role = get_role_response.json() - assert role["name"] == test_role_name - - # Cleanup - authenticated_keycloak_api.delete_client_role(client_internal_id, test_role_name) - authenticated_keycloak_api.delete_client(client_internal_id) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_list_client_roles( - authenticated_keycloak_api: KeycloakAPI, - test_client_id: str, -) -> None: - """Test listing all roles for a client.""" - # Create a client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": False, - "protocol": "openid-connect", - } - create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 - - # Get client internal ID - get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - client_internal_id = get_response.json()[0]["id"] - - # Get client roles - roles_response = authenticated_keycloak_api.get_client_roles(client_internal_id) - assert roles_response.status_code == 200 - roles = roles_response.json() - assert isinstance(roles, list) - - # Cleanup - authenticated_keycloak_api.delete_client(client_internal_id) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_assign_realm_role_to_user( - authenticated_keycloak_api: KeycloakAPI, - keycloak_api: KeycloakAPI, - test_username: str, - test_role_name: str, -) -> None: - """Test assigning a realm role to a user.""" - password = "TestPassword123!" - - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role for assignment", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Assign role to user - assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( - user_id, [role] - ) - assert assign_response.status_code == 204 - - # Verify user has role - user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) - assert user_roles_response.status_code == 200 - user_roles = user_roles_response.json() - role_names = [r["name"] for r in user_roles] - assert test_role_name in role_names - - # Verify role appears in token - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert test_role_name in token_payload["realm_access"]["roles"] - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_assign_client_role_to_user( - authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_client_id: str, - test_role_name: str, -) -> None: - """Test assigning a client role to a user.""" - # Create a client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": False, - "protocol": "openid-connect", - } - create_client_response = authenticated_keycloak_api.create_client(client_data) - assert create_client_response.status_code == 201 - - # Get client internal ID - get_client_response = authenticated_keycloak_api.get_clients( - client_id=test_client_id - ) - client_internal_id = get_client_response.json()[0]["id"] - - # Create a client role - role_data = { - "name": test_role_name, - "description": "Test client role for assignment", - } - create_role_response = authenticated_keycloak_api.create_client_role( - client_internal_id, role_data - ) - assert create_role_response.status_code == 201 - - # Get the role - role_response = authenticated_keycloak_api.get_client_role_by_name( - client_internal_id, test_role_name - ) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Assign client role to user - assign_response = authenticated_keycloak_api.assign_client_roles_to_user( - user_id, client_internal_id, [role] - ) - assert assign_response.status_code == 204 - - # Verify user has client role - user_roles_response = authenticated_keycloak_api.get_user_client_roles( - user_id, client_internal_id - ) - assert user_roles_response.status_code == 200 - user_roles = user_roles_response.json() - role_names = [r["name"] for r in user_roles] - assert test_role_name in role_names - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_client(client_internal_id) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_roles -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_remove_role_from_user( - authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_role_name: str, -) -> None: - """Test removing a role from a user.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role for removal", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Assign role to user - assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( - user_id, [role] - ) - assert assign_response.status_code == 204 - - # Remove role from user - remove_response = authenticated_keycloak_api.remove_realm_roles_from_user( - user_id, [role] - ) - assert remove_response.status_code == 204 - - # Verify role is removed - user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) - user_roles = user_roles_response.json() - role_names = [r["name"] for r in user_roles] - assert test_role_name not in role_names - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - - -# Group Tests - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_create_group( - authenticated_keycloak_api: KeycloakAPI, - test_group_name: str, -) -> None: - """Test creating a new group.""" - group_data = { - "name": test_group_name, - } - - response = authenticated_keycloak_api.create_group(group_data) - assert response.status_code == 201, f"Failed to create group: {response.text}" - - # Verify group was created - groups_response = authenticated_keycloak_api.get_groups() - assert groups_response.status_code == 200 - groups = groups_response.json() - group_names = [g["name"] for g in groups] - assert test_group_name in group_names - - # Get group ID for cleanup - group = [g for g in groups if g["name"] == test_group_name][0] - authenticated_keycloak_api.delete_group(group["id"]) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_list_groups( - authenticated_keycloak_api: KeycloakAPI, -) -> None: - """Test listing all groups.""" - response = authenticated_keycloak_api.get_groups() - - assert response.status_code == 200, f"Failed to list groups: {response.text}" - groups = response.json() - assert isinstance(groups, list), "Expected a list of groups" - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_add_user_to_group( - authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, -) -> None: - """Test adding a user to a group.""" - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Add user to group - add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_response.status_code == 204 - - # Verify user is in group - user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) - assert user_groups_response.status_code == 200 - user_groups = user_groups_response.json() - user_group_names = [g["name"] for g in user_groups] - assert test_group_name in user_group_names - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_remove_user_from_group( - authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, -) -> None: - """Test removing a user from a group.""" - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Add user to group - add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_response.status_code == 204 - - # Remove user from group - remove_response = authenticated_keycloak_api.remove_user_from_group( - user_id, group_id - ) - assert remove_response.status_code == 204 - - # Verify user is removed from group - user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) - user_groups = user_groups_response.json() - user_group_names = [g["name"] for g in user_groups] - assert test_group_name not in user_group_names - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_delete_group( - authenticated_keycloak_api: KeycloakAPI, - test_group_name: str, -) -> None: - """Test deleting a group.""" - # Create a group - group_data = {"name": test_group_name} - create_response = authenticated_keycloak_api.create_group(group_data) - assert create_response.status_code == 201 - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Delete the group - delete_response = authenticated_keycloak_api.delete_group(group_id) - assert delete_response.status_code == 204 - - # Verify group is deleted - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group_names = [g["name"] for g in groups] - assert test_group_name not in group_names - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_group_role_assignment( - authenticated_keycloak_api: KeycloakAPI, - keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, - test_role_name: str, -) -> None: - """Test that user inherits roles from group.""" - password = "TestPassword123!" - - # Create a role - role_data = { - "name": test_role_name, - "description": "Test role for group", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Assign role to group - assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( - group_id, [role] - ) - assert assign_role_response.status_code == 204 - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Add user to group - add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_user_response.status_code == 204 - - # Verify user has role from group in token - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert test_role_name in token_payload["realm_access"]["roles"] - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_nested_groups( - authenticated_keycloak_api: KeycloakAPI, - keycloak_api: KeycloakAPI, - test_username: str, -) -> None: - """Test nested group hierarchy and role inheritance.""" - parent_group_name = f"parent-group-{uuid.uuid4().hex[:8]}" - child_group_name = f"child-group-{uuid.uuid4().hex[:8]}" - role_name = f"parent-role-{uuid.uuid4().hex[:8]}" - password = "TestPassword123!" - - # Create a role - role_data = { - "name": role_name, - "description": "Test role for parent group", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) - role = role_response.json() - - # Create parent group - parent_group_data = {"name": parent_group_name} - create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) - assert create_parent_response.status_code == 201 - - # Get parent group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - parent_group = [g for g in groups if g["name"] == parent_group_name][0] - parent_group_id = parent_group["id"] - - # Assign role to parent group - assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( - parent_group_id, [role] - ) - assert assign_role_response.status_code == 204 - - # Create child group under parent - child_group_data = {"name": child_group_name} - create_child_response = authenticated_keycloak_api.create_subgroup( - parent_group_id, child_group_data - ) - assert create_child_response.status_code == 201 - - # Get child group ID - parent_details_response = authenticated_keycloak_api.get_group_by_id( - parent_group_id - ) - parent_details = parent_details_response.json() - child_group = parent_details["subGroups"][0] - child_group_id = child_group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Add user to child group - add_user_response = authenticated_keycloak_api.add_user_to_group( - user_id, child_group_id - ) - assert add_user_response.status_code == 204 - - # Verify user inherits role from parent group - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert role_name in token_payload["realm_access"]["roles"] - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too - authenticated_keycloak_api.delete_realm_role(role_name) - - -@pytest.mark.keycloak -@pytest.mark.keycloak_groups -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_group_scope_propagation( - authenticated_keycloak_api: KeycloakAPI, - keycloak_api: KeycloakAPI, - test_username: str, -) -> None: - """Test that user gets scopes from multiple groups.""" - group1_name = f"test-group-1-{uuid.uuid4().hex[:8]}" - group2_name = f"test-group-2-{uuid.uuid4().hex[:8]}" - role1_name = f"test-role-1-{uuid.uuid4().hex[:8]}" - role2_name = f"test-role-2-{uuid.uuid4().hex[:8]}" - password = "TestPassword123!" - - # Create two roles - role1_data = {"name": role1_name, "description": "Role 1"} - role2_data = {"name": role2_name, "description": "Role 2"} - authenticated_keycloak_api.create_realm_role(role1_data) - authenticated_keycloak_api.create_realm_role(role2_data) - - # Get roles - role1 = authenticated_keycloak_api.get_realm_role_by_name(role1_name).json() - role2 = authenticated_keycloak_api.get_realm_role_by_name(role2_name).json() - - # Create two groups - authenticated_keycloak_api.create_group({"name": group1_name}) - authenticated_keycloak_api.create_group({"name": group2_name}) - - # Get group IDs - groups = authenticated_keycloak_api.get_groups().json() - group1 = [g for g in groups if g["name"] == group1_name][0] - group2 = [g for g in groups if g["name"] == group2_name][0] - group1_id = group1["id"] - group2_id = group2["id"] - - # Assign roles to groups - authenticated_keycloak_api.assign_realm_roles_to_group(group1_id, [role1]) - authenticated_keycloak_api.assign_realm_roles_to_group(group2_id, [role2]) - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - authenticated_keycloak_api.create_user(user_data) - - # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] - - # Add user to both groups - authenticated_keycloak_api.add_user_to_group(user_id, group1_id) - authenticated_keycloak_api.add_user_to_group(user_id, group2_id) - - # Verify user has roles from both groups - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - - # Check user is in both groups - user_groups = authenticated_keycloak_api.get_user_groups(user_id).json() - user_group_names = [g["name"] for g in user_groups] - assert group1_name in user_group_names - assert group2_name in user_group_names - - # Check token contains both roles - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert role1_name in token_payload["realm_access"]["roles"] - assert role2_name in token_payload["realm_access"]["roles"] - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group1_id) - authenticated_keycloak_api.delete_group(group2_id) - authenticated_keycloak_api.delete_realm_role(role1_name) - authenticated_keycloak_api.delete_realm_role(role2_name) - - -# Integration Tests - - -@pytest.mark.keycloak -@pytest.mark.keycloak_integration -@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") -def test_admin_user_workflow( - authenticated_keycloak_api: KeycloakAPI, - keycloak_api: KeycloakAPI, -) -> None: - """End-to-end test covering complete workflow from admin perspective. - - This test covers: - 1. Admin creates infrastructure (user, groups, roles, OAuth client) - 2. User authenticates - 3. Group associations are verified - 4. OAuth client login with correct permissions - 5. Scope verification from token - 6. Cleanup - """ - test_username = f"workflow-user-{uuid.uuid4().hex[:8]}" - test_password = "WorkflowPassword123!" - test_client_id = f"workflow-client-{uuid.uuid4().hex[:8]}" - admin_group_name = f"workflow-admin-{uuid.uuid4().hex[:8]}" - user_group_name = f"workflow-user-{uuid.uuid4().hex[:8]}" - admin_role_name = f"workflow-admin-role-{uuid.uuid4().hex[:8]}" - user_role_name = f"workflow-user-role-{uuid.uuid4().hex[:8]}" - - # Step 1: Admin Setup - # Create roles - admin_role_data = {"name": admin_role_name, "description": "Admin role"} - user_role_data = {"name": user_role_name, "description": "User role"} - authenticated_keycloak_api.create_realm_role(admin_role_data) - authenticated_keycloak_api.create_realm_role(user_role_data) - - # Get roles - admin_role = authenticated_keycloak_api.get_realm_role_by_name( - admin_role_name - ).json() - user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() - - # Create groups - authenticated_keycloak_api.create_group({"name": admin_group_name}) - authenticated_keycloak_api.create_group({"name": user_group_name}) - - # Get group IDs - groups = authenticated_keycloak_api.get_groups().json() - admin_group = [g for g in groups if g["name"] == admin_group_name][0] - user_group = [g for g in groups if g["name"] == user_group_name][0] - admin_group_id = admin_group["id"] - user_group_id = user_group["id"] - - # Assign roles to groups - authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) - authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) - - # Create OAuth2 client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": False, - "protocol": "openid-connect", - "serviceAccountsEnabled": True, - "directAccessGrantsEnabled": True, - "standardFlowEnabled": False, - "implicitFlowEnabled": False, - } - authenticated_keycloak_api.create_client(client_data) - - # Get client internal ID and secret - client_internal_id = authenticated_keycloak_api.get_clients( - client_id=test_client_id - ).json()[0]["id"] - client_secret = authenticated_keycloak_api.get_client_secret( - client_internal_id - ).json()["value"] - - # Create test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "firstName": "Workflow", - "lastName": "User", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": test_password, - "temporary": False, - } - ], - } - authenticated_keycloak_api.create_user(user_data) - - # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] - - # Assign user to groups - authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) - authenticated_keycloak_api.add_user_to_group(user_id, user_group_id) - - # Step 2: User Authentication - token_data = keycloak_api.authenticate(test_username, test_password) - assert "access_token" in token_data - assert "refresh_token" in token_data - - # Step 3: Group Association Verification - user_details = authenticated_keycloak_api.get_user_by_id(user_id).json() - assert user_details["username"] == test_username - - # Verify user is member of expected groups - user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) - user_groups = user_groups_response.json() - user_group_names = [g["name"] for g in user_groups] - assert admin_group_name in user_group_names - assert user_group_name in user_group_names - - # Parse user's access token - user_token_payload = decode_jwt_token(token_data["access_token"]) - assert "preferred_username" in user_token_payload - assert user_token_payload["preferred_username"] == test_username - - # Step 4: OAuth Client Login - # Authenticate as test user through OAuth2 flow - oauth_token_data = authenticated_keycloak_api.oauth2_password_flow( - test_client_id, test_username, test_password, client_secret - ) - assert "access_token" in oauth_token_data - - # Also test client credentials flow - client_token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, client_secret - ) - assert "access_token" in client_token_data - - # Step 5: Scope Verification - oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) - - # Verify token contains expected group memberships and roles - if ( - "realm_access" in oauth_token_payload - and "roles" in oauth_token_payload["realm_access"] - ): - token_roles = oauth_token_payload["realm_access"]["roles"] - assert ( - admin_role_name in token_roles - ), f"Admin role not found in token roles: {token_roles}" - assert ( - user_role_name in token_roles - ), f"User role not found in token roles: {token_roles}" - - # Verify user identity in token - assert oauth_token_payload["preferred_username"] == test_username - - # Step 6: Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_client(client_internal_id) - authenticated_keycloak_api.delete_group(admin_group_id) - authenticated_keycloak_api.delete_group(user_group_id) - authenticated_keycloak_api.delete_realm_role(admin_role_name) - authenticated_keycloak_api.delete_realm_role(user_role_name) From 07c70f6ea539689ccf49ff63170721f5113f557c Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Tue, 21 Oct 2025 16:00:49 -0600 Subject: [PATCH 09/22] add functionality to work with gitlab CI --- tests/tests_deployment/test_keycloak_api.py | 28 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 2038357993..02902de238 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -1,30 +1,50 @@ """Keycloak API endpoint tests.""" import os +import pathlib import uuid import pytest import requests +from _nebari.config import read_configuration +from nebari.plugins import nebari_plugin_manager +from tests.tests_deployment import constants from .keycloak_api_utils import KeycloakAPI, decode_jwt_token +def get_nebari_config(): + config_schema = nebari_plugin_manager.config_schema + config_filepath = constants.NEBARI_CONFIG_PATH + assert pathlib.Path(config_filepath).exists() + config = read_configuration(config_filepath, config_schema) + return config + @pytest.fixture(scope="session") def keycloak_base_url() -> str: """Get the base URL for Keycloak.""" - return os.getenv("KEYCLOAK_BASE_URL", "https://tylertesting42.io/auth") + config = get_nebari_config() + keycloak_server_url = os.environ.get( + "KEYCLOAK_SERVER_URL", f"https://{config.domain}/auth/" + ) + return keycloak_server_url @pytest.fixture(scope="session") def keycloak_username() -> str: """Get the Keycloak admin username.""" - return os.getenv("KEYCLOAK_USERNAME", "root") - + keycloak_admin_username = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "root") + return keycloak_admin_username @pytest.fixture(scope="session") def keycloak_password() -> str: """Get the Keycloak admin password.""" - return os.getenv("KEYCLOAK_PASSWORD", "e7kjiszh9ykuzhkopnnw7vmgixyl5vto") + config = get_nebari_config() + keycloak_admin_password = os.environ.get( + "KEYCLOAK_ADMIN_PASSWORD", + config.security.keycloak.initial_root_password, + ) + return keycloak_admin_password @pytest.fixture(scope="session") From cc0521004ea47904740a6131fffcca81ab491914 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:01:32 +0000 Subject: [PATCH 10/22] [pre-commit.ci] Apply automatic pre-commit fixes --- tests/tests_deployment/test_keycloak_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 02902de238..698e37ff8a 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -6,10 +6,11 @@ import pytest import requests + from _nebari.config import read_configuration from nebari.plugins import nebari_plugin_manager - from tests.tests_deployment import constants + from .keycloak_api_utils import KeycloakAPI, decode_jwt_token @@ -20,6 +21,7 @@ def get_nebari_config(): config = read_configuration(config_filepath, config_schema) return config + @pytest.fixture(scope="session") def keycloak_base_url() -> str: """Get the base URL for Keycloak.""" @@ -30,12 +32,14 @@ def keycloak_base_url() -> str: ) return keycloak_server_url + @pytest.fixture(scope="session") def keycloak_username() -> str: """Get the Keycloak admin username.""" keycloak_admin_username = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "root") return keycloak_admin_username + @pytest.fixture(scope="session") def keycloak_password() -> str: """Get the Keycloak admin password.""" From a1ec9972719df2da7d77cf890846d45e861b92b0 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 15:14:22 -0600 Subject: [PATCH 11/22] add requests to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e0e736d327..e702d4fa69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ dev = [ "pytest", "python-dotenv", "python-hcl2", + "requests", "setuptools==63.4.3", "tqdm", ] From 2ff3e97823f823bfed5aadfa6d4e86a9627887db Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 16:08:39 -0600 Subject: [PATCH 12/22] remove try except for password users --- tests/tests_deployment/test_keycloak_api.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 698e37ff8a..f6bd158cbd 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -414,14 +414,6 @@ def test_update_user_password( users = get_response.json() user_id = users[0]["id"] - # Verify user can authenticate with old password - try: - token_data = keycloak_api.authenticate(test_username, old_password) - assert "access_token" in token_data - except requests.exceptions.HTTPError: - # Some Keycloak configurations may not allow password auth immediately - pass - # Update the password update_response = authenticated_keycloak_api.reset_user_password( user_id, new_password, temporary=False From 15a7c88df5a8d80278264b48463f915f25100ba8 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 16:34:27 -0600 Subject: [PATCH 13/22] add token auto-refresh functionality --- tests/tests_deployment/keycloak_api_utils.py | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 67912ae0ef..1b084d971e 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -2,6 +2,7 @@ import base64 import json +import time import requests @@ -78,6 +79,7 @@ def __init__( self.access_token = None self.refresh_token = None self.token_type = None + self.token_expires_at = None # Authenticate if credentials provided if username and password: @@ -136,6 +138,70 @@ def authenticate(self, username: str = None, password: str = None) -> dict: self.refresh_token = token_data.get("refresh_token") self.token_type = token_data.get("token_type") + # Track token expiration (default to 5 minutes if not provided) + expires_in = token_data.get("expires_in", 300) + self.token_expires_at = time.time() + expires_in + + return token_data + + def refresh_access_token(self) -> dict: + """Refresh the access token using the refresh token. + + Returns + ------- + dict + Token response containing new access_token and refresh_token + + Raises + ------ + ValueError + If no refresh token is available + requests.exceptions.HTTPError + If the refresh request fails (e.g., refresh token expired) + """ + if not self.refresh_token: + # If we have username/password, re-authenticate instead + if self.username and self.password: + return self.authenticate() + raise ValueError( + "No refresh token available and no credentials for re-authentication" + ) + + token_url = self._get_token_url() + + payload = { + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": self.refresh_token, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = requests.post( + token_url, + data=payload, + headers=headers, + verify=self.verify_ssl, + timeout=TIMEOUT, + ) + + # If refresh fails and we have credentials, try to re-authenticate + if response.status_code == 400 and self.username and self.password: + return self.authenticate() + + response.raise_for_status() + + token_data = response.json() + self.access_token = token_data.get("access_token") + self.refresh_token = token_data.get("refresh_token") + self.token_type = token_data.get("token_type") + + # Update token expiration + expires_in = token_data.get("expires_in", 300) + self.token_expires_at = time.time() + expires_in + return token_data def _get_admin_url(self, endpoint: str) -> str: @@ -181,6 +247,10 @@ def _make_admin_request( if not self.access_token: raise ValueError("Not authenticated. Call authenticate() first.") + # Auto-refresh token if expired or expiring soon (30 second buffer) + if self.token_expires_at and time.time() > (self.token_expires_at - 30): + self.refresh_access_token() + url = self._get_admin_url(endpoint) headers = { "Authorization": f"Bearer {self.access_token}", From fae3256d7b89e2642171796a78fcb803b29482cb Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 16:55:13 -0600 Subject: [PATCH 14/22] env var request timeout, warning message for JWT tokens --- tests/tests_deployment/keycloak_api_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 1b084d971e..28571b1a7a 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -2,16 +2,19 @@ import base64 import json +import os import time import requests -TIMEOUT = 10 +TIMEOUT = int(os.getenv("KEYCLOAK_TIMEOUT", "10")) def decode_jwt_token(token: str) -> dict: """Decode a JWT token without verification (for testing purposes). - + ****************************** + DO NOT USE THIS FOR PRODUCTION + ****************************** Parameters ---------- token : str From 0fb334ad6b6047af51aaefe9cef71eb37dd80e87 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 17:00:31 -0600 Subject: [PATCH 15/22] Use HTTPStatus for http return codes --- tests/tests_deployment/keycloak_api_utils.py | 3 +- tests/tests_deployment/test_keycloak_api.py | 185 ++++++++++--------- 2 files changed, 95 insertions(+), 93 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 28571b1a7a..97a8cf6bae 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -4,6 +4,7 @@ import json import os import time +from http import HTTPStatus import requests @@ -191,7 +192,7 @@ def refresh_access_token(self) -> dict: ) # If refresh fails and we have credentials, try to re-authenticate - if response.status_code == 400 and self.username and self.password: + if response.status_code == HTTPStatus.BAD_REQUEST and self.username and self.password: return self.authenticate() response.raise_for_status() diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index f6bd158cbd..0b35e0a322 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -3,6 +3,7 @@ import os import pathlib import uuid +from http import HTTPStatus import pytest import requests @@ -170,7 +171,7 @@ def test_keycloak_login_invalid_credentials( keycloak_api.authenticate("invalid_user", "invalid_password") # Verify it's a 401 Unauthorized error - assert exc_info.value.response.status_code == 401 + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED @pytest.mark.keycloak @@ -192,12 +193,12 @@ def test_create_user( response = authenticated_keycloak_api.create_user(user_data) # Keycloak returns 201 Created for successful user creation - assert response.status_code == 201, f"Failed to create user: {response.text}" + assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" # Cleanup: Delete the created user # Get the user to retrieve the ID get_response = authenticated_keycloak_api.get_users(username=test_username) - assert get_response.status_code == 200 + assert get_response.status_code == HTTPStatus.OK users = get_response.json() if users: user_id = users[0]["id"] @@ -213,7 +214,7 @@ def test_get_users( """Test getting users from Keycloak.""" response = authenticated_keycloak_api.get_users() - assert response.status_code == 200, f"Failed to get users: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to get users: {response.text}" users = response.json() assert isinstance(users, list), "Expected a list of users" @@ -233,11 +234,11 @@ def test_get_user_by_username( "enabled": True, } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user by username response = authenticated_keycloak_api.get_users(username=test_username) - assert response.status_code == 200, f"Failed to get user: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to get user: {response.text}" users = response.json() assert len(users) > 0, "User not found" @@ -265,7 +266,7 @@ def test_update_user( "enabled": True, } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -279,12 +280,12 @@ def test_update_user( } update_response = authenticated_keycloak_api.update_user(user_id, update_data) assert ( - update_response.status_code == 204 + update_response.status_code == HTTPStatus.NO_CONTENT ), f"Failed to update user: {update_response.text}" # Verify the update verify_response = authenticated_keycloak_api.get_user_by_id(user_id) - assert verify_response.status_code == 200 + assert verify_response.status_code == HTTPStatus.OK updated_user = verify_response.json() assert updated_user["firstName"] == "Updated" assert updated_user["lastName"] == "User" @@ -308,7 +309,7 @@ def test_delete_user( "enabled": True, } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -318,12 +319,12 @@ def test_delete_user( # Delete the user delete_response = authenticated_keycloak_api.delete_user(user_id) assert ( - delete_response.status_code == 204 + delete_response.status_code == HTTPStatus.NO_CONTENT ), f"Failed to delete user: {delete_response.text}" # Verify the user is deleted verify_response = authenticated_keycloak_api.get_user_by_id(user_id) - assert verify_response.status_code == 404, "User should not exist after deletion" + assert verify_response.status_code == HTTPStatus.NOT_FOUND, "User should not exist after deletion" @pytest.mark.keycloak @@ -335,7 +336,7 @@ def test_list_users( """Test listing all users from Keycloak.""" response = authenticated_keycloak_api.get_users() - assert response.status_code == 200, f"Failed to list users: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to list users: {response.text}" users = response.json() assert isinstance(users, list), "Expected a list of users" # Verify each user has expected fields @@ -361,7 +362,7 @@ def test_get_user_by_id( "enabled": True, } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -370,7 +371,7 @@ def test_get_user_by_id( # Get user by ID response = authenticated_keycloak_api.get_user_by_id(user_id) - assert response.status_code == 200, f"Failed to get user by ID: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to get user by ID: {response.text}" user = response.json() assert user["id"] == user_id @@ -407,7 +408,7 @@ def test_update_user_password( ], } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -419,7 +420,7 @@ def test_update_user_password( user_id, new_password, temporary=False ) assert ( - update_response.status_code == 204 + update_response.status_code == HTTPStatus.NO_CONTENT ), f"Failed to update password: {update_response.text}" # Verify user can authenticate with new password @@ -429,7 +430,7 @@ def test_update_user_password( # Verify old password no longer works with pytest.raises(requests.exceptions.HTTPError) as exc_info: keycloak_api.authenticate(test_username, old_password) - assert exc_info.value.response.status_code == 401 + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED # Cleanup authenticated_keycloak_api.delete_user(user_id) @@ -460,7 +461,7 @@ def test_enable_disable_user( ], } create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -480,7 +481,7 @@ def test_enable_disable_user( disable_response = authenticated_keycloak_api.update_user( user_id, {"enabled": False} ) - assert disable_response.status_code == 204 + assert disable_response.status_code == HTTPStatus.NO_CONTENT # Verify user is disabled verify_response = authenticated_keycloak_api.get_user_by_id(user_id) @@ -491,11 +492,11 @@ def test_enable_disable_user( # Keycloak may return 400 (Bad Request) or 401 (Unauthorized) for disabled users with pytest.raises(requests.exceptions.HTTPError) as exc_info: keycloak_api.authenticate(test_username, password) - assert exc_info.value.response.status_code in (400, 401) + assert exc_info.value.response.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED) # Re-enable the user enable_response = authenticated_keycloak_api.update_user(user_id, {"enabled": True}) - assert enable_response.status_code == 204 + assert enable_response.status_code == HTTPStatus.NO_CONTENT # Verify user is enabled verify_response = authenticated_keycloak_api.get_user_by_id(user_id) @@ -530,11 +531,11 @@ def test_create_client( response = authenticated_keycloak_api.create_client(client_data) # Keycloak returns 201 Created for successful client creation - assert response.status_code == 201, f"Failed to create client: {response.text}" + assert response.status_code == HTTPStatus.CREATED, f"Failed to create client: {response.text}" # Cleanup: Delete the created client get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - assert get_response.status_code == 200 + assert get_response.status_code == HTTPStatus.OK clients = get_response.json() if clients: client_internal_id = clients[0]["id"] @@ -550,7 +551,7 @@ def test_get_clients( """Test getting clients from Keycloak.""" response = authenticated_keycloak_api.get_clients() - assert response.status_code == 200, f"Failed to get clients: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to get clients: {response.text}" clients = response.json() assert isinstance(clients, list), "Expected a list of clients" @@ -571,11 +572,11 @@ def test_get_client_by_client_id( "protocol": "openid-connect", } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client by clientId response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - assert response.status_code == 200, f"Failed to get client: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to get client: {response.text}" clients = response.json() assert len(clients) > 0, "Client not found" @@ -603,7 +604,7 @@ def test_update_client( "description": "Original description", } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -622,12 +623,12 @@ def test_update_client( client_internal_id, update_data ) assert ( - update_response.status_code == 204 + update_response.status_code == HTTPStatus.NO_CONTENT ), f"Failed to update client: {update_response.text}" # Verify the update verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) - assert verify_response.status_code == 200 + assert verify_response.status_code == HTTPStatus.OK updated_client = verify_response.json() assert updated_client["description"] == "Updated description" assert updated_client["publicClient"] is False @@ -652,7 +653,7 @@ def test_delete_client( "protocol": "openid-connect", } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -662,12 +663,12 @@ def test_delete_client( # Delete the client delete_response = authenticated_keycloak_api.delete_client(client_internal_id) assert ( - delete_response.status_code == 204 + delete_response.status_code == HTTPStatus.NO_CONTENT ), f"Failed to delete client: {delete_response.text}" # Verify the client is deleted verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) - assert verify_response.status_code == 404, "Client should not exist after deletion" + assert verify_response.status_code == HTTPStatus.NOT_FOUND, "Client should not exist after deletion" @pytest.mark.keycloak @@ -679,7 +680,7 @@ def test_list_clients( """Test listing all clients from Keycloak.""" response = authenticated_keycloak_api.get_clients() - assert response.status_code == 200, f"Failed to list clients: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to list clients: {response.text}" clients = response.json() assert isinstance(clients, list), "Expected a list of clients" # Verify each client has expected fields @@ -705,7 +706,7 @@ def test_client_secret_regeneration( "serviceAccountsEnabled": True, } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -714,14 +715,14 @@ def test_client_secret_regeneration( # Get the initial secret secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) - assert secret_response.status_code == 200 + assert secret_response.status_code == HTTPStatus.OK old_secret = secret_response.json()["value"] # Regenerate the secret regen_response = authenticated_keycloak_api.regenerate_client_secret( client_internal_id ) - assert regen_response.status_code == 200 + assert regen_response.status_code == HTTPStatus.OK new_secret = regen_response.json()["value"] # Verify the secrets are different @@ -732,7 +733,7 @@ def test_client_secret_regeneration( authenticated_keycloak_api.oauth2_client_credentials_flow( test_client_id, old_secret ) - assert exc_info.value.response.status_code == 401 + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED # Verify new secret works token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( @@ -765,7 +766,7 @@ def test_client_oauth2_flow( "directAccessGrantsEnabled": True, } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client internal ID and secret get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -801,7 +802,7 @@ def test_client_oauth2_flow( ], } user_create_response = authenticated_keycloak_api.create_user(user_data) - assert user_create_response.status_code == 201 + assert user_create_response.status_code == HTTPStatus.CREATED # Get user ID for cleanup user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -840,7 +841,7 @@ def test_client_with_invalid_credentials( "serviceAccountsEnabled": True, } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -852,7 +853,7 @@ def test_client_with_invalid_credentials( authenticated_keycloak_api.oauth2_client_credentials_flow( test_client_id, "invalid-secret" ) - assert exc_info.value.response.status_code == 401 + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED # Cleanup authenticated_keycloak_api.delete_client(client_internal_id) @@ -875,11 +876,11 @@ def test_create_realm_role( } response = authenticated_keycloak_api.create_realm_role(role_data) - assert response.status_code == 201, f"Failed to create realm role: {response.text}" + assert response.status_code == HTTPStatus.CREATED, f"Failed to create realm role: {response.text}" # Verify role was created get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert get_response.status_code == 200 + assert get_response.status_code == HTTPStatus.OK role = get_response.json() assert role["name"] == test_role_name @@ -896,7 +897,7 @@ def test_list_realm_roles( """Test listing all realm roles.""" response = authenticated_keycloak_api.get_realm_roles() - assert response.status_code == 200, f"Failed to list realm roles: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to list realm roles: {response.text}" roles = response.json() assert isinstance(roles, list), "Expected a list of roles" # Verify we have at least some default roles @@ -917,11 +918,11 @@ def test_get_realm_role_by_name( "description": "Test role", } create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get the role by name response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK role = response.json() assert role["name"] == test_role_name @@ -945,15 +946,15 @@ def test_delete_realm_role( "description": "Test role to delete", } create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Delete the role delete_response = authenticated_keycloak_api.delete_realm_role(test_role_name) - assert delete_response.status_code == 204 + assert delete_response.status_code == HTTPStatus.NO_CONTENT # Verify role is deleted get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - assert get_response.status_code == 404 + assert get_response.status_code == HTTPStatus.NOT_FOUND @pytest.mark.keycloak @@ -973,7 +974,7 @@ def test_create_client_role( "protocol": "openid-connect", } create_client_response = authenticated_keycloak_api.create_client(client_data) - assert create_client_response.status_code == 201 + assert create_client_response.status_code == HTTPStatus.CREATED # Get client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -987,13 +988,13 @@ def test_create_client_role( role_response = authenticated_keycloak_api.create_client_role( client_internal_id, role_data ) - assert role_response.status_code == 201 + assert role_response.status_code == HTTPStatus.CREATED # Verify role was created get_role_response = authenticated_keycloak_api.get_client_role_by_name( client_internal_id, test_role_name ) - assert get_role_response.status_code == 200 + assert get_role_response.status_code == HTTPStatus.OK role = get_role_response.json() assert role["name"] == test_role_name @@ -1018,7 +1019,7 @@ def test_list_client_roles( "protocol": "openid-connect", } create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -1026,7 +1027,7 @@ def test_list_client_roles( # Get client roles roles_response = authenticated_keycloak_api.get_client_roles(client_internal_id) - assert roles_response.status_code == 200 + assert roles_response.status_code == HTTPStatus.OK roles = roles_response.json() assert isinstance(roles, list) @@ -1052,7 +1053,7 @@ def test_assign_realm_role_to_user( "description": "Test role for assignment", } create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 + assert create_role_response.status_code == HTTPStatus.CREATED # Get the role role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -1072,7 +1073,7 @@ def test_assign_realm_role_to_user( ], } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1082,11 +1083,11 @@ def test_assign_realm_role_to_user( assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( user_id, [role] ) - assert assign_response.status_code == 204 + assert assign_response.status_code == HTTPStatus.NO_CONTENT # Verify user has role user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) - assert user_roles_response.status_code == 200 + assert user_roles_response.status_code == HTTPStatus.OK user_roles = user_roles_response.json() role_names = [r["name"] for r in user_roles] assert test_role_name in role_names @@ -1120,7 +1121,7 @@ def test_assign_client_role_to_user( "protocol": "openid-connect", } create_client_response = authenticated_keycloak_api.create_client(client_data) - assert create_client_response.status_code == 201 + assert create_client_response.status_code == HTTPStatus.CREATED # Get client internal ID get_client_response = authenticated_keycloak_api.get_clients( @@ -1136,7 +1137,7 @@ def test_assign_client_role_to_user( create_role_response = authenticated_keycloak_api.create_client_role( client_internal_id, role_data ) - assert create_role_response.status_code == 201 + assert create_role_response.status_code == HTTPStatus.CREATED # Get the role role_response = authenticated_keycloak_api.get_client_role_by_name( @@ -1151,7 +1152,7 @@ def test_assign_client_role_to_user( "enabled": True, } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1161,13 +1162,13 @@ def test_assign_client_role_to_user( assign_response = authenticated_keycloak_api.assign_client_roles_to_user( user_id, client_internal_id, [role] ) - assert assign_response.status_code == 204 + assert assign_response.status_code == HTTPStatus.NO_CONTENT # Verify user has client role user_roles_response = authenticated_keycloak_api.get_user_client_roles( user_id, client_internal_id ) - assert user_roles_response.status_code == 200 + assert user_roles_response.status_code == HTTPStatus.OK user_roles = user_roles_response.json() role_names = [r["name"] for r in user_roles] assert test_role_name in role_names @@ -1192,7 +1193,7 @@ def test_remove_role_from_user( "description": "Test role for removal", } create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 + assert create_role_response.status_code == HTTPStatus.CREATED # Get the role role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -1205,7 +1206,7 @@ def test_remove_role_from_user( "enabled": True, } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1215,13 +1216,13 @@ def test_remove_role_from_user( assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( user_id, [role] ) - assert assign_response.status_code == 204 + assert assign_response.status_code == HTTPStatus.NO_CONTENT # Remove role from user remove_response = authenticated_keycloak_api.remove_realm_roles_from_user( user_id, [role] ) - assert remove_response.status_code == 204 + assert remove_response.status_code == HTTPStatus.NO_CONTENT # Verify role is removed user_roles_response = authenticated_keycloak_api.get_user_realm_roles(user_id) @@ -1250,11 +1251,11 @@ def test_create_group( } response = authenticated_keycloak_api.create_group(group_data) - assert response.status_code == 201, f"Failed to create group: {response.text}" + assert response.status_code == HTTPStatus.CREATED, f"Failed to create group: {response.text}" # Verify group was created groups_response = authenticated_keycloak_api.get_groups() - assert groups_response.status_code == 200 + assert groups_response.status_code == HTTPStatus.OK groups = groups_response.json() group_names = [g["name"] for g in groups] assert test_group_name in group_names @@ -1273,7 +1274,7 @@ def test_list_groups( """Test listing all groups.""" response = authenticated_keycloak_api.get_groups() - assert response.status_code == 200, f"Failed to list groups: {response.text}" + assert response.status_code == HTTPStatus.OK, f"Failed to list groups: {response.text}" groups = response.json() assert isinstance(groups, list), "Expected a list of groups" @@ -1290,7 +1291,7 @@ def test_add_user_to_group( # Create a group group_data = {"name": test_group_name} create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 + assert create_group_response.status_code == HTTPStatus.CREATED # Get group ID groups_response = authenticated_keycloak_api.get_groups() @@ -1305,7 +1306,7 @@ def test_add_user_to_group( "enabled": True, } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1313,11 +1314,11 @@ def test_add_user_to_group( # Add user to group add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_response.status_code == 204 + assert add_response.status_code == HTTPStatus.NO_CONTENT # Verify user is in group user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) - assert user_groups_response.status_code == 200 + assert user_groups_response.status_code == HTTPStatus.OK user_groups = user_groups_response.json() user_group_names = [g["name"] for g in user_groups] assert test_group_name in user_group_names @@ -1339,7 +1340,7 @@ def test_remove_user_from_group( # Create a group group_data = {"name": test_group_name} create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 + assert create_group_response.status_code == HTTPStatus.CREATED # Get group ID groups_response = authenticated_keycloak_api.get_groups() @@ -1354,7 +1355,7 @@ def test_remove_user_from_group( "enabled": True, } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1362,13 +1363,13 @@ def test_remove_user_from_group( # Add user to group add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_response.status_code == 204 + assert add_response.status_code == HTTPStatus.NO_CONTENT # Remove user from group remove_response = authenticated_keycloak_api.remove_user_from_group( user_id, group_id ) - assert remove_response.status_code == 204 + assert remove_response.status_code == HTTPStatus.NO_CONTENT # Verify user is removed from group user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) @@ -1392,7 +1393,7 @@ def test_delete_group( # Create a group group_data = {"name": test_group_name} create_response = authenticated_keycloak_api.create_group(group_data) - assert create_response.status_code == 201 + assert create_response.status_code == HTTPStatus.CREATED # Get group ID groups_response = authenticated_keycloak_api.get_groups() @@ -1402,7 +1403,7 @@ def test_delete_group( # Delete the group delete_response = authenticated_keycloak_api.delete_group(group_id) - assert delete_response.status_code == 204 + assert delete_response.status_code == HTTPStatus.NO_CONTENT # Verify group is deleted groups_response = authenticated_keycloak_api.get_groups() @@ -1430,7 +1431,7 @@ def test_group_role_assignment( "description": "Test role for group", } create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 + assert create_role_response.status_code == HTTPStatus.CREATED # Get the role role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -1439,7 +1440,7 @@ def test_group_role_assignment( # Create a group group_data = {"name": test_group_name} create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == 201 + assert create_group_response.status_code == HTTPStatus.CREATED # Get group ID groups_response = authenticated_keycloak_api.get_groups() @@ -1451,7 +1452,7 @@ def test_group_role_assignment( assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( group_id, [role] ) - assert assign_role_response.status_code == 204 + assert assign_role_response.status_code == HTTPStatus.NO_CONTENT # Create a user user_data = { @@ -1467,7 +1468,7 @@ def test_group_role_assignment( ], } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1475,7 +1476,7 @@ def test_group_role_assignment( # Add user to group add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) - assert add_user_response.status_code == 204 + assert add_user_response.status_code == HTTPStatus.NO_CONTENT # Verify user has role from group in token token_data = keycloak_api.authenticate(test_username, password) @@ -1509,7 +1510,7 @@ def test_nested_groups( "description": "Test role for parent group", } create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == 201 + assert create_role_response.status_code == HTTPStatus.CREATED # Get the role role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) @@ -1518,7 +1519,7 @@ def test_nested_groups( # Create parent group parent_group_data = {"name": parent_group_name} create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) - assert create_parent_response.status_code == 201 + assert create_parent_response.status_code == HTTPStatus.CREATED # Get parent group ID groups_response = authenticated_keycloak_api.get_groups() @@ -1530,14 +1531,14 @@ def test_nested_groups( assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( parent_group_id, [role] ) - assert assign_role_response.status_code == 204 + assert assign_role_response.status_code == HTTPStatus.NO_CONTENT # Create child group under parent child_group_data = {"name": child_group_name} create_child_response = authenticated_keycloak_api.create_subgroup( parent_group_id, child_group_data ) - assert create_child_response.status_code == 201 + assert create_child_response.status_code == HTTPStatus.CREATED # Get child group ID parent_details_response = authenticated_keycloak_api.get_group_by_id( @@ -1561,7 +1562,7 @@ def test_nested_groups( ], } create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == 201 + assert create_user_response.status_code == HTTPStatus.CREATED # Get user ID user_get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -1571,7 +1572,7 @@ def test_nested_groups( add_user_response = authenticated_keycloak_api.add_user_to_group( user_id, child_group_id ) - assert add_user_response.status_code == 204 + assert add_user_response.status_code == HTTPStatus.NO_CONTENT # Verify user inherits role from parent group token_data = keycloak_api.authenticate(test_username, password) From 276195da03c0c56f66400c044513ff1c026b12b2 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Thu, 30 Oct 2025 17:03:22 -0600 Subject: [PATCH 16/22] replace parameters using "id" with descriptive names --- tests/tests_deployment/keycloak_api_utils.py | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 97a8cf6bae..4f8adf20f6 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -391,12 +391,12 @@ def get_clients(self, client_id: str = None) -> requests.Response: endpoint = f"clients?clientId={client_id}" return self._make_admin_request(endpoint) - def get_client_by_id(self, id: str) -> requests.Response: + def get_client_by_id(self, client_internal_id: str) -> requests.Response: """Get a specific client by internal ID. Parameters ---------- - id : str + client_internal_id : str The Keycloak client internal ID (not clientId) Returns @@ -404,14 +404,14 @@ def get_client_by_id(self, id: str) -> requests.Response: requests.Response Response containing client data """ - return self._make_admin_request(f"clients/{id}") + return self._make_admin_request(f"clients/{client_internal_id}") - def update_client(self, id: str, client_data: dict) -> requests.Response: + def update_client(self, client_internal_id: str, client_data: dict) -> requests.Response: """Update a client in Keycloak. Parameters ---------- - id : str + client_internal_id : str The Keycloak client internal ID client_data : dict Client data to update (partial updates supported) @@ -422,15 +422,15 @@ def update_client(self, id: str, client_data: dict) -> requests.Response: Response from the update request """ return self._make_admin_request( - f"clients/{id}", method="PUT", json_data=client_data + f"clients/{client_internal_id}", method="PUT", json_data=client_data ) - def delete_client(self, id: str) -> requests.Response: + def delete_client(self, client_internal_id: str) -> requests.Response: """Delete a client from Keycloak. Parameters ---------- - id : str + client_internal_id : str The Keycloak client internal ID to delete Returns @@ -438,7 +438,7 @@ def delete_client(self, id: str) -> requests.Response: requests.Response Response from the delete request """ - return self._make_admin_request(f"clients/{id}", method="DELETE") + return self._make_admin_request(f"clients/{client_internal_id}", method="DELETE") def reset_user_password( self, user_id: str, password: str, temporary: bool = False @@ -464,12 +464,12 @@ def reset_user_password( f"users/{user_id}/reset-password", method="PUT", json_data=password_data ) - def get_client_secret(self, id: str) -> requests.Response: + def get_client_secret(self, client_internal_id: str) -> requests.Response: """Get the secret for a confidential client. Parameters ---------- - id : str + client_internal_id : str The Keycloak client internal ID Returns @@ -477,14 +477,14 @@ def get_client_secret(self, id: str) -> requests.Response: requests.Response Response containing the client secret """ - return self._make_admin_request(f"clients/{id}/client-secret") + return self._make_admin_request(f"clients/{client_internal_id}/client-secret") - def regenerate_client_secret(self, id: str) -> requests.Response: + def regenerate_client_secret(self, client_internal_id: str) -> requests.Response: """Regenerate the secret for a confidential client. Parameters ---------- - id : str + client_internal_id : str The Keycloak client internal ID Returns @@ -492,7 +492,7 @@ def regenerate_client_secret(self, id: str) -> requests.Response: requests.Response Response containing the new client secret """ - return self._make_admin_request(f"clients/{id}/client-secret", method="POST") + return self._make_admin_request(f"clients/{client_internal_id}/client-secret", method="POST") def create_realm_role(self, role_data: dict) -> requests.Response: """Create a new realm role. From 9682b7a7b82e60be801b41b375ee7237786e6737 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:04:27 +0000 Subject: [PATCH 17/22] [pre-commit.ci] Apply automatic pre-commit fixes --- tests/tests_deployment/keycloak_api_utils.py | 18 ++++-- tests/tests_deployment/test_keycloak_api.py | 61 +++++++++++++++----- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 4f8adf20f6..3ff19fb63a 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -192,7 +192,11 @@ def refresh_access_token(self) -> dict: ) # If refresh fails and we have credentials, try to re-authenticate - if response.status_code == HTTPStatus.BAD_REQUEST and self.username and self.password: + if ( + response.status_code == HTTPStatus.BAD_REQUEST + and self.username + and self.password + ): return self.authenticate() response.raise_for_status() @@ -406,7 +410,9 @@ def get_client_by_id(self, client_internal_id: str) -> requests.Response: """ return self._make_admin_request(f"clients/{client_internal_id}") - def update_client(self, client_internal_id: str, client_data: dict) -> requests.Response: + def update_client( + self, client_internal_id: str, client_data: dict + ) -> requests.Response: """Update a client in Keycloak. Parameters @@ -438,7 +444,9 @@ def delete_client(self, client_internal_id: str) -> requests.Response: requests.Response Response from the delete request """ - return self._make_admin_request(f"clients/{client_internal_id}", method="DELETE") + return self._make_admin_request( + f"clients/{client_internal_id}", method="DELETE" + ) def reset_user_password( self, user_id: str, password: str, temporary: bool = False @@ -492,7 +500,9 @@ def regenerate_client_secret(self, client_internal_id: str) -> requests.Response requests.Response Response containing the new client secret """ - return self._make_admin_request(f"clients/{client_internal_id}/client-secret", method="POST") + return self._make_admin_request( + f"clients/{client_internal_id}/client-secret", method="POST" + ) def create_realm_role(self, role_data: dict) -> requests.Response: """Create a new realm role. diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 0b35e0a322..378ce6d4f9 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -193,7 +193,9 @@ def test_create_user( response = authenticated_keycloak_api.create_user(user_data) # Keycloak returns 201 Created for successful user creation - assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create user: {response.text}" # Cleanup: Delete the created user # Get the user to retrieve the ID @@ -214,7 +216,9 @@ def test_get_users( """Test getting users from Keycloak.""" response = authenticated_keycloak_api.get_users() - assert response.status_code == HTTPStatus.OK, f"Failed to get users: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to get users: {response.text}" users = response.json() assert isinstance(users, list), "Expected a list of users" @@ -324,7 +328,9 @@ def test_delete_user( # Verify the user is deleted verify_response = authenticated_keycloak_api.get_user_by_id(user_id) - assert verify_response.status_code == HTTPStatus.NOT_FOUND, "User should not exist after deletion" + assert ( + verify_response.status_code == HTTPStatus.NOT_FOUND + ), "User should not exist after deletion" @pytest.mark.keycloak @@ -336,7 +342,9 @@ def test_list_users( """Test listing all users from Keycloak.""" response = authenticated_keycloak_api.get_users() - assert response.status_code == HTTPStatus.OK, f"Failed to list users: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to list users: {response.text}" users = response.json() assert isinstance(users, list), "Expected a list of users" # Verify each user has expected fields @@ -371,7 +379,9 @@ def test_get_user_by_id( # Get user by ID response = authenticated_keycloak_api.get_user_by_id(user_id) - assert response.status_code == HTTPStatus.OK, f"Failed to get user by ID: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to get user by ID: {response.text}" user = response.json() assert user["id"] == user_id @@ -492,7 +502,10 @@ def test_enable_disable_user( # Keycloak may return 400 (Bad Request) or 401 (Unauthorized) for disabled users with pytest.raises(requests.exceptions.HTTPError) as exc_info: keycloak_api.authenticate(test_username, password) - assert exc_info.value.response.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED) + assert exc_info.value.response.status_code in ( + HTTPStatus.BAD_REQUEST, + HTTPStatus.UNAUTHORIZED, + ) # Re-enable the user enable_response = authenticated_keycloak_api.update_user(user_id, {"enabled": True}) @@ -531,7 +544,9 @@ def test_create_client( response = authenticated_keycloak_api.create_client(client_data) # Keycloak returns 201 Created for successful client creation - assert response.status_code == HTTPStatus.CREATED, f"Failed to create client: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create client: {response.text}" # Cleanup: Delete the created client get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -551,7 +566,9 @@ def test_get_clients( """Test getting clients from Keycloak.""" response = authenticated_keycloak_api.get_clients() - assert response.status_code == HTTPStatus.OK, f"Failed to get clients: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to get clients: {response.text}" clients = response.json() assert isinstance(clients, list), "Expected a list of clients" @@ -576,7 +593,9 @@ def test_get_client_by_client_id( # Get the client by clientId response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - assert response.status_code == HTTPStatus.OK, f"Failed to get client: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to get client: {response.text}" clients = response.json() assert len(clients) > 0, "Client not found" @@ -668,7 +687,9 @@ def test_delete_client( # Verify the client is deleted verify_response = authenticated_keycloak_api.get_client_by_id(client_internal_id) - assert verify_response.status_code == HTTPStatus.NOT_FOUND, "Client should not exist after deletion" + assert ( + verify_response.status_code == HTTPStatus.NOT_FOUND + ), "Client should not exist after deletion" @pytest.mark.keycloak @@ -680,7 +701,9 @@ def test_list_clients( """Test listing all clients from Keycloak.""" response = authenticated_keycloak_api.get_clients() - assert response.status_code == HTTPStatus.OK, f"Failed to list clients: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to list clients: {response.text}" clients = response.json() assert isinstance(clients, list), "Expected a list of clients" # Verify each client has expected fields @@ -876,7 +899,9 @@ def test_create_realm_role( } response = authenticated_keycloak_api.create_realm_role(role_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create realm role: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create realm role: {response.text}" # Verify role was created get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -897,7 +922,9 @@ def test_list_realm_roles( """Test listing all realm roles.""" response = authenticated_keycloak_api.get_realm_roles() - assert response.status_code == HTTPStatus.OK, f"Failed to list realm roles: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to list realm roles: {response.text}" roles = response.json() assert isinstance(roles, list), "Expected a list of roles" # Verify we have at least some default roles @@ -1251,7 +1278,9 @@ def test_create_group( } response = authenticated_keycloak_api.create_group(group_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create group: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create group: {response.text}" # Verify group was created groups_response = authenticated_keycloak_api.get_groups() @@ -1274,7 +1303,9 @@ def test_list_groups( """Test listing all groups.""" response = authenticated_keycloak_api.get_groups() - assert response.status_code == HTTPStatus.OK, f"Failed to list groups: {response.text}" + assert ( + response.status_code == HTTPStatus.OK + ), f"Failed to list groups: {response.text}" groups = response.json() assert isinstance(groups, list), "Expected a list of groups" From 85831b3d7f5daf23b54c7e0de33d2f98c925c7e1 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Fri, 31 Oct 2025 11:02:28 -0600 Subject: [PATCH 18/22] add missing type hint for function --- tests/tests_deployment/test_keycloak_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 378ce6d4f9..9b4b713d46 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -15,7 +15,7 @@ from .keycloak_api_utils import KeycloakAPI, decode_jwt_token -def get_nebari_config(): +def get_nebari_config() -> dict: config_schema = nebari_plugin_manager.config_schema config_filepath = constants.NEBARI_CONFIG_PATH assert pathlib.Path(config_filepath).exists() From 812f9afec25b63a30f4ee0b476edac367cc52276 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Fri, 31 Oct 2025 13:44:06 -0600 Subject: [PATCH 19/22] add fixtures for auto-deleting entities created in tests --- tests/tests_deployment/test_keycloak_api.py | 1598 ++++++++++--------- 1 file changed, 810 insertions(+), 788 deletions(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 9b4b713d46..4254a8d3b3 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -136,6 +136,261 @@ def test_group_name() -> str: return f"test-group-{uuid.uuid4().hex[:8]}" +# Resource cleanup fixtures + +@pytest.fixture +def created_user( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> dict: + """Create a test user and ensure cleanup. + + Returns + ------- + dict + Dictionary with 'id', 'username', and 'data' keys + """ + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + } + + response = authenticated_keycloak_api.create_user(user_data) + assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" + + # Get user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] if users else None + + user_info = { + "id": user_id, + "username": test_username, + "data": user_data, + } + + yield user_info + + # Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except requests.exceptions.HTTPError as e: + if e.response.status_code == HTTPStatus.NOT_FOUND: + # User was already deleted by the test - this is fine + pass + else: + # Unexpected HTTP error - log it + print(f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}") + except Exception as e: + # Non-HTTP error (network, timeout, etc.) - log it + print(f"WARNING: Failed to delete user {user_id}: {type(e).__name__}: {e}") + + +@pytest.fixture +def created_user_with_password( + authenticated_keycloak_api: KeycloakAPI, + test_username: str, +) -> dict: + """Create a test user with password and ensure cleanup. + + Returns + ------- + dict + Dictionary with 'id', 'username', 'password', and 'data' keys + """ + password = "TestPassword123!" + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + + response = authenticated_keycloak_api.create_user(user_data) + assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" + + # Get user ID + get_response = authenticated_keycloak_api.get_users(username=test_username) + users = get_response.json() + user_id = users[0]["id"] if users else None + + user_info = { + "id": user_id, + "username": test_username, + "password": password, + "data": user_data, + } + + yield user_info + + # Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except requests.exceptions.HTTPError as e: + if e.response.status_code == HTTPStatus.NOT_FOUND: + # User was already deleted by the test - this is fine + pass + else: + # Unexpected HTTP error - log it + print(f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}") + except Exception as e: + # Non-HTTP error (network, timeout, etc.) - log it + print(f"WARNING: Failed to delete user {user_id}: {type(e).__name__}: {e}") + + +@pytest.fixture +def created_client( + authenticated_keycloak_api: KeycloakAPI, + test_client_id: str, +) -> dict: + """Create a test client and ensure cleanup. + + Returns + ------- + dict + Dictionary with 'internal_id', 'client_id', and 'data' keys + """ + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + } + + response = authenticated_keycloak_api.create_client(client_data) + assert response.status_code == HTTPStatus.CREATED, f"Failed to create client: {response.text}" + + # Get client internal ID + get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) + clients = get_response.json() + client_internal_id = clients[0]["id"] if clients else None + + client_info = { + "internal_id": client_internal_id, + "client_id": test_client_id, + "data": client_data, + } + + yield client_info + + # Cleanup + if client_internal_id: + try: + authenticated_keycloak_api.delete_client(client_internal_id) + except requests.exceptions.HTTPError as e: + if e.response.status_code == HTTPStatus.NOT_FOUND: + # Client was already deleted by the test - this is fine + pass + else: + # Unexpected HTTP error - log it + print(f"WARNING: Failed to delete client {client_internal_id}: HTTP {e.response.status_code}") + except Exception as e: + # Non-HTTP error (network, timeout, etc.) - log it + print(f"WARNING: Failed to delete client {client_internal_id}: {type(e).__name__}: {e}") + + +@pytest.fixture +def created_realm_role( + authenticated_keycloak_api: KeycloakAPI, + test_role_name: str, +) -> dict: + """Create a test realm role and ensure cleanup. + + Returns + ------- + dict + Dictionary with 'name' and 'role' (full role object) keys + """ + role_data = { + "name": test_role_name, + "description": "Test realm role", + } + + response = authenticated_keycloak_api.create_realm_role(role_data) + assert response.status_code == HTTPStatus.CREATED, f"Failed to create realm role: {response.text}" + + # Get the full role object + get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) + role = get_response.json() if get_response.status_code == HTTPStatus.OK else None + + role_info = { + "name": test_role_name, + "role": role, + } + + yield role_info + + # Cleanup + try: + authenticated_keycloak_api.delete_realm_role(test_role_name) + except requests.exceptions.HTTPError as e: + if e.response.status_code == HTTPStatus.NOT_FOUND: + # Role was already deleted by the test - this is fine + pass + else: + # Unexpected HTTP error - log it + print(f"WARNING: Failed to delete realm role {test_role_name}: HTTP {e.response.status_code}") + except Exception as e: + # Non-HTTP error (network, timeout, etc.) - log it + print(f"WARNING: Failed to delete realm role {test_role_name}: {type(e).__name__}: {e}") + + +@pytest.fixture +def created_group( + authenticated_keycloak_api: KeycloakAPI, + test_group_name: str, +) -> dict: + """Create a test group and ensure cleanup. + + Returns + ------- + dict + Dictionary with 'id', 'name', and 'data' keys + """ + group_data = {"name": test_group_name} + + response = authenticated_keycloak_api.create_group(group_data) + assert response.status_code == HTTPStatus.CREATED, f"Failed to create group: {response.text}" + + # Get group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + group = next((g for g in groups if g["name"] == test_group_name), None) + group_id = group["id"] if group else None + + group_info = { + "id": group_id, + "name": test_group_name, + "data": group_data, + } + + yield group_info + + # Cleanup + if group_id: + try: + authenticated_keycloak_api.delete_group(group_id) + except requests.exceptions.HTTPError as e: + if e.response.status_code == HTTPStatus.NOT_FOUND: + # Group was already deleted by the test - this is fine + pass + else: + # Unexpected HTTP error - log it + print(f"WARNING: Failed to delete group {group_id}: HTTP {e.response.status_code}") + except Exception as e: + # Non-HTTP error (network, timeout, etc.) - log it + print(f"WARNING: Failed to delete group {group_id}: {type(e).__name__}: {e}") + + @pytest.mark.keycloak @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_keycloak_login_with_credentials( @@ -228,29 +483,16 @@ def test_get_users( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_user_by_username( authenticated_keycloak_api: KeycloakAPI, - test_username: str, + created_user: dict, ) -> None: """Test getting a specific user by username.""" - # First, create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - # Get the user by username - response = authenticated_keycloak_api.get_users(username=test_username) + response = authenticated_keycloak_api.get_users(username=created_user["username"]) assert response.status_code == HTTPStatus.OK, f"Failed to get user: {response.text}" users = response.json() assert len(users) > 0, "User not found" - assert users[0]["username"] == test_username - - # Cleanup: Delete the test user - user_id = users[0]["id"] - authenticated_keycloak_api.delete_user(user_id) + assert users[0]["username"] == created_user["username"] @pytest.mark.keycloak @@ -258,24 +500,10 @@ def test_get_user_by_username( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_update_user( authenticated_keycloak_api: KeycloakAPI, - test_username: str, + created_user: dict, ) -> None: """Test updating a user in Keycloak.""" - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "firstName": "Original", - "lastName": "Name", - "enabled": True, - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the user ID - get_response = authenticated_keycloak_api.get_users(username=test_username) - users = get_response.json() - user_id = users[0]["id"] + user_id = created_user["id"] # Update the user update_data = { @@ -294,31 +522,16 @@ def test_update_user( assert updated_user["firstName"] == "Updated" assert updated_user["lastName"] == "User" - # Cleanup: Delete the test user - authenticated_keycloak_api.delete_user(user_id) - @pytest.mark.keycloak @pytest.mark.keycloak_users @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_user( authenticated_keycloak_api: KeycloakAPI, - test_username: str, + created_user: dict, ) -> None: """Test deleting a user from Keycloak.""" - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the user ID - get_response = authenticated_keycloak_api.get_users(username=test_username) - users = get_response.json() - user_id = users[0]["id"] + user_id = created_user["id"] # Delete the user delete_response = authenticated_keycloak_api.delete_user(user_id) @@ -358,24 +571,11 @@ def test_list_users( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_user_by_id( authenticated_keycloak_api: KeycloakAPI, - test_username: str, + created_user: dict, ) -> None: """Test getting a specific user by ID.""" - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "firstName": "Test", - "lastName": "User", - "enabled": True, - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the user ID - get_response = authenticated_keycloak_api.get_users(username=test_username) - users = get_response.json() - user_id = users[0]["id"] + user_id = created_user["id"] + test_username = created_user["username"] # Get user by ID response = authenticated_keycloak_api.get_user_by_id(user_id) @@ -388,9 +588,6 @@ def test_get_user_by_id( assert user["username"] == test_username assert user["email"] == f"{test_username}@example.com" - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - @pytest.mark.keycloak @pytest.mark.keycloak_users @@ -398,33 +595,14 @@ def test_get_user_by_id( def test_update_user_password( authenticated_keycloak_api: KeycloakAPI, keycloak_api: KeycloakAPI, - test_username: str, + created_user_with_password: dict, ) -> None: """Test updating a user's password and verifying authentication.""" - old_password = "OldPassword123!" + user_id = created_user_with_password["id"] + test_username = created_user_with_password["username"] + old_password = created_user_with_password["password"] new_password = "NewPassword456!" - # Create a test user with initial password - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": old_password, - "temporary": False, - } - ], - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the user ID - get_response = authenticated_keycloak_api.get_users(username=test_username) - users = get_response.json() - user_id = users[0]["id"] - # Update the password update_response = authenticated_keycloak_api.reset_user_password( user_id, new_password, temporary=False @@ -442,9 +620,6 @@ def test_update_user_password( keycloak_api.authenticate(test_username, old_password) assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - @pytest.mark.keycloak @pytest.mark.keycloak_users @@ -452,31 +627,12 @@ def test_update_user_password( def test_enable_disable_user( authenticated_keycloak_api: KeycloakAPI, keycloak_api: KeycloakAPI, - test_username: str, + created_user_with_password: dict, ) -> None: """Test disabling and re-enabling a user account.""" - password = "TestPassword123!" - - # Create an enabled test user with password - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_response = authenticated_keycloak_api.create_user(user_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the user ID - get_response = authenticated_keycloak_api.get_users(username=test_username) - users = get_response.json() - user_id = users[0]["id"] + user_id = created_user_with_password["id"] + test_username = created_user_with_password["username"] + password = created_user_with_password["password"] # Verify user can authenticate while enabled try: @@ -521,9 +677,6 @@ def test_enable_disable_user( token_data = keycloak_api.authenticate(test_username, password) assert "access_token" in token_data - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - @pytest.mark.keycloak @pytest.mark.keycloak_clients @@ -578,18 +731,10 @@ def test_get_clients( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_client_by_client_id( authenticated_keycloak_api: KeycloakAPI, - test_client_id: str, + created_client: dict, ) -> None: """Test getting a specific client by clientId.""" - # First, create a test client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": True, - "protocol": "openid-connect", - } - create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == HTTPStatus.CREATED + test_client_id = created_client["client_id"] # Get the client by clientId response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -601,34 +746,17 @@ def test_get_client_by_client_id( assert len(clients) > 0, "Client not found" assert clients[0]["clientId"] == test_client_id - # Cleanup: Delete the test client - client_internal_id = clients[0]["id"] - authenticated_keycloak_api.delete_client(client_internal_id) - @pytest.mark.keycloak @pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_update_client( authenticated_keycloak_api: KeycloakAPI, - test_client_id: str, + created_client: dict, ) -> None: """Test updating a client in Keycloak.""" - # Create a test client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": True, - "protocol": "openid-connect", - "description": "Original description", - } - create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the client internal ID - get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - clients = get_response.json() - client_internal_id = clients[0]["id"] + client_internal_id = created_client["internal_id"] + test_client_id = created_client["client_id"] # Update the client update_data = { @@ -652,32 +780,16 @@ def test_update_client( assert updated_client["description"] == "Updated description" assert updated_client["publicClient"] is False - # Cleanup: Delete the test client - authenticated_keycloak_api.delete_client(client_internal_id) - @pytest.mark.keycloak @pytest.mark.keycloak_clients @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_client( authenticated_keycloak_api: KeycloakAPI, - test_client_id: str, + created_client: dict, ) -> None: """Test deleting a client from Keycloak.""" - # Create a test client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": True, - "protocol": "openid-connect", - } - create_response = authenticated_keycloak_api.create_client(client_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get the client internal ID - get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) - clients = get_response.json() - client_internal_id = clients[0]["id"] + client_internal_id = created_client["internal_id"] # Delete the client delete_response = authenticated_keycloak_api.delete_client(client_internal_id) @@ -720,7 +832,7 @@ def test_client_secret_regeneration( test_client_id: str, ) -> None: """Test regenerating a client secret.""" - # Create a confidential client + # Create a confidential client with service accounts enabled client_data = { "clientId": test_client_id, "enabled": True, @@ -736,36 +848,37 @@ def test_client_secret_regeneration( clients = get_response.json() client_internal_id = clients[0]["id"] - # Get the initial secret - secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) - assert secret_response.status_code == HTTPStatus.OK - old_secret = secret_response.json()["value"] - - # Regenerate the secret - regen_response = authenticated_keycloak_api.regenerate_client_secret( - client_internal_id - ) - assert regen_response.status_code == HTTPStatus.OK - new_secret = regen_response.json()["value"] - - # Verify the secrets are different - assert old_secret != new_secret - - # Verify old secret no longer works for client credentials flow - with pytest.raises(requests.exceptions.HTTPError) as exc_info: - authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, old_secret - ) - assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED - - # Verify new secret works - token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, new_secret - ) - assert "access_token" in token_data - - # Cleanup - authenticated_keycloak_api.delete_client(client_internal_id) + try: + # Get the initial secret + secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) + assert secret_response.status_code == HTTPStatus.OK + old_secret = secret_response.json()["value"] + + # Regenerate the secret + regen_response = authenticated_keycloak_api.regenerate_client_secret( + client_internal_id + ) + assert regen_response.status_code == HTTPStatus.OK + new_secret = regen_response.json()["value"] + + # Verify the secrets are different + assert old_secret != new_secret + + # Verify old secret no longer works for client credentials flow + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, old_secret + ) + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED + + # Verify new secret works + token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, new_secret + ) + assert "access_token" in token_data + finally: + # Ensure cleanup + authenticated_keycloak_api.delete_client(client_internal_id) @pytest.mark.keycloak @@ -799,52 +912,61 @@ def test_client_oauth2_flow( secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) client_secret = secret_response.json()["value"] - # Test client credentials flow - token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, client_secret - ) - assert "access_token" in token_data - assert "token_type" in token_data - - # Verify token is valid by decoding it - access_token = token_data["access_token"] - token_payload = decode_jwt_token(access_token) - assert "exp" in token_payload # Has expiration - - # Create a test user for password flow - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - user_create_response = authenticated_keycloak_api.create_user(user_data) - assert user_create_response.status_code == HTTPStatus.CREATED - - # Get user ID for cleanup - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - - # Test password flow - password_token_data = authenticated_keycloak_api.oauth2_password_flow( - test_client_id, test_username, password, client_secret - ) - assert "access_token" in password_token_data - - # Verify user token contains username - user_token_payload = decode_jwt_token(password_token_data["access_token"]) - assert "preferred_username" in user_token_payload - assert user_token_payload["preferred_username"] == test_username - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_client(client_internal_id) + user_id = None + try: + # Test client credentials flow + token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, client_secret + ) + assert "access_token" in token_data + assert "token_type" in token_data + + # Verify token is valid by decoding it + access_token = token_data["access_token"] + token_payload = decode_jwt_token(access_token) + assert "exp" in token_payload # Has expiration + + # Create a test user for password flow + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + user_create_response = authenticated_keycloak_api.create_user(user_data) + assert user_create_response.status_code == HTTPStatus.CREATED + + # Get user ID for cleanup + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Test password flow + password_token_data = authenticated_keycloak_api.oauth2_password_flow( + test_client_id, test_username, password, client_secret + ) + assert "access_token" in password_token_data + + # Verify user token contains username + user_token_payload = decode_jwt_token(password_token_data["access_token"]) + assert "preferred_username" in user_token_payload + assert user_token_payload["preferred_username"] == test_username + finally: + # Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except Exception: + pass + try: + authenticated_keycloak_api.delete_client(client_internal_id) + except Exception: + pass @pytest.mark.keycloak @@ -855,7 +977,7 @@ def test_client_with_invalid_credentials( test_client_id: str, ) -> None: """Test OAuth2 flow with invalid client credentials.""" - # Create a confidential client + # Create a confidential client with service accounts enabled client_data = { "clientId": test_client_id, "enabled": True, @@ -871,15 +993,16 @@ def test_client_with_invalid_credentials( clients = get_response.json() client_internal_id = clients[0]["id"] - # Attempt OAuth2 flow with invalid secret - with pytest.raises(requests.exceptions.HTTPError) as exc_info: - authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, "invalid-secret" - ) - assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED - - # Cleanup - authenticated_keycloak_api.delete_client(client_internal_id) + try: + # Attempt OAuth2 flow with invalid secret + with pytest.raises(requests.exceptions.HTTPError) as exc_info: + authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, "invalid-secret" + ) + assert exc_info.value.response.status_code == HTTPStatus.UNAUTHORIZED + finally: + # Ensure cleanup + authenticated_keycloak_api.delete_client(client_internal_id) # Role Tests @@ -936,16 +1059,10 @@ def test_list_realm_roles( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_get_realm_role_by_name( authenticated_keycloak_api: KeycloakAPI, - test_role_name: str, + created_realm_role: dict, ) -> None: """Test getting a specific realm role by name.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role", - } - create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == HTTPStatus.CREATED + test_role_name = created_realm_role["name"] # Get the role by name response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -953,10 +1070,7 @@ def test_get_realm_role_by_name( role = response.json() assert role["name"] == test_role_name - assert role["description"] == "Test role" - - # Cleanup - authenticated_keycloak_api.delete_realm_role(test_role_name) + assert role["description"] == "Test realm role" @pytest.mark.keycloak @@ -964,16 +1078,10 @@ def test_get_realm_role_by_name( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_realm_role( authenticated_keycloak_api: KeycloakAPI, - test_role_name: str, + created_realm_role: dict, ) -> None: """Test deleting a realm role.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role to delete", - } - create_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_response.status_code == HTTPStatus.CREATED + test_role_name = created_realm_role["name"] # Delete the role delete_response = authenticated_keycloak_api.delete_realm_role(test_role_name) @@ -1068,43 +1176,15 @@ def test_list_client_roles( def test_assign_realm_role_to_user( authenticated_keycloak_api: KeycloakAPI, keycloak_api: KeycloakAPI, - test_username: str, - test_role_name: str, + created_user_with_password: dict, + created_realm_role: dict, ) -> None: """Test assigning a realm role to a user.""" - password = "TestPassword123!" - - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role for assignment", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == HTTPStatus.CREATED - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + user_id = created_user_with_password["id"] + test_username = created_user_with_password["username"] + password = created_user_with_password["password"] + test_role_name = created_realm_role["name"] + role = created_realm_role["role"] # Assign role to user assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( @@ -1125,21 +1205,19 @@ def test_assign_realm_role_to_user( if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: assert test_role_name in token_payload["realm_access"]["roles"] - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - @pytest.mark.keycloak @pytest.mark.keycloak_roles @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_assign_client_role_to_user( authenticated_keycloak_api: KeycloakAPI, - test_username: str, + created_user: dict, test_client_id: str, test_role_name: str, ) -> None: """Test assigning a client role to a user.""" + user_id = created_user["id"] + # Create a client client_data = { "clientId": test_client_id, @@ -1156,53 +1234,40 @@ def test_assign_client_role_to_user( ) client_internal_id = get_client_response.json()[0]["id"] - # Create a client role - role_data = { - "name": test_role_name, - "description": "Test client role for assignment", - } - create_role_response = authenticated_keycloak_api.create_client_role( - client_internal_id, role_data - ) - assert create_role_response.status_code == HTTPStatus.CREATED - - # Get the role - role_response = authenticated_keycloak_api.get_client_role_by_name( - client_internal_id, test_role_name - ) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + try: + # Create a client role + role_data = { + "name": test_role_name, + "description": "Test client role for assignment", + } + create_role_response = authenticated_keycloak_api.create_client_role( + client_internal_id, role_data + ) + assert create_role_response.status_code == HTTPStatus.CREATED - # Assign client role to user - assign_response = authenticated_keycloak_api.assign_client_roles_to_user( - user_id, client_internal_id, [role] - ) - assert assign_response.status_code == HTTPStatus.NO_CONTENT + # Get the role + role_response = authenticated_keycloak_api.get_client_role_by_name( + client_internal_id, test_role_name + ) + role = role_response.json() - # Verify user has client role - user_roles_response = authenticated_keycloak_api.get_user_client_roles( - user_id, client_internal_id - ) - assert user_roles_response.status_code == HTTPStatus.OK - user_roles = user_roles_response.json() - role_names = [r["name"] for r in user_roles] - assert test_role_name in role_names + # Assign client role to user + assign_response = authenticated_keycloak_api.assign_client_roles_to_user( + user_id, client_internal_id, [role] + ) + assert assign_response.status_code == HTTPStatus.NO_CONTENT - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_client(client_internal_id) + # Verify user has client role + user_roles_response = authenticated_keycloak_api.get_user_client_roles( + user_id, client_internal_id + ) + assert user_roles_response.status_code == HTTPStatus.OK + user_roles = user_roles_response.json() + role_names = [r["name"] for r in user_roles] + assert test_role_name in role_names + finally: + # Cleanup + authenticated_keycloak_api.delete_client(client_internal_id) @pytest.mark.keycloak @@ -1210,34 +1275,13 @@ def test_assign_client_role_to_user( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_remove_role_from_user( authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_role_name: str, + created_user: dict, + created_realm_role: dict, ) -> None: """Test removing a role from a user.""" - # Create a test role - role_data = { - "name": test_role_name, - "description": "Test role for removal", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == HTTPStatus.CREATED - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + user_id = created_user["id"] + test_role_name = created_realm_role["name"] + role = created_realm_role["role"] # Assign role to user assign_response = authenticated_keycloak_api.assign_realm_roles_to_user( @@ -1257,10 +1301,6 @@ def test_remove_role_from_user( role_names = [r["name"] for r in user_roles] assert test_role_name not in role_names - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - # Group Tests @@ -1315,33 +1355,13 @@ def test_list_groups( @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_add_user_to_group( authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, + created_user: dict, + created_group: dict, ) -> None: """Test adding a user to a group.""" - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == HTTPStatus.CREATED - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + user_id = created_user["id"] + group_id = created_group["id"] + test_group_name = created_group["name"] # Add user to group add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) @@ -1354,43 +1374,19 @@ def test_add_user_to_group( user_group_names = [g["name"] for g in user_groups] assert test_group_name in user_group_names - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - @pytest.mark.keycloak @pytest.mark.keycloak_groups @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_remove_user_from_group( authenticated_keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, + created_user: dict, + created_group: dict, ) -> None: """Test removing a user from a group.""" - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == HTTPStatus.CREATED - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + user_id = created_user["id"] + group_id = created_group["id"] + test_group_name = created_group["name"] # Add user to group add_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) @@ -1408,29 +1404,17 @@ def test_remove_user_from_group( user_group_names = [g["name"] for g in user_groups] assert test_group_name not in user_group_names - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - @pytest.mark.keycloak @pytest.mark.keycloak_groups @pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") def test_delete_group( authenticated_keycloak_api: KeycloakAPI, - test_group_name: str, + created_group: dict, ) -> None: """Test deleting a group.""" - # Create a group - group_data = {"name": test_group_name} - create_response = authenticated_keycloak_api.create_group(group_data) - assert create_response.status_code == HTTPStatus.CREATED - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] + group_id = created_group["id"] + test_group_name = created_group["name"] # Delete the group delete_response = authenticated_keycloak_api.delete_group(group_id) @@ -1449,35 +1433,17 @@ def test_delete_group( def test_group_role_assignment( authenticated_keycloak_api: KeycloakAPI, keycloak_api: KeycloakAPI, - test_username: str, - test_group_name: str, - test_role_name: str, + created_user_with_password: dict, + created_group: dict, + created_realm_role: dict, ) -> None: """Test that user inherits roles from group.""" - password = "TestPassword123!" - - # Create a role - role_data = { - "name": test_role_name, - "description": "Test role for group", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == HTTPStatus.CREATED - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) - role = role_response.json() - - # Create a group - group_data = {"name": test_group_name} - create_group_response = authenticated_keycloak_api.create_group(group_data) - assert create_group_response.status_code == HTTPStatus.CREATED - - # Get group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - group = [g for g in groups if g["name"] == test_group_name][0] - group_id = group["id"] + user_id = created_user_with_password["id"] + test_username = created_user_with_password["username"] + password = created_user_with_password["password"] + group_id = created_group["id"] + test_role_name = created_realm_role["name"] + role = created_realm_role["role"] # Assign role to group assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( @@ -1485,26 +1451,6 @@ def test_group_role_assignment( ) assert assign_role_response.status_code == HTTPStatus.NO_CONTENT - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED - - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] - # Add user to group add_user_response = authenticated_keycloak_api.add_user_to_group(user_id, group_id) assert add_user_response.status_code == HTTPStatus.NO_CONTENT @@ -1515,11 +1461,6 @@ def test_group_role_assignment( if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: assert test_role_name in token_payload["realm_access"]["roles"] - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group_id) - authenticated_keycloak_api.delete_realm_role(test_role_name) - @pytest.mark.keycloak @pytest.mark.keycloak_groups @@ -1535,86 +1476,104 @@ def test_nested_groups( role_name = f"parent-role-{uuid.uuid4().hex[:8]}" password = "TestPassword123!" - # Create a role - role_data = { - "name": role_name, - "description": "Test role for parent group", - } - create_role_response = authenticated_keycloak_api.create_realm_role(role_data) - assert create_role_response.status_code == HTTPStatus.CREATED - - # Get the role - role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) - role = role_response.json() - - # Create parent group - parent_group_data = {"name": parent_group_name} - create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) - assert create_parent_response.status_code == HTTPStatus.CREATED - - # Get parent group ID - groups_response = authenticated_keycloak_api.get_groups() - groups = groups_response.json() - parent_group = [g for g in groups if g["name"] == parent_group_name][0] - parent_group_id = parent_group["id"] - - # Assign role to parent group - assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( - parent_group_id, [role] - ) - assert assign_role_response.status_code == HTTPStatus.NO_CONTENT - - # Create child group under parent - child_group_data = {"name": child_group_name} - create_child_response = authenticated_keycloak_api.create_subgroup( - parent_group_id, child_group_data - ) - assert create_child_response.status_code == HTTPStatus.CREATED - - # Get child group ID - parent_details_response = authenticated_keycloak_api.get_group_by_id( - parent_group_id - ) - parent_details = parent_details_response.json() - child_group = parent_details["subGroups"][0] - child_group_id = child_group["id"] - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - create_user_response = authenticated_keycloak_api.create_user(user_data) - assert create_user_response.status_code == HTTPStatus.CREATED + user_id = None + parent_group_id = None + role_created = False - # Get user ID - user_get_response = authenticated_keycloak_api.get_users(username=test_username) - user_id = user_get_response.json()[0]["id"] + try: + # Create a role + role_data = { + "name": role_name, + "description": "Test role for parent group", + } + create_role_response = authenticated_keycloak_api.create_realm_role(role_data) + assert create_role_response.status_code == HTTPStatus.CREATED + role_created = True + + # Get the role + role_response = authenticated_keycloak_api.get_realm_role_by_name(role_name) + role = role_response.json() + + # Create parent group + parent_group_data = {"name": parent_group_name} + create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) + assert create_parent_response.status_code == HTTPStatus.CREATED + + # Get parent group ID + groups_response = authenticated_keycloak_api.get_groups() + groups = groups_response.json() + parent_group = [g for g in groups if g["name"] == parent_group_name][0] + parent_group_id = parent_group["id"] + + # Assign role to parent group + assign_role_response = authenticated_keycloak_api.assign_realm_roles_to_group( + parent_group_id, [role] + ) + assert assign_role_response.status_code == HTTPStatus.NO_CONTENT - # Add user to child group - add_user_response = authenticated_keycloak_api.add_user_to_group( - user_id, child_group_id - ) - assert add_user_response.status_code == HTTPStatus.NO_CONTENT + # Create child group under parent + child_group_data = {"name": child_group_name} + create_child_response = authenticated_keycloak_api.create_subgroup( + parent_group_id, child_group_data + ) + assert create_child_response.status_code == HTTPStatus.CREATED - # Verify user inherits role from parent group - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert role_name in token_payload["realm_access"]["roles"] + # Get child group ID + parent_details_response = authenticated_keycloak_api.get_group_by_id( + parent_group_id + ) + parent_details = parent_details_response.json() + child_group = parent_details["subGroups"][0] + child_group_id = child_group["id"] + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + create_user_response = authenticated_keycloak_api.create_user(user_data) + assert create_user_response.status_code == HTTPStatus.CREATED + + # Get user ID + user_get_response = authenticated_keycloak_api.get_users(username=test_username) + user_id = user_get_response.json()[0]["id"] + + # Add user to child group + add_user_response = authenticated_keycloak_api.add_user_to_group( + user_id, child_group_id + ) + assert add_user_response.status_code == HTTPStatus.NO_CONTENT - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too - authenticated_keycloak_api.delete_realm_role(role_name) + # Verify user inherits role from parent group + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role_name in token_payload["realm_access"]["roles"] + finally: + # Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except Exception: + pass + if parent_group_id: + try: + authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too + except Exception: + pass + if role_created: + try: + authenticated_keycloak_api.delete_realm_role(role_name) + except Exception: + pass @pytest.mark.keycloak @@ -1632,76 +1591,105 @@ def test_group_scope_propagation( role2_name = f"test-role-2-{uuid.uuid4().hex[:8]}" password = "TestPassword123!" - # Create two roles - role1_data = {"name": role1_name, "description": "Role 1"} - role2_data = {"name": role2_name, "description": "Role 2"} - authenticated_keycloak_api.create_realm_role(role1_data) - authenticated_keycloak_api.create_realm_role(role2_data) + user_id = None + group1_id = None + group2_id = None + role1_created = False + role2_created = False - # Get roles - role1 = authenticated_keycloak_api.get_realm_role_by_name(role1_name).json() - role2 = authenticated_keycloak_api.get_realm_role_by_name(role2_name).json() - - # Create two groups - authenticated_keycloak_api.create_group({"name": group1_name}) - authenticated_keycloak_api.create_group({"name": group2_name}) - - # Get group IDs - groups = authenticated_keycloak_api.get_groups().json() - group1 = [g for g in groups if g["name"] == group1_name][0] - group2 = [g for g in groups if g["name"] == group2_name][0] - group1_id = group1["id"] - group2_id = group2["id"] - - # Assign roles to groups - authenticated_keycloak_api.assign_realm_roles_to_group(group1_id, [role1]) - authenticated_keycloak_api.assign_realm_roles_to_group(group2_id, [role2]) - - # Create a user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": password, - "temporary": False, - } - ], - } - authenticated_keycloak_api.create_user(user_data) - - # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] - - # Add user to both groups - authenticated_keycloak_api.add_user_to_group(user_id, group1_id) - authenticated_keycloak_api.add_user_to_group(user_id, group2_id) - - # Verify user has roles from both groups - token_data = keycloak_api.authenticate(test_username, password) - token_payload = decode_jwt_token(token_data["access_token"]) - - # Check user is in both groups - user_groups = authenticated_keycloak_api.get_user_groups(user_id).json() - user_group_names = [g["name"] for g in user_groups] - assert group1_name in user_group_names - assert group2_name in user_group_names - - # Check token contains both roles - if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: - assert role1_name in token_payload["realm_access"]["roles"] - assert role2_name in token_payload["realm_access"]["roles"] - - # Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_group(group1_id) - authenticated_keycloak_api.delete_group(group2_id) - authenticated_keycloak_api.delete_realm_role(role1_name) - authenticated_keycloak_api.delete_realm_role(role2_name) + try: + # Create two roles + role1_data = {"name": role1_name, "description": "Role 1"} + role2_data = {"name": role2_name, "description": "Role 2"} + authenticated_keycloak_api.create_realm_role(role1_data) + role1_created = True + authenticated_keycloak_api.create_realm_role(role2_data) + role2_created = True + + # Get roles + role1 = authenticated_keycloak_api.get_realm_role_by_name(role1_name).json() + role2 = authenticated_keycloak_api.get_realm_role_by_name(role2_name).json() + + # Create two groups + authenticated_keycloak_api.create_group({"name": group1_name}) + authenticated_keycloak_api.create_group({"name": group2_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + group1 = [g for g in groups if g["name"] == group1_name][0] + group2 = [g for g in groups if g["name"] == group2_name][0] + group1_id = group1["id"] + group2_id = group2["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(group1_id, [role1]) + authenticated_keycloak_api.assign_realm_roles_to_group(group2_id, [role2]) + + # Create a user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] + + # Add user to both groups + authenticated_keycloak_api.add_user_to_group(user_id, group1_id) + authenticated_keycloak_api.add_user_to_group(user_id, group2_id) + + # Verify user has roles from both groups + token_data = keycloak_api.authenticate(test_username, password) + token_payload = decode_jwt_token(token_data["access_token"]) + + # Check user is in both groups + user_groups = authenticated_keycloak_api.get_user_groups(user_id).json() + user_group_names = [g["name"] for g in user_groups] + assert group1_name in user_group_names + assert group2_name in user_group_names + + # Check token contains both roles + if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: + assert role1_name in token_payload["realm_access"]["roles"] + assert role2_name in token_payload["realm_access"]["roles"] + finally: + # Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except Exception: + pass + if group1_id: + try: + authenticated_keycloak_api.delete_group(group1_id) + except Exception: + pass + if group2_id: + try: + authenticated_keycloak_api.delete_group(group2_id) + except Exception: + pass + if role1_created: + try: + authenticated_keycloak_api.delete_realm_role(role1_name) + except Exception: + pass + if role2_created: + try: + authenticated_keycloak_api.delete_realm_role(role2_name) + except Exception: + pass # Integration Tests @@ -1732,138 +1720,172 @@ def test_admin_user_workflow( admin_role_name = f"workflow-admin-role-{uuid.uuid4().hex[:8]}" user_role_name = f"workflow-user-role-{uuid.uuid4().hex[:8]}" - # Step 1: Admin Setup - # Create roles - admin_role_data = {"name": admin_role_name, "description": "Admin role"} - user_role_data = {"name": user_role_name, "description": "User role"} - authenticated_keycloak_api.create_realm_role(admin_role_data) - authenticated_keycloak_api.create_realm_role(user_role_data) - - # Get roles - admin_role = authenticated_keycloak_api.get_realm_role_by_name( - admin_role_name - ).json() - user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() - - # Create groups - authenticated_keycloak_api.create_group({"name": admin_group_name}) - authenticated_keycloak_api.create_group({"name": user_group_name}) - - # Get group IDs - groups = authenticated_keycloak_api.get_groups().json() - admin_group = [g for g in groups if g["name"] == admin_group_name][0] - user_group = [g for g in groups if g["name"] == user_group_name][0] - admin_group_id = admin_group["id"] - user_group_id = user_group["id"] - - # Assign roles to groups - authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) - authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) - - # Create OAuth2 client - client_data = { - "clientId": test_client_id, - "enabled": True, - "publicClient": False, - "protocol": "openid-connect", - "serviceAccountsEnabled": True, - "directAccessGrantsEnabled": True, - "standardFlowEnabled": False, - "implicitFlowEnabled": False, - } - authenticated_keycloak_api.create_client(client_data) - - # Get client internal ID and secret - client_internal_id = authenticated_keycloak_api.get_clients( - client_id=test_client_id - ).json()[0]["id"] - client_secret = authenticated_keycloak_api.get_client_secret( - client_internal_id - ).json()["value"] - - # Create test user - user_data = { - "username": test_username, - "email": f"{test_username}@example.com", - "firstName": "Workflow", - "lastName": "User", - "enabled": True, - "credentials": [ - { - "type": "password", - "value": test_password, - "temporary": False, - } - ], - } - authenticated_keycloak_api.create_user(user_data) - - # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] - - # Assign user to groups - authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) - authenticated_keycloak_api.add_user_to_group(user_id, user_group_id) - - # Step 2: User Authentication - token_data = keycloak_api.authenticate(test_username, test_password) - assert "access_token" in token_data - assert "refresh_token" in token_data - - # Step 3: Group Association Verification - user_details = authenticated_keycloak_api.get_user_by_id(user_id).json() - assert user_details["username"] == test_username + user_id = None + client_internal_id = None + admin_group_id = None + user_group_id = None + admin_role_created = False + user_role_created = False - # Verify user is member of expected groups - user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) - user_groups = user_groups_response.json() - user_group_names = [g["name"] for g in user_groups] - assert admin_group_name in user_group_names - assert user_group_name in user_group_names - - # Parse user's access token - user_token_payload = decode_jwt_token(token_data["access_token"]) - assert "preferred_username" in user_token_payload - assert user_token_payload["preferred_username"] == test_username - - # Step 4: OAuth Client Login - # Authenticate as test user through OAuth2 flow - oauth_token_data = authenticated_keycloak_api.oauth2_password_flow( - test_client_id, test_username, test_password, client_secret - ) - assert "access_token" in oauth_token_data + try: + # Step 1: Admin Setup + # Create roles + admin_role_data = {"name": admin_role_name, "description": "Admin role"} + user_role_data = {"name": user_role_name, "description": "User role"} + authenticated_keycloak_api.create_realm_role(admin_role_data) + admin_role_created = True + authenticated_keycloak_api.create_realm_role(user_role_data) + user_role_created = True + + # Get roles + admin_role = authenticated_keycloak_api.get_realm_role_by_name( + admin_role_name + ).json() + user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() + + # Create groups + authenticated_keycloak_api.create_group({"name": admin_group_name}) + authenticated_keycloak_api.create_group({"name": user_group_name}) + + # Get group IDs + groups = authenticated_keycloak_api.get_groups().json() + admin_group = [g for g in groups if g["name"] == admin_group_name][0] + user_group = [g for g in groups if g["name"] == user_group_name][0] + admin_group_id = admin_group["id"] + user_group_id = user_group["id"] + + # Assign roles to groups + authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) + authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) + + # Create OAuth2 client + client_data = { + "clientId": test_client_id, + "enabled": True, + "publicClient": False, + "protocol": "openid-connect", + "serviceAccountsEnabled": True, + "directAccessGrantsEnabled": True, + "standardFlowEnabled": False, + "implicitFlowEnabled": False, + } + authenticated_keycloak_api.create_client(client_data) + + # Get client internal ID and secret + client_internal_id = authenticated_keycloak_api.get_clients( + client_id=test_client_id + ).json()[0]["id"] + client_secret = authenticated_keycloak_api.get_client_secret( + client_internal_id + ).json()["value"] + + # Create test user + user_data = { + "username": test_username, + "email": f"{test_username}@example.com", + "firstName": "Workflow", + "lastName": "User", + "enabled": True, + "credentials": [ + { + "type": "password", + "value": test_password, + "temporary": False, + } + ], + } + authenticated_keycloak_api.create_user(user_data) + + # Get user ID + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ + "id" + ] + + # Assign user to groups + authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) + authenticated_keycloak_api.add_user_to_group(user_id, user_group_id) + + # Step 2: User Authentication + token_data = keycloak_api.authenticate(test_username, test_password) + assert "access_token" in token_data + assert "refresh_token" in token_data + + # Step 3: Group Association Verification + user_details = authenticated_keycloak_api.get_user_by_id(user_id).json() + assert user_details["username"] == test_username + + # Verify user is member of expected groups + user_groups_response = authenticated_keycloak_api.get_user_groups(user_id) + user_groups = user_groups_response.json() + user_group_names = [g["name"] for g in user_groups] + assert admin_group_name in user_group_names + assert user_group_name in user_group_names + + # Parse user's access token + user_token_payload = decode_jwt_token(token_data["access_token"]) + assert "preferred_username" in user_token_payload + assert user_token_payload["preferred_username"] == test_username + + # Step 4: OAuth Client Login + # Authenticate as test user through OAuth2 flow + oauth_token_data = authenticated_keycloak_api.oauth2_password_flow( + test_client_id, test_username, test_password, client_secret + ) + assert "access_token" in oauth_token_data - # Also test client credentials flow - client_token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( - test_client_id, client_secret - ) - assert "access_token" in client_token_data - - # Step 5: Scope Verification - oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) - - # Verify token contains expected group memberships and roles - if ( - "realm_access" in oauth_token_payload - and "roles" in oauth_token_payload["realm_access"] - ): - token_roles = oauth_token_payload["realm_access"]["roles"] - assert ( - admin_role_name in token_roles - ), f"Admin role not found in token roles: {token_roles}" - assert ( - user_role_name in token_roles - ), f"User role not found in token roles: {token_roles}" - - # Verify user identity in token - assert oauth_token_payload["preferred_username"] == test_username - - # Step 6: Cleanup - authenticated_keycloak_api.delete_user(user_id) - authenticated_keycloak_api.delete_client(client_internal_id) - authenticated_keycloak_api.delete_group(admin_group_id) - authenticated_keycloak_api.delete_group(user_group_id) - authenticated_keycloak_api.delete_realm_role(admin_role_name) - authenticated_keycloak_api.delete_realm_role(user_role_name) + # Also test client credentials flow + client_token_data = authenticated_keycloak_api.oauth2_client_credentials_flow( + test_client_id, client_secret + ) + assert "access_token" in client_token_data + + # Step 5: Scope Verification + oauth_token_payload = decode_jwt_token(oauth_token_data["access_token"]) + + # Verify token contains expected group memberships and roles + if ( + "realm_access" in oauth_token_payload + and "roles" in oauth_token_payload["realm_access"] + ): + token_roles = oauth_token_payload["realm_access"]["roles"] + assert ( + admin_role_name in token_roles + ), f"Admin role not found in token roles: {token_roles}" + assert ( + user_role_name in token_roles + ), f"User role not found in token roles: {token_roles}" + + # Verify user identity in token + assert oauth_token_payload["preferred_username"] == test_username + finally: + # Step 6: Cleanup + if user_id: + try: + authenticated_keycloak_api.delete_user(user_id) + except Exception: + pass + if client_internal_id: + try: + authenticated_keycloak_api.delete_client(client_internal_id) + except Exception: + pass + if admin_group_id: + try: + authenticated_keycloak_api.delete_group(admin_group_id) + except Exception: + pass + if user_group_id: + try: + authenticated_keycloak_api.delete_group(user_group_id) + except Exception: + pass + if admin_role_created: + try: + authenticated_keycloak_api.delete_realm_role(admin_role_name) + except Exception: + pass + if user_role_created: + try: + authenticated_keycloak_api.delete_realm_role(user_role_name) + except Exception: + pass From 8da016a043f5e384e2f7d080b77a854f09009c06 Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Fri, 31 Oct 2025 14:18:23 -0600 Subject: [PATCH 20/22] add deletion verification to api utility code --- tests/tests_deployment/keycloak_api_utils.py | 87 +++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 3ff19fb63a..603fd15ba5 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -346,7 +346,7 @@ def update_user(self, user_id: str, user_data: dict) -> requests.Response: ) def delete_user(self, user_id: str) -> requests.Response: - """Delete a user from Keycloak. + """Delete a user from Keycloak and verify deletion. Parameters ---------- @@ -357,8 +357,21 @@ def delete_user(self, user_id: str) -> requests.Response: ------- requests.Response Response from the delete request + + Raises + ------ + RuntimeError + If the user still exists after deletion """ - return self._make_admin_request(f"users/{user_id}", method="DELETE") + response = self._make_admin_request(f"users/{user_id}", method="DELETE") + + # Verify deletion if the delete request was successful + if response.status_code == HTTPStatus.NO_CONTENT: + verify_response = self._make_admin_request(f"users/{user_id}") + if verify_response.status_code != HTTPStatus.NOT_FOUND: + raise RuntimeError(f"User {user_id} still exists after deletion") + + return response def create_client(self, client_data: dict) -> requests.Response: """Create a new client in Keycloak. @@ -432,7 +445,7 @@ def update_client( ) def delete_client(self, client_internal_id: str) -> requests.Response: - """Delete a client from Keycloak. + """Delete a client from Keycloak and verify deletion. Parameters ---------- @@ -443,10 +456,21 @@ def delete_client(self, client_internal_id: str) -> requests.Response: ------- requests.Response Response from the delete request + + Raises + ------ + RuntimeError + If the client still exists after deletion """ - return self._make_admin_request( - f"clients/{client_internal_id}", method="DELETE" - ) + response = self._make_admin_request(f"clients/{client_internal_id}", method="DELETE") + + # Verify deletion if the delete request was successful + if response.status_code == HTTPStatus.NO_CONTENT: + verify_response = self._make_admin_request(f"clients/{client_internal_id}") + if verify_response.status_code != HTTPStatus.NOT_FOUND: + raise RuntimeError(f"Client {client_internal_id} still exists after deletion") + + return response def reset_user_password( self, user_id: str, password: str, temporary: bool = False @@ -546,7 +570,7 @@ def get_realm_role_by_name(self, role_name: str) -> requests.Response: return self._make_admin_request(f"roles/{role_name}") def delete_realm_role(self, role_name: str) -> requests.Response: - """Delete a realm role. + """Delete a realm role and verify deletion. Parameters ---------- @@ -557,8 +581,21 @@ def delete_realm_role(self, role_name: str) -> requests.Response: ------- requests.Response Response from the delete request + + Raises + ------ + RuntimeError + If the role still exists after deletion """ - return self._make_admin_request(f"roles/{role_name}", method="DELETE") + response = self._make_admin_request(f"roles/{role_name}", method="DELETE") + + # Verify deletion if the delete request was successful + if response.status_code == HTTPStatus.NO_CONTENT: + verify_response = self._make_admin_request(f"roles/{role_name}") + if verify_response.status_code != HTTPStatus.NOT_FOUND: + raise RuntimeError(f"Realm role {role_name} still exists after deletion") + + return response def create_client_role(self, client_id: str, role_data: dict) -> requests.Response: """Create a new client role. @@ -614,7 +651,7 @@ def get_client_role_by_name( return self._make_admin_request(f"clients/{client_id}/roles/{role_name}") def delete_client_role(self, client_id: str, role_name: str) -> requests.Response: - """Delete a client role. + """Delete a client role and verify deletion. Parameters ---------- @@ -627,11 +664,24 @@ def delete_client_role(self, client_id: str, role_name: str) -> requests.Respons ------- requests.Response Response from the delete request + + Raises + ------ + RuntimeError + If the role still exists after deletion """ - return self._make_admin_request( + response = self._make_admin_request( f"clients/{client_id}/roles/{role_name}", method="DELETE" ) + # Verify deletion if the delete request was successful + if response.status_code == HTTPStatus.NO_CONTENT: + verify_response = self._make_admin_request(f"clients/{client_id}/roles/{role_name}") + if verify_response.status_code != HTTPStatus.NOT_FOUND: + raise RuntimeError(f"Client role {role_name} still exists after deletion") + + return response + def assign_realm_roles_to_user( self, user_id: str, roles: list ) -> requests.Response: @@ -800,7 +850,7 @@ def get_group_by_id(self, group_id: str) -> requests.Response: return self._make_admin_request(f"groups/{group_id}") def delete_group(self, group_id: str) -> requests.Response: - """Delete a group. + """Delete a group and verify deletion. Parameters ---------- @@ -811,8 +861,21 @@ def delete_group(self, group_id: str) -> requests.Response: ------- requests.Response Response from the delete request + + Raises + ------ + RuntimeError + If the group still exists after deletion """ - return self._make_admin_request(f"groups/{group_id}", method="DELETE") + response = self._make_admin_request(f"groups/{group_id}", method="DELETE") + + # Verify deletion if the delete request was successful + if response.status_code == HTTPStatus.NO_CONTENT: + verify_response = self._make_admin_request(f"groups/{group_id}") + if verify_response.status_code != HTTPStatus.NOT_FOUND: + raise RuntimeError(f"Group {group_id} still exists after deletion") + + return response def add_user_to_group(self, user_id: str, group_id: str) -> requests.Response: """Add a user to a group. From 08897843469dd155a51fcebe8edab86362c4becb Mon Sep 17 00:00:00 2001 From: Tyler Potts Date: Fri, 31 Oct 2025 14:05:42 -0600 Subject: [PATCH 21/22] better logging for manually cleaned up resources --- tests/tests_deployment/test_keycloak_api.py | 62 ++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 4254a8d3b3..505e0d49e8 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -1558,22 +1558,22 @@ def test_nested_groups( if "realm_access" in token_payload and "roles" in token_payload["realm_access"]: assert role_name in token_payload["realm_access"]["roles"] finally: - # Cleanup + # Cleanup - log failures but continue if user_id: try: authenticated_keycloak_api.delete_user(user_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") if parent_group_id: try: authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup group {parent_group_id}: {type(e).__name__}: {e}") if role_created: try: authenticated_keycloak_api.delete_realm_role(role_name) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup role {role_name}: {type(e).__name__}: {e}") @pytest.mark.keycloak @@ -1664,32 +1664,32 @@ def test_group_scope_propagation( assert role1_name in token_payload["realm_access"]["roles"] assert role2_name in token_payload["realm_access"]["roles"] finally: - # Cleanup + # Cleanup - log failures but continue if user_id: try: authenticated_keycloak_api.delete_user(user_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") if group1_id: try: authenticated_keycloak_api.delete_group(group1_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup group {group1_id}: {type(e).__name__}: {e}") if group2_id: try: authenticated_keycloak_api.delete_group(group2_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup group {group2_id}: {type(e).__name__}: {e}") if role1_created: try: authenticated_keycloak_api.delete_realm_role(role1_name) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup role {role1_name}: {type(e).__name__}: {e}") if role2_created: try: authenticated_keycloak_api.delete_realm_role(role2_name) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup role {role2_name}: {type(e).__name__}: {e}") # Integration Tests @@ -1858,34 +1858,34 @@ def test_admin_user_workflow( # Verify user identity in token assert oauth_token_payload["preferred_username"] == test_username finally: - # Step 6: Cleanup + # Step 6: Cleanup - log failures but continue if user_id: try: authenticated_keycloak_api.delete_user(user_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") if client_internal_id: try: authenticated_keycloak_api.delete_client(client_internal_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup client {client_internal_id}: {type(e).__name__}: {e}") if admin_group_id: try: authenticated_keycloak_api.delete_group(admin_group_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup group {admin_group_id}: {type(e).__name__}: {e}") if user_group_id: try: authenticated_keycloak_api.delete_group(user_group_id) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup group {user_group_id}: {type(e).__name__}: {e}") if admin_role_created: try: authenticated_keycloak_api.delete_realm_role(admin_role_name) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup role {admin_role_name}: {type(e).__name__}: {e}") if user_role_created: try: authenticated_keycloak_api.delete_realm_role(user_role_name) - except Exception: - pass + except Exception as e: + print(f"WARNING: Failed to cleanup role {user_role_name}: {type(e).__name__}: {e}") From 0a32df712dc7410647d87e5cb2d13a6760f00108 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:19:14 +0000 Subject: [PATCH 22/22] [pre-commit.ci] Apply automatic pre-commit fixes --- tests/tests_deployment/keycloak_api_utils.py | 20 ++- tests/tests_deployment/test_keycloak_api.py | 145 ++++++++++++++----- 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/tests/tests_deployment/keycloak_api_utils.py b/tests/tests_deployment/keycloak_api_utils.py index 603fd15ba5..585058dfad 100644 --- a/tests/tests_deployment/keycloak_api_utils.py +++ b/tests/tests_deployment/keycloak_api_utils.py @@ -462,13 +462,17 @@ def delete_client(self, client_internal_id: str) -> requests.Response: RuntimeError If the client still exists after deletion """ - response = self._make_admin_request(f"clients/{client_internal_id}", method="DELETE") + response = self._make_admin_request( + f"clients/{client_internal_id}", method="DELETE" + ) # Verify deletion if the delete request was successful if response.status_code == HTTPStatus.NO_CONTENT: verify_response = self._make_admin_request(f"clients/{client_internal_id}") if verify_response.status_code != HTTPStatus.NOT_FOUND: - raise RuntimeError(f"Client {client_internal_id} still exists after deletion") + raise RuntimeError( + f"Client {client_internal_id} still exists after deletion" + ) return response @@ -593,7 +597,9 @@ def delete_realm_role(self, role_name: str) -> requests.Response: if response.status_code == HTTPStatus.NO_CONTENT: verify_response = self._make_admin_request(f"roles/{role_name}") if verify_response.status_code != HTTPStatus.NOT_FOUND: - raise RuntimeError(f"Realm role {role_name} still exists after deletion") + raise RuntimeError( + f"Realm role {role_name} still exists after deletion" + ) return response @@ -676,9 +682,13 @@ def delete_client_role(self, client_id: str, role_name: str) -> requests.Respons # Verify deletion if the delete request was successful if response.status_code == HTTPStatus.NO_CONTENT: - verify_response = self._make_admin_request(f"clients/{client_id}/roles/{role_name}") + verify_response = self._make_admin_request( + f"clients/{client_id}/roles/{role_name}" + ) if verify_response.status_code != HTTPStatus.NOT_FOUND: - raise RuntimeError(f"Client role {role_name} still exists after deletion") + raise RuntimeError( + f"Client role {role_name} still exists after deletion" + ) return response diff --git a/tests/tests_deployment/test_keycloak_api.py b/tests/tests_deployment/test_keycloak_api.py index 505e0d49e8..1d5e969e78 100644 --- a/tests/tests_deployment/test_keycloak_api.py +++ b/tests/tests_deployment/test_keycloak_api.py @@ -138,6 +138,7 @@ def test_group_name() -> str: # Resource cleanup fixtures + @pytest.fixture def created_user( authenticated_keycloak_api: KeycloakAPI, @@ -157,7 +158,9 @@ def created_user( } response = authenticated_keycloak_api.create_user(user_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create user: {response.text}" # Get user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -182,7 +185,9 @@ def created_user( pass else: # Unexpected HTTP error - log it - print(f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}") + print( + f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}" + ) except Exception as e: # Non-HTTP error (network, timeout, etc.) - log it print(f"WARNING: Failed to delete user {user_id}: {type(e).__name__}: {e}") @@ -215,7 +220,9 @@ def created_user_with_password( } response = authenticated_keycloak_api.create_user(user_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create user: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create user: {response.text}" # Get user ID get_response = authenticated_keycloak_api.get_users(username=test_username) @@ -241,7 +248,9 @@ def created_user_with_password( pass else: # Unexpected HTTP error - log it - print(f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}") + print( + f"WARNING: Failed to delete user {user_id}: HTTP {e.response.status_code}" + ) except Exception as e: # Non-HTTP error (network, timeout, etc.) - log it print(f"WARNING: Failed to delete user {user_id}: {type(e).__name__}: {e}") @@ -267,7 +276,9 @@ def created_client( } response = authenticated_keycloak_api.create_client(client_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create client: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create client: {response.text}" # Get client internal ID get_response = authenticated_keycloak_api.get_clients(client_id=test_client_id) @@ -292,10 +303,14 @@ def created_client( pass else: # Unexpected HTTP error - log it - print(f"WARNING: Failed to delete client {client_internal_id}: HTTP {e.response.status_code}") + print( + f"WARNING: Failed to delete client {client_internal_id}: HTTP {e.response.status_code}" + ) except Exception as e: # Non-HTTP error (network, timeout, etc.) - log it - print(f"WARNING: Failed to delete client {client_internal_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to delete client {client_internal_id}: {type(e).__name__}: {e}" + ) @pytest.fixture @@ -316,7 +331,9 @@ def created_realm_role( } response = authenticated_keycloak_api.create_realm_role(role_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create realm role: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create realm role: {response.text}" # Get the full role object get_response = authenticated_keycloak_api.get_realm_role_by_name(test_role_name) @@ -338,10 +355,14 @@ def created_realm_role( pass else: # Unexpected HTTP error - log it - print(f"WARNING: Failed to delete realm role {test_role_name}: HTTP {e.response.status_code}") + print( + f"WARNING: Failed to delete realm role {test_role_name}: HTTP {e.response.status_code}" + ) except Exception as e: # Non-HTTP error (network, timeout, etc.) - log it - print(f"WARNING: Failed to delete realm role {test_role_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to delete realm role {test_role_name}: {type(e).__name__}: {e}" + ) @pytest.fixture @@ -359,7 +380,9 @@ def created_group( group_data = {"name": test_group_name} response = authenticated_keycloak_api.create_group(group_data) - assert response.status_code == HTTPStatus.CREATED, f"Failed to create group: {response.text}" + assert ( + response.status_code == HTTPStatus.CREATED + ), f"Failed to create group: {response.text}" # Get group ID groups_response = authenticated_keycloak_api.get_groups() @@ -385,10 +408,14 @@ def created_group( pass else: # Unexpected HTTP error - log it - print(f"WARNING: Failed to delete group {group_id}: HTTP {e.response.status_code}") + print( + f"WARNING: Failed to delete group {group_id}: HTTP {e.response.status_code}" + ) except Exception as e: # Non-HTTP error (network, timeout, etc.) - log it - print(f"WARNING: Failed to delete group {group_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to delete group {group_id}: {type(e).__name__}: {e}" + ) @pytest.mark.keycloak @@ -850,7 +877,9 @@ def test_client_secret_regeneration( try: # Get the initial secret - secret_response = authenticated_keycloak_api.get_client_secret(client_internal_id) + secret_response = authenticated_keycloak_api.get_client_secret( + client_internal_id + ) assert secret_response.status_code == HTTPStatus.OK old_secret = secret_response.json()["value"] @@ -1496,7 +1525,9 @@ def test_nested_groups( # Create parent group parent_group_data = {"name": parent_group_name} - create_parent_response = authenticated_keycloak_api.create_group(parent_group_data) + create_parent_response = authenticated_keycloak_api.create_group( + parent_group_data + ) assert create_parent_response.status_code == HTTPStatus.CREATED # Get parent group ID @@ -1563,17 +1594,25 @@ def test_nested_groups( try: authenticated_keycloak_api.delete_user(user_id) except Exception as e: - print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}" + ) if parent_group_id: try: - authenticated_keycloak_api.delete_group(parent_group_id) # Deletes children too + authenticated_keycloak_api.delete_group( + parent_group_id + ) # Deletes children too except Exception as e: - print(f"WARNING: Failed to cleanup group {parent_group_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup group {parent_group_id}: {type(e).__name__}: {e}" + ) if role_created: try: authenticated_keycloak_api.delete_realm_role(role_name) except Exception as e: - print(f"WARNING: Failed to cleanup role {role_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup role {role_name}: {type(e).__name__}: {e}" + ) @pytest.mark.keycloak @@ -1641,9 +1680,9 @@ def test_group_scope_propagation( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[ + 0 + ]["id"] # Add user to both groups authenticated_keycloak_api.add_user_to_group(user_id, group1_id) @@ -1669,27 +1708,37 @@ def test_group_scope_propagation( try: authenticated_keycloak_api.delete_user(user_id) except Exception as e: - print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}" + ) if group1_id: try: authenticated_keycloak_api.delete_group(group1_id) except Exception as e: - print(f"WARNING: Failed to cleanup group {group1_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup group {group1_id}: {type(e).__name__}: {e}" + ) if group2_id: try: authenticated_keycloak_api.delete_group(group2_id) except Exception as e: - print(f"WARNING: Failed to cleanup group {group2_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup group {group2_id}: {type(e).__name__}: {e}" + ) if role1_created: try: authenticated_keycloak_api.delete_realm_role(role1_name) except Exception as e: - print(f"WARNING: Failed to cleanup role {role1_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup role {role1_name}: {type(e).__name__}: {e}" + ) if role2_created: try: authenticated_keycloak_api.delete_realm_role(role2_name) except Exception as e: - print(f"WARNING: Failed to cleanup role {role2_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup role {role2_name}: {type(e).__name__}: {e}" + ) # Integration Tests @@ -1741,7 +1790,9 @@ def test_admin_user_workflow( admin_role = authenticated_keycloak_api.get_realm_role_by_name( admin_role_name ).json() - user_role = authenticated_keycloak_api.get_realm_role_by_name(user_role_name).json() + user_role = authenticated_keycloak_api.get_realm_role_by_name( + user_role_name + ).json() # Create groups authenticated_keycloak_api.create_group({"name": admin_group_name}) @@ -1755,8 +1806,12 @@ def test_admin_user_workflow( user_group_id = user_group["id"] # Assign roles to groups - authenticated_keycloak_api.assign_realm_roles_to_group(admin_group_id, [admin_role]) - authenticated_keycloak_api.assign_realm_roles_to_group(user_group_id, [user_role]) + authenticated_keycloak_api.assign_realm_roles_to_group( + admin_group_id, [admin_role] + ) + authenticated_keycloak_api.assign_realm_roles_to_group( + user_group_id, [user_role] + ) # Create OAuth2 client client_data = { @@ -1797,9 +1852,9 @@ def test_admin_user_workflow( authenticated_keycloak_api.create_user(user_data) # Get user ID - user_id = authenticated_keycloak_api.get_users(username=test_username).json()[0][ - "id" - ] + user_id = authenticated_keycloak_api.get_users(username=test_username).json()[ + 0 + ]["id"] # Assign user to groups authenticated_keycloak_api.add_user_to_group(user_id, admin_group_id) @@ -1863,29 +1918,41 @@ def test_admin_user_workflow( try: authenticated_keycloak_api.delete_user(user_id) except Exception as e: - print(f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup user {user_id}: {type(e).__name__}: {e}" + ) if client_internal_id: try: authenticated_keycloak_api.delete_client(client_internal_id) except Exception as e: - print(f"WARNING: Failed to cleanup client {client_internal_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup client {client_internal_id}: {type(e).__name__}: {e}" + ) if admin_group_id: try: authenticated_keycloak_api.delete_group(admin_group_id) except Exception as e: - print(f"WARNING: Failed to cleanup group {admin_group_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup group {admin_group_id}: {type(e).__name__}: {e}" + ) if user_group_id: try: authenticated_keycloak_api.delete_group(user_group_id) except Exception as e: - print(f"WARNING: Failed to cleanup group {user_group_id}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup group {user_group_id}: {type(e).__name__}: {e}" + ) if admin_role_created: try: authenticated_keycloak_api.delete_realm_role(admin_role_name) except Exception as e: - print(f"WARNING: Failed to cleanup role {admin_role_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup role {admin_role_name}: {type(e).__name__}: {e}" + ) if user_role_created: try: authenticated_keycloak_api.delete_realm_role(user_role_name) except Exception as e: - print(f"WARNING: Failed to cleanup role {user_role_name}: {type(e).__name__}: {e}") + print( + f"WARNING: Failed to cleanup role {user_role_name}: {type(e).__name__}: {e}" + )