airbyte_cdk.sources.declarative.auth.token_provider

  1#
  2# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
  3#
  4
  5
  6import datetime
  7import threading
  8from abc import abstractmethod
  9from dataclasses import InitVar, dataclass, field
 10from typing import Any, List, Mapping, Optional, Union
 11
 12import dpath
 13from isodate import Duration
 14
 15from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
 16from airbyte_cdk.sources.declarative.decoders.json_decoder import JsonDecoder
 17from airbyte_cdk.sources.declarative.exceptions import ReadException
 18from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
 19from airbyte_cdk.sources.declarative.requesters.requester import Requester
 20from airbyte_cdk.sources.http_logger import format_http_message
 21from airbyte_cdk.sources.message import MessageRepository, NoopMessageRepository
 22from airbyte_cdk.sources.types import Config
 23from airbyte_cdk.utils.datetime_helpers import AirbyteDateTime, ab_datetime_now
 24
 25
 26class TokenProvider:
 27    @abstractmethod
 28    def get_token(self) -> str:
 29        pass
 30
 31
 32@dataclass
 33class SessionTokenProvider(TokenProvider):
 34    login_requester: Requester
 35    session_token_path: List[str]
 36    expiration_duration: Optional[Union[datetime.timedelta, Duration]]
 37    parameters: InitVar[Mapping[str, Any]]
 38    message_repository: MessageRepository = NoopMessageRepository()
 39    decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={}))
 40
 41    _next_expiration_time: Optional[AirbyteDateTime] = None
 42    _token: Optional[str] = None
 43
 44    def __post_init__(self, parameters: Mapping[str, Any]) -> None:
 45        self._lock = threading.Lock()
 46
 47    def get_token(self) -> str:
 48        self._refresh_if_necessary()
 49        if self._token is None:
 50            raise ReadException("Failed to get session token, token is None")
 51        return self._token
 52
 53    def _refresh_if_necessary(self) -> None:
 54        if self.expiration_duration is None:
 55            # Without an expiry nothing is cached and every call logs in, so there is nothing to lock
 56            self._refresh()
 57        elif self._token_has_expired():
 58            with self._lock:
 59                if self._token_has_expired():
 60                    self._refresh()
 61
 62    def _token_has_expired(self) -> bool:
 63        return self._next_expiration_time is None or self._next_expiration_time < ab_datetime_now()
 64
 65    def _refresh(self) -> None:
 66        response = self.login_requester.send_request(
 67            log_formatter=lambda response: format_http_message(
 68                response,
 69                "Login request",
 70                "Obtains session token",
 71                None,
 72                is_auxiliary=True,
 73                type="AUTH",
 74            ),
 75        )
 76        if response is None:
 77            raise ReadException("Failed to get session token, response got ignored by requester")
 78        session_token = dpath.get(next(self.decoder.decode(response)), self.session_token_path)
 79        # Set the token first: `get_token` reads it without the lock as soon as the expiry is fresh
 80        self._token = session_token  # type: ignore # Returned decoded response will be Mapping and therefore session_token will be str or None
 81        if self.expiration_duration is not None:
 82            self._next_expiration_time = ab_datetime_now() + self.expiration_duration
 83
 84
 85@dataclass
 86class InterpolatedStringTokenProvider(TokenProvider):
 87    """Provides a token by interpolating a string with config values."""
 88
 89    config: Config
 90    api_token: Union[InterpolatedString, str]
 91    parameters: Mapping[str, Any]
 92
 93    def __post_init__(self) -> None:
 94        self._token = InterpolatedString.create(self.api_token, parameters=self.parameters)
 95
 96    def get_token(self) -> str:
 97        return str(self._token.eval(self.config))
 98
 99
