Source code for kstlib.auth.errors

"""Authentication module exceptions."""

from __future__ import annotations

from typing import Any

from kstlib.config.exceptions import KstlibError


[docs] class AuthError(KstlibError): """Base exception for all authentication errors."""
[docs] def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None: """Initialize the auth error with a message and optional structured details.""" super().__init__(message) self.message = message self.details = details or {}
[docs] class ConfigurationError(AuthError): """Raised when auth configuration is invalid or missing."""
[docs] class ProviderNotFoundError(AuthError): """Raised when a named provider is not configured."""
[docs] def __init__(self, provider_name: str) -> None: """Initialize with the name of the missing provider.""" super().__init__(f"Provider '{provider_name}' not found in configuration") self.provider_name = provider_name
[docs] class DiscoveryError(AuthError): """Raised when OIDC discovery fails."""
[docs] def __init__(self, issuer: str, reason: str) -> None: """Initialize with the failing issuer URL and the reason for the failure.""" super().__init__(f"Discovery failed for '{issuer}': {reason}") self.issuer = issuer self.reason = reason
[docs] class TokenError(AuthError): """Base exception for token-related errors."""
[docs] class TokenExpiredError(TokenError): """Raised when a token has expired and cannot be refreshed."""
[docs] class TokenRefreshError(TokenError): """Raised when token refresh fails."""
[docs] def __init__(self, reason: str, *, retryable: bool = False) -> None: """Initialize with the reason for the refresh failure and a retryable flag.""" super().__init__(f"Token refresh failed: {reason}") self.reason = reason self.retryable = retryable
[docs] class TokenExchangeError(TokenError): """Raised when authorization code exchange fails."""
[docs] def __init__(self, reason: str, *, error_code: str | None = None) -> None: """Initialize with the reason for the exchange failure and an optional OAuth error code.""" super().__init__(f"Token exchange failed: {reason}") self.reason = reason self.error_code = error_code
[docs] class TokenValidationError(TokenError): """Raised when JWT validation fails (signature, claims, expiry)."""
[docs] def __init__(self, reason: str, *, claim: str | None = None) -> None: """Initialize with the reason for the validation failure and the offending claim name.""" super().__init__(f"Token validation failed: {reason}") self.reason = reason self.claim = claim
[docs] class TokenStorageError(TokenError): """Raised when token persistence fails (save/load/delete)."""
[docs] class AuthorizationError(AuthError): """Raised during authorization flow failures."""
[docs] def __init__( self, reason: str, *, error_code: str | None = None, error_description: str | None = None, ) -> None: """Initialize with the reason for the failure plus optional OAuth error code and description.""" super().__init__(f"Authorization failed: {reason}") self.reason = reason self.error_code = error_code self.error_description = error_description
[docs] class CallbackServerError(AuthError): """Raised when the local callback server fails to start or receive callback."""
[docs] def __init__(self, reason: str, *, port: int | None = None) -> None: """Initialize with the reason for the callback server failure and the port that was in use.""" super().__init__(f"Callback server error: {reason}") self.reason = reason self.port = port
[docs] class PreflightError(AuthError): """Raised when preflight validation fails."""
[docs] def __init__(self, step: str, reason: str) -> None: """Initialize with the failing preflight step name and the reason for the failure.""" super().__init__(f"Preflight failed at '{step}': {reason}") self.step = step self.reason = reason
[docs] class AuthExpiredError(AuthError): """Raised when an authenticated request returns HTTP 401 indicating token expiration. Surfaced by ``kstlib.rapi.client`` (and any other consumer) when a server response signals that the previously-valid access token has expired or been invalidated during the session. The user must re-authenticate via the appropriate channel (for example, ``sas-admin auth login`` for Viya, or via a dedicated OAuth client when configured in :mod:`kstlib.auth`). Note: Distinct from :class:`TokenExpiredError`. The two cover different lifecycle points and originate from different sub-systems : - ``AuthExpiredError`` (this class, inherits from :class:`AuthError`) is raised by ``kstlib.rapi.client`` when the server returns HTTP 401 at runtime, signalling that a token which was valid at send time has been expired or invalidated by the identity provider during the session. - :class:`TokenExpiredError` (inherits from :class:`TokenError`) is raised by ``kstlib.auth`` when a loaded token is detected as already expired before the request is sent (client-side pre-flight check). Attributes: token_source: Optional label identifying where the token was loaded from (for example, ``'~/.sas/credentials.json'``, ``'env:KSTLIB_TOKEN'``, ``'sops:secrets/api.sops.json'``). ``None`` when the source is unknown. suggested_action: Optional human-readable hint guiding the user toward a successful re-authentication (for example, ``'Run: sas-admin auth login -u <user>'``). ``None`` when no contextual hint is available. Examples: >>> err = AuthExpiredError( ... "Access token expired (HTTP 401).", ... token_source="~/.sas/credentials.json", ... suggested_action="Run: sas-admin auth login -u <user>", ... ) >>> err.token_source '~/.sas/credentials.json' >>> isinstance(err, AuthError) True """
[docs] def __init__( self, message: str, *, token_source: str | None = None, suggested_action: str | None = None, ) -> None: """Initialize AuthExpiredError. Args: message: Human-readable description of the expiration (typically including the HTTP status and a short rationale, never the raw token or response body). token_source: Optional label for where the token came from (used by callers to surface a contextual hint without exposing the secret material itself). suggested_action: Optional hint pointing the user to the right re-authentication procedure. """ super().__init__( message, details={ "token_source": token_source, "suggested_action": suggested_action, }, ) self.token_source = token_source self.suggested_action = suggested_action
__all__ = [ "AuthError", "AuthExpiredError", "AuthorizationError", "CallbackServerError", "ConfigurationError", "DiscoveryError", "PreflightError", "ProviderNotFoundError", "TokenError", "TokenExchangeError", "TokenExpiredError", "TokenRefreshError", "TokenStorageError", "TokenValidationError", ]