"""Login helper for Keycloak.""" # numpydoc ignore=EX01,ES01 from __future__ import annotations import html import logging import re from typing import Any, cast import urllib.parse import requests from requests.adapters import HTTPAdapter, Retry from .const import ( CLIENT_ID, DEMO_USER_ACCOUNT, GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN, PROVIDER_URL, REDIRECT_URI, RESPONSE_MODE, RESPONSE_TYPE, SCOPE, TIMEOUT, ) from .exception_classes import ( KeycloakAuthenticationError, KeycloakCodeNotFound, KeycloakGetError, KeycloakInvalidTokenError, KeycloakOperationError, KeycloakPostError, ) from .types import GetTokenResponse class LoginHelper: # numpydoc ignore=ES01,EX01,PR01 """Login helper for Keycloak. Attributes ---------- session : requests.Session Optional session object for making HTTP requests. username : str Username for authentication. password : str Password for authentication. cookie : str Authentication cookie. auth_code : str Authorization code. form_action : str Form action URL for authentication. Notes ----- This class provides utility methods for handling authentication and session management using Keycloak. """ session: requests.Session cookie: str auth_code: str form_action: str def __init__( self, username: str, password: str, totp: str | None = None, session: requests.Session | None = None, logger=None, ) -> None: # numpydoc ignore=ES01,EX01 """Initialize the object with username and password. Parameters ---------- username : str Username for authentication. password : str Password for authentication. totp : str, optional Time-based One-Time Password if enabled, by default None. session : requests.Session, optional Optional session object for making HTTP requests, by default None. logger : logging.Logger, optional Logger object for logging messages, by default None. """ self.username: str = username self.password: str = password self.totp: str | None = totp self.session = session or requests.Session() self.session.verify = True retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504, 408]) self.session.mount("https://", HTTPAdapter(max_retries=retries)) self.logger = logger or logging.getLogger(__name__) def _send_request(self, method, url, **kwargs) -> requests.Response: # numpydoc ignore=ES01,EX01 """Send an HTTP request using the session object. Parameters ---------- method : str HTTP method for the request (e.g., 'GET', 'POST', 'PUT', 'DELETE'). url : str URL to send the request to. **kwargs : dict Additional keyword arguments to pass to `session.request`. Returns ------- requests.Response Response object returned by the HTTP request. Raises ------ ValueError If `self.session` is not initialized (i.e., is `None`). KeycloakOperationError If an HTTP request exception (`requests.RequestException`) occurs. """ if self.session is None: raise ValueError("Session object is not initialized.") try: response = self.session.request(method, url, **kwargs) self.logger.debug("Performed %s request: %s [%s]:\n%s", method, url, response.status_code, response.text[:100]) response.raise_for_status() except requests.RequestException as e: raise KeycloakOperationError from e return response def _login(self) -> None: # numpydoc ignore=ES01,EX01 """Log in to ista EcoTrend. Raises ------ KeycloakAuthenticationError If an authentication error occurs during the login process. """ try: self.auth_code = self._get_auth_code() except KeycloakAuthenticationError as error: raise KeycloakAuthenticationError(error.error_message) from error def _get_auth_code(self) -> str: # numpydoc ignore=ES01,EX01 """ Retrieve the authentication code for ista EcoTrend. Returns ------- str The authentication code obtained from the ista EcoTrend API. Raises ------ KeycloakAuthenticationError If an authentication error occurs during the login process. KeycloakCodeNotFound If the authentication code ('code') is not found in the redirection URL parameters. """ cookie, form_action = self._get_cookie_and_action() resp: requests.Response = self._send_request( "POST", form_action, data={ "username": self.username, "password": self.password, "login": "Login", "credentialId": None, }, headers={"Cookie": cookie}, timeout=TIMEOUT, allow_redirects=False, ) # If the response code is not 302 # raise_error_from_response(resp, KeycloakAuthenticationError, expected_codes=[302]) if resp.status_code != 302: if resp.status_code == 200: form_action = re.search(r'