100@dataclass
101class InterpolatedSessionTokenProvider(TokenProvider):
102    """Provides a token by interpolating a template with the session token.
103
104    This allows flexible token formatting, such as "Token {{ session_token }}"
105    for Django REST Framework APIs that expect "Authorization: Token <value>".
106    """
107
108    config: Config
109    api_token: Union[InterpolatedString, str]
110    session_token_provider: TokenProvider
111    parameters: Mapping[str, Any]
112
113    def __post_init__(self) -> None:
114        self._token_template = InterpolatedString.create(self.api_token, parameters=self.parameters)
115
116    def get_token(self) -> str:
117        session_token = self.session_token_provider.get_token()
118        return str(self._token_template.eval(self.config, session_token=session_token))
class TokenProvider:
27class TokenProvider:
28    @abstractmethod
29    def get_token(self) -> str:
30        pass
@abstractmethod
def get_token(self) -> str:
28    @abstractmethod
29    def get_token(self) -> str:
30        pass
@dataclass
class SessionTokenProvider(TokenProvider):
33@dataclass
34class SessionTokenProvider(TokenProvider):
35    login_requester: Requester
36    session_token_path: List[str]
37    expiration_duration: Optional[Union[datetime.timedelta, Duration]]
38    parameters: InitVar[Mapping[str, Any]]
39    message_repository: MessageRepository = NoopMessageRepository()
40    decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={}))
41
42    _next_expiration_time: Optional[AirbyteDateTime] = None
43    _token: Optional[str] = None
44
45    def __post_init__(self, parameters: Mapping[str, Any]) -> None:
46        self._lock = threading.Lock()
47
48    def get_token(self) -> str:
49        self._refresh_if_necessary()
50        if self._token is None:
51            raise ReadException("Failed to get session token, token is None")
52        return self._token
53
54    def _refresh_if_necessary(self) -> None:
55        if self.expiration_duration is None:
56            # Without an expiry nothing is cached and every call logs in, so there is nothing to lock
57            self._refresh()
58        elif self._token_has_expired():
59            with self._lock:
60                if self._token_has_expired():
61                    self._refresh()
62
63    def _token_has_expired(self) -> bool:
64        return self._next_expiration_time is None or self._next_expiration_time < ab_datetime_now()
65
66    def _refresh(self) -> None:
67        response = self.login_requester.send_request(
68            log_formatter=lambda response: format_http_message(
69                response,
70                "Login request",
71                "Obtains session token",
72                None,
73                is_auxiliary=True,
74                type="AUTH",
75            ),
76        )
77        if response is None:
78            raise ReadException("Failed to get session token, response got ignored by requester")
79        session_token = dpath.get(next(self.decoder.decode(response)), self.session_token_path)
80        # Set the token first: `get_token` reads it without the lock as soon as the expiry is fresh
81        self._token = session_token  # type: ignore # Returned decoded response will be Mapping and therefore session_token will be str or None
82        if self.expiration_duration is not None:
83            self._next_expiration_time = ab_datetime_now() + self.expiration_duration
SessionTokenProvider( login_requester: airbyte_cdk.Requester, session_token_path: List[str], expiration_duration: Union[datetime.timedelta, isodate.duration.Duration, NoneType], parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]], message_repository: airbyte_cdk.MessageRepository = <airbyte_cdk.sources.message.NoopMessageRepository object>, decoder: airbyte_cdk.Decoder = <factory>, _next_expiration_time: Optional[airbyte_cdk.utils.datetime_helpers.AirbyteDateTime] = None, _token: Optional[str] = None)
login_requester: airbyte_cdk.Requester
session_token_path: List[str]
expiration_duration: Union[datetime.timedelta, isodate.duration.Duration, NoneType]
parameters: dataclasses.InitVar[typing.Mapping[str, typing.Any]]
def get_token(self) -> str:
48    def get_token(self) -> str:
49        self._refresh_if_necessary()
50        if self._token is None:
51            raise ReadException("Failed to get session token, token is None")
52        return self._token
@dataclass
class InterpolatedStringTokenProvider(TokenProvider):
86@dataclass
87class InterpolatedStringTokenProvider(TokenProvider):
88    """Provides a token by interpolating a string with config values."""
89
90    config: Config
91    api_token: Union[InterpolatedString, str]
92    parameters: Mapping[str, Any]
93
94    def __post_init__(self) -> None:
95        self._token = InterpolatedString.create(self.api_token, parameters=self.parameters)
96
97    def get_token(self) -> str:
98        return str(self._token.eval(self.config))

Provides a token by interpolating a string with config values.

InterpolatedStringTokenProvider( config: Mapping[str, Any], api_token: Union[airbyte_cdk.InterpolatedString, str], parameters: Mapping[str, Any])
config: Mapping[str, Any]
api_token: Union[airbyte_cdk.InterpolatedString, str]
parameters: Mapping[str, Any]
def get_token(self) -> str:
97    def get_token(self) -> str:
98        return str(self._token.eval(self.config))
@dataclass
class InterpolatedSessionTokenProvider(TokenProvider):
101@dataclass
102class InterpolatedSessionTokenProvider(TokenProvider):
103    """Provides a token by interpolating a template with the session token.
104
105    This allows flexible token formatting, such as "Token {{ session_token }}"
106    for Django REST Framework APIs that expect "Authorization: Token <value>".
107    """
108
109    config: Config
110    api_token: Union[InterpolatedString, str]
111    session_token_provider: TokenProvider
112    parameters: Mapping[str, Any]
113
114    def __post_init__(self) -> None:
115        self._token_template = InterpolatedString.create(self.api_token, parameters=self.parameters)
116
117    def get_token(self) -> str:
118        session_token = self.session_token_provider.get_token()
119        return str(self._token_template.eval(self.config, session_token=session_token))

Provides a token by interpolating a template with the session token.

This allows flexible token formatting, such as "Token {{ session_token }}" for Django REST Framework APIs that expect "Authorization: Token ".

InterpolatedSessionTokenProvider( config: Mapping[str, Any], api_token: Union[airbyte_cdk.InterpolatedString, str], session_token_provider: TokenProvider, parameters: Mapping[str, Any])
config: Mapping[str, Any]
api_token: Union[airbyte_cdk.InterpolatedString, str]
session_token_provider: TokenProvider
parameters: Mapping[str, Any]
def get_token(self) -> str:
117    def get_token(self) -> str:
118        session_token = self.session_token_provider.get_token()
119        return str(self._token_template.eval(self.config, session_token=session_token))