Source code for restgdf._models.credentials

"""Credentials and token-session configuration models.

Two pydantic models live here:

* :class:`AGOLUserPass` — ArcGIS Online / Enterprise username + password
  credentials. The password field is a :class:`pydantic.SecretStr` so it
  is redacted from ``str()`` / ``repr()`` / logs; the literal value is
  available via ``creds.password.get_secret_value()`` and is only
  dereferenced at the HTTP-POST boundary in
  :mod:`restgdf.utils.token`.

* :class:`TokenSessionConfig` — validated configuration for
  :class:`restgdf.utils.token.ArcGISTokenSession`. Centralizes the
  ``token_url``/``refresh_leeway_seconds``/``clock_skew_seconds``/
  ``verify_ssl`` knobs so validation logic is not scattered across the
  dataclass.

Both models are ``StrictModel`` subclasses — invalid config is an
operator-visible bug, not schema drift.
"""

from __future__ import annotations

from typing import Literal

from pydantic import Field, SecretStr, field_validator, model_validator

from restgdf._compat import _warn_deprecated
from restgdf._models._drift import StrictModel


[docs] class AGOLUserPass(StrictModel): """ArcGIS Online / Enterprise credentials used to mint tokens. ``password`` is stored as :class:`pydantic.SecretStr`. Call ``creds.password.get_secret_value()`` only at the HTTP-POST boundary; never store or log the unwrapped value. """ username: str = Field(..., min_length=1) password: SecretStr referer: str | None = None expiration: int = 60 # minutes; ArcGIS ``generateToken`` default
[docs] class TokenSessionConfig(StrictModel): """Validated configuration for :class:`ArcGISTokenSession`. ``token_url`` is intentionally a plain :class:`str` with a custom validator rather than :class:`pydantic.AnyHttpUrl`. ArcGIS Enterprise deployments commonly run plain HTTP on internal networks, and ``AnyHttpUrl`` normalizes/rejects real-world URLs (for example it appends trailing slashes and may reject edge cases). Accepting any ``http://`` or ``https://`` string matches the behavior ArcGIS clients need. Refresh semantics (BL-04 / R-36, R-37): * ``refresh_leeway_seconds`` (default ``120``) — how far in advance of the token's expiry the session eagerly refreshes. * ``clock_skew_seconds`` (default ``30``, capped at ``30`` when derived from the legacy alias) — extra padding for client / server clock drift. ``refresh_threshold_seconds`` is retained as a deprecation-warning alias. Reads return ``refresh_leeway_seconds + clock_skew_seconds``; writes via the constructor kwarg split the supplied total into ``clock_skew_seconds = min(30, total)`` and ``refresh_leeway_seconds = total - clock_skew_seconds``. """ token_url: str credentials: AGOLUserPass transport: Literal["header", "body", "query"] = "header" header_name: str = Field(default="X-Esri-Authorization", min_length=1) referer: str | None = None token: SecretStr | None = None refresh_leeway_seconds: int = Field(default=120, ge=0) clock_skew_seconds: int = Field(default=30, ge=0) verify_ssl: bool = True @field_validator("token_url") @classmethod def _check_token_url_scheme(cls, value: str) -> str: if not isinstance(value, str) or not value.startswith( ("http://", "https://"), ): raise ValueError( "token_url must start with 'http://' or 'https://' " "(ArcGIS Enterprise frequently uses http on internal networks)", ) return value @model_validator(mode="before") @classmethod def _translate_legacy_refresh_threshold(cls, data: object) -> object: """Translate ``refresh_threshold_seconds=N`` into the new field pair. ``StrictModel`` is configured with ``extra="ignore"``, so unknown keys are silently dropped during normal validation. This validator intercepts ``refresh_threshold_seconds`` *before* that filtering so the legacy alias keeps working and emits a ``DeprecationWarning`` via :func:`restgdf._compat._warn_deprecated`. """ if not isinstance(data, dict): return data if "refresh_threshold_seconds" not in data: return data total = data.pop("refresh_threshold_seconds") _warn_deprecated( "`TokenSessionConfig.refresh_threshold_seconds` is deprecated; " "set `refresh_leeway_seconds` and `clock_skew_seconds` " "explicitly instead. The alias will be removed in a future " "release.", ) if not isinstance(total, int) or isinstance(total, bool): # Let pydantic surface a clear type error by leaving the # translated fields for the field validators to reject. data.setdefault("refresh_leeway_seconds", total) return data skew = min(30, total) leeway = total - skew data.setdefault("clock_skew_seconds", skew) data.setdefault("refresh_leeway_seconds", leeway) return data @property def refresh_threshold_seconds(self) -> int: """Return the legacy threshold sum (``leeway + skew``) and emit a ``DeprecationWarning``.""" _warn_deprecated( "`TokenSessionConfig.refresh_threshold_seconds` is deprecated; " "read `refresh_leeway_seconds` and `clock_skew_seconds` " "directly. The alias will be removed in a future release.", stacklevel=3, ) return self.refresh_leeway_seconds + self.clock_skew_seconds
__all__ = ["AGOLUserPass", "TokenSessionConfig"]