iaptoolkit 0.2.2__tar.gz → 0.3.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/PKG-INFO +4 -4
  2. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/README.md +2 -2
  3. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/pyproject.toml +2 -2
  4. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/__init__.py +64 -78
  5. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/exceptions.py +20 -3
  6. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/headers.py +3 -1
  7. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/base.py +68 -0
  8. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/__init__.py +110 -0
  9. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/datastore_oauth2.py +39 -0
  10. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/gua.py +11 -0
  11. iaptoolkit-0.2.2/src/iaptoolkit/tokens/service_account.py → iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/__init__.py +149 -188
  12. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/datastore_oidc.py +35 -0
  13. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/gsa.py +11 -0
  14. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/tokens/structs.py +5 -2
  15. iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/token_datastore.py +63 -0
  16. iaptoolkit-0.3.0a0/src/iaptoolkit/utils/__init__.py +0 -0
  17. iaptoolkit-0.2.2/src/iaptoolkit/tokens/__init__.py +0 -24
  18. iaptoolkit-0.2.2/src/iaptoolkit/tokens/token_datastore.py +0 -68
  19. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/LICENSE +0 -0
  20. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/constants.py +0 -0
  21. {iaptoolkit-0.2.2/src/iaptoolkit/utils → iaptoolkit-0.3.0a0/src/iaptoolkit/tokens}/__init__.py +0 -0
  22. {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/utils/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iaptoolkit
3
- Version: 0.2.2
3
+ Version: 0.3.0a0
4
4
  Summary: Library of common utils for interacting with Identity-Aware Proxies
5
5
  Author: Rob Voigt
6
6
  Author-email: code@ravoigt.com
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Requires-Dist: google-auth (>=2.29.0,<3.0.0)
12
- Requires-Dist: kvcommon (>=0.1.3,<0.2.0)
12
+ Requires-Dist: kvcommon (>=0.1.4,<0.2.0)
13
13
  Requires-Dist: pytest (>=7.4.4,<8.0.0)
14
14
  Requires-Dist: requests (>=2.31.0,<3.0.0)
15
15
  Requires-Dist: toml (>=0.10.2,<0.11.0)
@@ -34,9 +34,9 @@ https://pypi.org/project/iaptoolkit/
34
34
  ```python
35
35
  import requests
36
36
 
37
- from iaptoolkit import IAPToolkit
37
+ from iaptoolkit import IAPToolkit_OIDC
38
38
 
39
- iaptk = IAPToolkit(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
39
+ iaptk_oidc = IAPToolkit_OIDC(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
40
40
  allowed_domains = ["example.com", ]
41
41
 
42
42
 
@@ -17,9 +17,9 @@ https://pypi.org/project/iaptoolkit/
17
17
  ```python
18
18
  import requests
19
19
 
20
- from iaptoolkit import IAPToolkit
20
+ from iaptoolkit import IAPToolkit_OIDC
21
21
 
22
- iaptk = IAPToolkit(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
22
+ iaptk_oidc = IAPToolkit_OIDC(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
23
23
  allowed_domains = ["example.com", ]
24
24
 
25
25
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "iaptoolkit"
3
- version = "0.2.2"
3
+ version = "0.3.0a"
4
4
  description = "Library of common utils for interacting with Identity-Aware Proxies"
5
5
  authors = ["Rob Voigt <code@ravoigt.com>"]
6
6
  readme = "README.md"
@@ -28,7 +28,7 @@ google-auth = "^2.29.0"
28
28
  requests = "^2.31.0"
29
29
  pytest = "^7.4.4"
30
30
  toml = "^0.10.2"
31
- kvcommon = "^0.1.3"
31
+ kvcommon = "^0.1.4"
32
32
 
33
33
  [tool.poetry.dev-dependencies]
34
34
  black = "*"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  import logging
4
5
 
5
6
  logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -8,59 +9,50 @@ import typing as t
8
9
  from urllib.parse import ParseResult
9
10
  from urllib.parse import urlparse
10
11
 
11
- from kvcommon import logger
12
+ from kvcommon.logger import get_logger
12
13
 
13
14
  from iaptoolkit import headers
14
15
  from iaptoolkit.exceptions import ServiceAccountTokenException
15
- from iaptoolkit.tokens.service_account import ServiceAccount
16
+ from iaptoolkit.tokens.base import BaseTokenInterface
17
+ from iaptoolkit.tokens.oauth2 import OAuth2
18
+ from iaptoolkit.tokens.oidc import OIDC
16
19
  from iaptoolkit.tokens.structs import ResultAddTokenHeader
17
-
18
20
  from iaptoolkit.tokens.structs import TokenRefreshStruct
19
21
  from iaptoolkit.utils.urls import is_url_safe_for_token
20
22
 
21
- LOG = logger.get_logger("iaptk")
23
+ LOG = get_logger("iaptk")
22
24
 
23
25
 
24
- class IAPToolkit:
26
+ class IAPToolkit(ABC):
25
27
  """
26
- Class to encapsulate client-specific vars and forward them to static functions
28
+ Abstract base class wrapping up core iaptoolkit functionality in a single interface
29
+
30
+ In practice, you should use IAPToolkit_OIDC or IAPToolkit_OAuth2 for
31
+ OIDC(ServiceAccounts) or OAuth2(Users) respectively.
27
32
  """
28
33
 
29
34
  _GOOGLE_IAP_CLIENT_ID: str
35
+ _interface: BaseTokenInterface
30
36
 
31
- def __init__(
32
- self,
33
- google_iap_client_id: str,
34
- ) -> None:
37
+ def __init__(self, google_iap_client_id: str) -> None:
35
38
  self._GOOGLE_IAP_CLIENT_ID = google_iap_client_id
36
39
 
37
40
  @staticmethod
38
41
  def sanitize_request_headers(request_headers: dict) -> dict:
39
42
  return headers.sanitize_request_headers(request_headers)
40
43
 
41
- def get_token_oidc(self, bypass_cached: bool = False) -> TokenRefreshStruct:
42
- try:
43
- return ServiceAccount.get_token(
44
- iap_client_id=self._GOOGLE_IAP_CLIENT_ID, bypass_cached=bypass_cached
45
- )
46
- except ServiceAccountTokenException as ex:
47
- LOG.debug(ex)
48
- raise
49
-
50
- def get_token_oidc_str(self, bypass_cached: bool = False) -> str:
51
- struct = self.get_token_oidc(bypass_cached=bypass_cached)
52
- return struct.id_token
53
-
54
- def get_token_oauth2(self, bypass_cached: bool = False) -> TokenRefreshStruct:
55
- # TODO
44
+ @abstractmethod
45
+ def get_token(
46
+ self, refresh_token: str | None, bypass_cached: bool = False
47
+ ) -> TokenRefreshStruct:
56
48
  raise NotImplementedError()
57
49
 
58
- def get_token_oauth2_str(self, bypass_cached: bool = False) -> str:
59
- struct = self.get_token_oauth2(bypass_cached=bypass_cached)
50
+ def get_token_str(self, refresh_token: str | None, bypass_cached: bool = False) -> str:
51
+ struct = self.get_token(refresh_token=refresh_token, bypass_cached=bypass_cached)
60
52
  return struct.id_token
61
53
 
62
54
  def get_token_and_add_to_headers(
63
- self, request_headers: dict, use_oauth2: bool = False, use_auth_header: bool = False
55
+ self, request_headers: dict, use_auth_header: bool = False, refresh_token: str | None = None
64
56
  ) -> bool:
65
57
  """
66
58
  Retrieves an auth token and inserts it into the supplied request_headers dict.
@@ -71,13 +63,9 @@ class IAPToolkit:
71
63
  use_oauth2: Use OAuth2.0 credentials and respective token, else use OIDC (default)
72
64
  As a general guideline, OIDC is the assumed default approach for ServiceAccounts.
73
65
  use_auth_header: If true, use the 'Authorization' header instead of 'Proxy-Authorization'
74
-
75
-
66
+ refresh_token: Refresh token for OAuth2.0 (Unused by OIDC)
76
67
  """
77
- if not use_oauth2:
78
- token_refresh_struct: TokenRefreshStruct = self.get_token_oidc()
79
- else:
80
- token_refresh_struct: TokenRefreshStruct = self.get_token_oauth2()
68
+ token_refresh_struct = self.get_token(refresh_token=refresh_token)
81
69
 
82
70
  headers.add_token_to_request_headers(
83
71
  request_headers=request_headers,
@@ -102,8 +90,8 @@ class IAPToolkit:
102
90
  url: str | ParseResult,
103
91
  request_headers: dict,
104
92
  valid_domains: t.List[str] | None = None,
105
- use_oauth2: bool = False,
106
93
  use_auth_header: bool = False,
94
+ refresh_token: str | None = None,
107
95
  ) -> ResultAddTokenHeader:
108
96
  """
109
97
  Checks that the supplied URL is valid (i.e.; in valid_domains) and if so, retrieves a
@@ -115,66 +103,54 @@ class IAPToolkit:
115
103
  url: URL string or urllib.ParseResult to check for validity
116
104
  request_headers: Dict of headers to insert into
117
105
  valid_domains: List of domains to validate URL against
118
- use_oauth2: Passed to get_token_and_add_to_headers() to determine if OAuth2.0 is used or OIDC (default)
106
+ use_auth_header: If true, use the 'Authorization' header instead of 'Proxy-Authorization' for IAP
107
+ refresh_token: Refresh token for OAuth2.0 (Unused by OIDC)
119
108
  """
120
109
 
121
110
  if self.is_url_safe_for_token(url=url, valid_domains=valid_domains):
122
111
  token_is_fresh = self.get_token_and_add_to_headers(
123
112
  request_headers=request_headers,
124
- use_oauth2=use_oauth2,
125
113
  use_auth_header=use_auth_header,
114
+ refresh_token=refresh_token,
126
115
  )
127
- return ResultAddTokenHeader(token_added=True, token_is_fresh=token_is_fresh)
116
+ return ResultAddTokenHeader(token_added=True, token_is_new=token_is_fresh)
128
117
  else:
129
118
  LOG.warn(
130
119
  "URL is not approved: %s - Token will not be added to headers. Valid domains are: %s",
131
120
  url,
132
121
  valid_domains,
133
122
  )
134
- return ResultAddTokenHeader(token_added=False, token_is_fresh=False)
123
+ return ResultAddTokenHeader(token_added=False, token_is_new=False)
135
124
 
136
125
 
137
126
  class IAPToolkit_OIDC(IAPToolkit):
138
127
  """
139
- Convenience subclass of IAPToolkit for scenarios where OIDC will always be used, never OAuth2
128
+ OIDC-only implementation of IAPToolkit
140
129
  """
130
+ _interface: OIDC
141
131
 
142
- def get_token_oauth2(self, *args, **kwargs):
143
- raise NotImplementedError("Cannot call OAuth2 methods on OIDC-only instance of IAPToolkit.")
132
+ def __init__(self, google_iap_client_id: str) -> None:
133
+ super().__init__(google_iap_client_id)
134
+ self._interface = OIDC(iap_client_id=google_iap_client_id)
144
135
 
145
- def get_token_oauth2_str(self, *args, **kwargs):
146
- raise NotImplementedError("Cannot call OAuth2 methods on OIDC-only instance of IAPToolkit.")
147
-
148
- def get_token_and_add_to_headers(
149
- self, request_headers: dict, use_auth_header: bool = False
150
- ) -> bool:
151
- return super().get_token_and_add_to_headers(
152
- request_headers=request_headers, use_oauth2=False, use_auth_header=use_auth_header
153
- )
154
-
155
- def check_url_and_add_token_header(
156
- self,
157
- url: str | ParseResult,
158
- request_headers: dict,
159
- valid_domains: t.List[str] | None = None,
160
- use_auth_header: bool = False,
161
- ) -> ResultAddTokenHeader:
162
- return super().check_url_and_add_token_header(
163
- url,
164
- request_headers=request_headers,
165
- valid_domains=valid_domains,
166
- use_oauth2=False,
167
- use_auth_header=use_auth_header,
168
- )
136
+ def get_token(self, bypass_cached: bool = False) -> TokenRefreshStruct:
137
+ try:
138
+ return self._interface.get_token(
139
+ iap_client_id=self._GOOGLE_IAP_CLIENT_ID, bypass_cached=bypass_cached
140
+ )
141
+ except ServiceAccountTokenException as ex:
142
+ LOG.debug(ex)
143
+ raise
169
144
 
170
145
 
171
146
  class IAPToolkit_OAuth2(IAPToolkit):
172
147
  """
173
- Convenience subclass of IAPToolkit for scenarios where OAuth2 will always be used, never OIDC
148
+ OAuth2.0-only implementation of IAPToolkit
174
149
  """
175
150
 
176
151
  _GOOGLE_CLIENT_ID: str
177
152
  _GOOGLE_CLIENT_SECRET: str
153
+ _interface: OAuth2
178
154
 
179
155
  def __init__(
180
156
  self,
@@ -185,24 +161,34 @@ class IAPToolkit_OAuth2(IAPToolkit):
185
161
  super().__init__(google_iap_client_id=google_iap_client_id)
186
162
  self._GOOGLE_CLIENT_ID = google_client_id
187
163
  self._GOOGLE_CLIENT_SECRET = google_client_secret
164
+ self._interface = OAuth2(iap_client_id=google_iap_client_id, client_id=google_client_id)
188
165
 
189
- def get_token_oidc(self, *args, **kwargs):
190
- raise NotImplementedError("Cannot call OIDC methods on OAuth2-only instance of IAPToolkit.")
166
+ def get_refresh_token(self, bypass_cached: bool = False) -> t.Any:
167
+ pass
191
168
 
192
- def get_token_oidc_str(self, *args, **kwargs):
193
- raise NotImplementedError("Cannot call OIDC methods on OAuth2-only instance of IAPToolkit.")
169
+ def get_token(self, refresh_token: str, bypass_cached: bool = False) -> TokenRefreshStruct:
170
+ if not self._GOOGLE_CLIENT_ID or not self._GOOGLE_CLIENT_SECRET:
171
+ raise ValueError() # TODO
194
172
 
195
- def get_token_and_add_to_headers(
196
- self, request_headers: dict, use_auth_header: bool = False
197
- ) -> bool:
198
- return super().get_token_and_add_to_headers(
199
- request_headers=request_headers, use_oauth2=True, use_auth_header=use_auth_header
200
- )
173
+ # TODO: Get from cache and check expiry
174
+ expired = True
175
+
176
+ if expired or bypass_cached:
177
+ token: str = self._interface.get_id_token_from_refresh_token(
178
+ client_id=self._GOOGLE_CLIENT_ID,
179
+ client_secret=self._GOOGLE_CLIENT_SECRET,
180
+ refresh_token=refresh_token,
181
+ iap_client_id=self._GOOGLE_IAP_CLIENT_ID,
182
+ )
183
+
184
+ # TODO: Move this when implementing cache
185
+ return TokenRefreshStruct(id_token=token, token_is_new=expired or bypass_cached)
201
186
 
202
187
  def check_url_and_add_token_header(
203
188
  self,
204
189
  url: str | ParseResult,
205
190
  request_headers: dict,
191
+ refresh_token: str,
206
192
  valid_domains: t.List[str] | None = None,
207
193
  use_auth_header: bool = False,
208
194
  ) -> ResultAddTokenHeader:
@@ -210,6 +196,6 @@ class IAPToolkit_OAuth2(IAPToolkit):
210
196
  url=url,
211
197
  request_headers=request_headers,
212
198
  valid_domains=valid_domains,
213
- use_oauth2=True,
214
199
  use_auth_header=use_auth_header,
200
+ refresh_token=refresh_token,
215
201
  )
@@ -14,6 +14,10 @@ class IAPBadResponse(IAPToolkitBaseException):
14
14
  pass
15
15
 
16
16
 
17
+ class InvalidDomain(IAPToolkitBaseException):
18
+ pass
19
+
20
+
17
21
  class TokenException(IAPToolkitBaseException):
18
22
  pass
19
23
 
@@ -22,7 +26,10 @@ class TokenStorageException(TokenException):
22
26
  pass
23
27
 
24
28
 
25
- class ServiceAccountTokenException(TokenException):
29
+ class GoogleTokenException(TokenException):
30
+ """
31
+ Wrapper for exceptions from Google's auth lib
32
+ """
26
33
  def __init__(
27
34
  self, message: str, google_exception: t.Union[DefaultCredentialsError, RefreshError] | None
28
35
  ):
@@ -42,8 +49,9 @@ class ServiceAccountTokenException(TokenException):
42
49
  def retryable(self):
43
50
  return self.google_exception and self.google_exception._retryable
44
51
 
52
+ # SA / OIDC
45
53
 
46
- class ServiceAccountNoDefaultCredentials(ServiceAccountTokenException):
54
+ class ServiceAccountTokenException(GoogleTokenException):
47
55
  pass
48
56
 
49
57
 
@@ -51,5 +59,14 @@ class ServiceAccountTokenFailedRefresh(ServiceAccountTokenException):
51
59
  pass
52
60
 
53
61
 
54
- class InvalidDomain(IAPToolkitBaseException):
62
+ class ServiceAccountNoDefaultCredentials(ServiceAccountTokenException):
63
+ pass
64
+
65
+ # OAuth2 / User
66
+
67
+ class OAuth2RefreshFromAuthCodeFailed(TokenException):
68
+ pass
69
+
70
+
71
+ class OAuth2IDTokenFromRefreshFailed(TokenException):
55
72
  pass
@@ -23,7 +23,9 @@ def _sanitize_request_header(headers_dict: dict, header_key: str):
23
23
  def sanitize_request_headers(headers: dict) -> dict:
24
24
  """
25
25
  Sanitizes a headers dict to remove sensitive strings for logging purposes.
26
- Returns A COPY of the dict with sensitive k/v pairs replaced. Does NOT modify in-place/by-reference.
26
+
27
+ Returns:
28
+ A COPY of the dict with sensitive k/v pairs replaced. Does NOT modify in-place/by-reference.
27
29
  """
28
30
  log_safe_headers = headers.copy()
29
31
 
@@ -0,0 +1,68 @@
1
+ import datetime
2
+ from abc import ABC, abstractmethod
3
+
4
+ from kvcommon import logger
5
+
6
+ from iaptoolkit.exceptions import TokenStorageException
7
+ from iaptoolkit.tokens.token_datastore import TokenDatastore
8
+
9
+ from iaptoolkit.tokens.structs import TokenStruct
10
+
11
+
12
+ LOG = logger.get_logger("iaptk")
13
+
14
+
15
+ class BaseTokenInterface(ABC):
16
+ _datastore: TokenDatastore
17
+ _iap_client_id: str
18
+
19
+ def __init__(self, datastore: TokenDatastore, iap_client_id: str) -> None:
20
+ super().__init__()
21
+ self._iap_client_id = iap_client_id
22
+ self._datastore = datastore
23
+
24
+ @abstractmethod
25
+ def _get_stored_token(self) -> dict | None:
26
+ raise NotImplementedError()
27
+
28
+ @abstractmethod
29
+ def _store_token(self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
30
+ raise NotImplementedError()
31
+
32
+ def get_stored_token(self) -> TokenStruct | None:
33
+ try:
34
+ token_dict = self._get_stored_token()
35
+ if (
36
+ not token_dict
37
+ or not token_dict.get("id_token", None)
38
+ or not token_dict.get("token_expiry", None)
39
+ ):
40
+ LOG.debug("No stored token for supplied client_id(s)")
41
+ return
42
+
43
+ id_token_from_dict = token_dict.get("id_token")
44
+ token_expiry_from_dict = token_dict.get("token_expiry", "")
45
+
46
+ if not id_token_from_dict:
47
+ LOG.warning("Invalid stored ID token")
48
+ return
49
+
50
+ token_expiry = ""
51
+ try:
52
+ token_expiry = datetime.datetime.fromisoformat(token_expiry_from_dict)
53
+ except (ValueError, TypeError) as ex:
54
+ LOG.debug("Invalid token expiry for supplied client_id(s)")
55
+ return
56
+
57
+ token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry)
58
+ if token_struct.expired:
59
+ LOG.debug("Stored OAuth2 token for supplied client_id(s) has EXPIRED")
60
+ return
61
+ return token_struct
62
+
63
+ except Exception as ex:
64
+ # Err on the side of not letting token-caching break requests, hence blanket except
65
+ # Caller can `try/except TokenStorageException` for safety
66
+ raise TokenStorageException(
67
+ f"Exception when trying to retrieve stored token. exception={ex}"
68
+ )
@@ -0,0 +1,110 @@
1
+ import datetime
2
+ import json
3
+ import typing as t
4
+ import requests
5
+
6
+ from kvcommon import logger
7
+ from kvcommon.datastore.backend import DictBackend
8
+
9
+ from iaptoolkit.tokens.base import BaseTokenInterface
10
+ from iaptoolkit.exceptions import OAuth2IDTokenFromRefreshFailed
11
+ from iaptoolkit.exceptions import OAuth2RefreshFromAuthCodeFailed
12
+
13
+ from .datastore_oauth2 import TokenDatastore_OAuth2
14
+
15
+
16
+ LOG = logger.get_logger("iaptk-oauth2")
17
+
18
+ GOOGLE_OAUTH_TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token"
19
+
20
+
21
+ def get_localhost_redirect_uri(listen_port: int):
22
+ return f"http://localhost:{listen_port}"
23
+
24
+
25
+ def get_oauth2_auth_url(client_id: str, redirect_uri: str):
26
+ # TODO: Unhardcode
27
+ return (
28
+ f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}"
29
+ f"&response_type=code&scope=openid%20email&access_type=offline&redirect_uri={redirect_uri}"
30
+ )
31
+
32
+
33
+ class OAuth2(BaseTokenInterface):
34
+ """
35
+ Base class for interacting with OAuth2.0 tokens for IAP
36
+
37
+ OAuth2.0 access Tokens have a shorter expiry (<60mins)
38
+ Refresh tokens have a longer expiry and are used to retrieve new access tokens
39
+
40
+ # TODO: Move Google-specific logic to GoogleServiceAccount
41
+ """
42
+
43
+ _datastore: TokenDatastore_OAuth2
44
+ _client_id: str
45
+
46
+ def __init__(self, iap_client_id: str, client_id: str) -> None:
47
+ super().__init__(
48
+ datastore=TokenDatastore_OAuth2(DictBackend),
49
+ iap_client_id=iap_client_id,
50
+ )
51
+ self._client_id = client_id
52
+
53
+ def _store_token(self, id_token: str, token_expiry: datetime.datetime):
54
+ self._datastore.store_token(self._iap_client_id, self._client_id, id_token, token_expiry)
55
+
56
+ def _get_stored_token(self, iap_client_id: str, client_id: str) -> dict | None:
57
+ return self._datastore.get_stored_token(iap_client_id=iap_client_id, client_id=client_id)
58
+
59
+ # TODO: Unstatic
60
+ @staticmethod
61
+ def get_id_token_from_refresh_token(
62
+ client_id: str,
63
+ client_secret: str,
64
+ refresh_token: str,
65
+ iap_client_id: str,
66
+ ) -> str:
67
+
68
+ oauth2_token_url = GOOGLE_OAUTH_TOKEN_URL # TODO: Unhardcode
69
+ request_payload = {
70
+ "client_id": client_id,
71
+ "client_secret": client_secret,
72
+ "refresh_token": refresh_token,
73
+ "grant_type": "refresh_token",
74
+ "audience": iap_client_id,
75
+ }
76
+ response = requests.post(oauth2_token_url, data=request_payload)
77
+ response_dict = json.loads(response.text)
78
+ id_token: str = response_dict.get("id_token", None)
79
+ if response.status_code != 200 or not id_token:
80
+ raise OAuth2IDTokenFromRefreshFailed(
81
+ f"Failure in acquiring OAuth2.0 access token from refresh token - HTTP Response:"
82
+ f"{response.status_code} : {response.reason or 'Unknown'} : {response.text or ''}"
83
+ )
84
+ return id_token
85
+
86
+ # TODO: Unstatic
87
+ @staticmethod
88
+ def get_refresh_token_from_auth_code(
89
+ client_id: str,
90
+ client_secret: str,
91
+ auth_code: str,
92
+ redirect_uri: str,
93
+ ) -> str:
94
+ oauth2_token_url = GOOGLE_OAUTH_TOKEN_URL # TODO: Unhardcode
95
+ request_payload = {
96
+ "code": auth_code,
97
+ "client_id": client_id,
98
+ "client_secret": client_secret,
99
+ "grant_type": "authorization_code",
100
+ "redirect_uri": redirect_uri,
101
+ }
102
+ response = requests.post(oauth2_token_url, data=request_payload)
103
+ response_dict = json.loads(response.text)
104
+ refresh_token: str = response_dict.get("refresh_token", None)
105
+ if response.status_code != 200 or not refresh_token:
106
+ raise OAuth2RefreshFromAuthCodeFailed(
107
+ f"Failure in acquiring refresh token from auth code - HTTP Response:"
108
+ f"{response.status_code} : {response.reason or 'Unknown'} : {response.text or ''}"
109
+ )
110
+ return refresh_token
@@ -0,0 +1,39 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ from kvcommon import logger
5
+
6
+ from iaptoolkit.tokens.token_datastore import TokenDatastore
7
+
8
+
9
+ LOG = logger.get_logger("iaptk-ds-oauth2")
10
+
11
+
12
+ class TokenDatastore_OAuth2(TokenDatastore):
13
+ _tokens_key: str = "outh2_tokens"
14
+
15
+ def get_stored_token(self, iap_client_id: str, client_id: str) -> dict | None:
16
+ tokens_dict = self.get_or_create_nested_dict(self._tokens_key)
17
+ source_key = f"{iap_client_id}{client_id}"
18
+ token_struct_dict = self._retrieve_hashed_dict_entry(
19
+ target=tokens_dict, source_key=source_key
20
+ )
21
+ if not token_struct_dict:
22
+ LOG.debug("No stored service account token for current iap_client_id")
23
+ return
24
+ return token_struct_dict
25
+
26
+ def store_token(
27
+ self, iap_client_id: str, client_id: str, id_token: str, token_expiry: datetime.datetime
28
+ ):
29
+ tokens_dict = self.get_or_create_nested_dict(self._tokens_key)
30
+
31
+ # TODO: Encode/encrypt token?
32
+ value = dict(id_token=id_token, token_expiry=token_expiry.isoformat())
33
+ source_key = f"{iap_client_id}{client_id}"
34
+ self._insert_hashed_dict_entry(target=tokens_dict, source_key=source_key, value=value)
35
+
36
+ try:
37
+ self.update_data(outh2_tokens=tokens_dict)
38
+ except OSError as ex:
39
+ LOG.error("Failed to store OAuth2 token for re-use. exception=%s", ex)
@@ -0,0 +1,11 @@
1
+ from . import OAuth2
2
+
3
+
4
+ class GoogleUserAccount(OAuth2):
5
+ """
6
+ For interacting with Google user accounts and OAuth2.0 tokens for Google IAP
7
+
8
+ # TODO: Move Google-specific logic into this class
9
+ """
10
+
11
+ pass
@@ -1,188 +1,149 @@
1
- import datetime
2
- import typing as t
3
-
4
- from google.auth.compute_engine import IDTokenCredentials as GoogleIDTokenCredentials
5
- from google.auth.exceptions import DefaultCredentialsError as GoogleDefaultCredentialsError
6
- from google.auth.exceptions import RefreshError as GoogleRefreshError
7
- from google.auth.transport.requests import Request as GoogleRequest
8
- from google.oauth2 import id_token as google_id_token_lib
9
-
10
- from kvcommon import logger
11
-
12
- # TODO: Don't hardcode the association between OIDC/SA and dict-datastore
13
- from iaptoolkit.tokens.token_datastore import datastore
14
- from iaptoolkit.exceptions import ServiceAccountTokenException
15
- from iaptoolkit.exceptions import ServiceAccountTokenFailedRefresh
16
- from iaptoolkit.exceptions import ServiceAccountNoDefaultCredentials
17
- from iaptoolkit.exceptions import TokenStorageException
18
-
19
- from .structs import TokenStruct
20
- from .structs import TokenRefreshStruct
21
-
22
-
23
- LOG = logger.get_logger("iaptk")
24
- MAX_RECURSE = 3
25
-
26
-
27
- class ServiceAccount(object):
28
- """Base class for interacting with service accounts and OIDC tokens for IAP"""
29
-
30
- # TODO: This is a static namespace for SA functions. Turn it into a per-iap-client-id client
31
- # TODO: Move Google-specific logic to GoogleServiceAccount
32
-
33
- @staticmethod
34
- def _store_token(iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
35
- try:
36
- datastore.store_service_account_token(iap_client_id, id_token, token_expiry)
37
- except Exception as ex: # Err on the side of not letting token-caching break requests.
38
- raise TokenStorageException(f"Exception when trying to store token. exception={ex}")
39
-
40
- @staticmethod
41
- def get_stored_token(iap_client_id: str) -> t.Optional[TokenStruct]:
42
- try:
43
- token_dict = datastore.get_stored_service_account_token(iap_client_id)
44
- if (
45
- not token_dict
46
- or not token_dict.get("id_token", None)
47
- or not token_dict.get("token_expiry", None)
48
- ):
49
- LOG.debug("No stored service account token for current iap_client_id")
50
- return
51
-
52
- id_token_from_dict = token_dict.get("id_token")
53
- token_expiry_from_dict = token_dict.get("token_expiry", "")
54
-
55
- if not id_token_from_dict:
56
- LOG.warning("Invalid stored ID token")
57
- return
58
-
59
- token_expiry = ""
60
- try:
61
- token_expiry = datetime.datetime.fromisoformat(token_expiry_from_dict)
62
- except (ValueError, TypeError) as ex:
63
- LOG.debug("Invalid token expiry for current iap_client_id")
64
- return
65
-
66
- token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry)
67
- if token_struct.expired:
68
- LOG.debug("Stored service account token for current iap_client_id has EXPIRED")
69
- return
70
- return token_struct
71
-
72
- except Exception as ex:
73
- # Err on the side of not letting token-caching break requests, hence blanket except
74
- raise TokenStorageException(
75
- f"Exception when trying to retrieve stored token. exception={ex}"
76
- )
77
-
78
- @staticmethod
79
- def _get_fresh_credentials(iap_client_id: str) -> GoogleIDTokenCredentials:
80
-
81
- try:
82
- request = GoogleRequest()
83
- credentials: GoogleIDTokenCredentials = google_id_token_lib.fetch_id_token_credentials(
84
- iap_client_id, request
85
- ) # type: ignore
86
- credentials.refresh(request)
87
-
88
- except GoogleDefaultCredentialsError as ex:
89
- # The exceptions that google's libs raise in this case are somewhat vague; wrap them.
90
- raise ServiceAccountNoDefaultCredentials(
91
- message="Failed to get ServiceAccount token: Lacking default credentials.",
92
- google_exception=ex,
93
- )
94
- except GoogleRefreshError as ex:
95
- # Likely attempting to get a token for a service account in an environment that
96
- # doesn't have one attached.
97
- raise ServiceAccountTokenFailedRefresh(
98
- message="Failed to get ServiceAccount token: Refreshing token failed.",
99
- google_exception=ex,
100
- )
101
- return credentials
102
-
103
- @staticmethod
104
- def _get_fresh_token(iap_client_id: str) -> TokenStruct:
105
- google_credentials = ServiceAccount._get_fresh_credentials(iap_client_id)
106
- id_token: str = str(google_credentials.token)
107
-
108
- # Google lib uses deprecated 'utcfromtimestamp' func as of v2.29.x
109
- # e.g.: datetime.datetime.utcfromtimestamp(payload["exp"])
110
- # This creates a TZ-naive datetime in UTC from a POSIX timestamp.
111
- # Python datetimes assume local TZ, and we want to explicitly only work in UTC here.
112
- token_expiry = google_credentials.expiry.replace(tzinfo=datetime.timezone.utc)
113
-
114
- return TokenStruct(id_token=id_token, expiry=token_expiry)
115
-
116
- @staticmethod
117
- def get_token(
118
- iap_client_id: str, bypass_cached: bool = False, attempts: int = 0
119
- ) -> TokenRefreshStruct:
120
- """Retrieves an OIDC token for the current environment either from environment variable or from
121
- metadata service.
122
-
123
- 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
124
- to the path of a valid service account JSON file, then ID token is
125
- acquired using this service account credentials.
126
- 2. If the application is running in Compute Engine, App Engine or Cloud Run,
127
- then the ID token is obtained from the metadata server.
128
-
129
- Args:
130
- iap_client_id: The client ID used by IAP. Can be thought of as JWT audience.
131
-
132
- Returns:
133
- An OIDC token for use in connecting through IAP.
134
-
135
- Raises:
136
- :class:`ServiceAccountTokenException` if a token could not be retrieved due to either
137
- missing credentials from env-var/JSON or inability to talk to metadata server.
138
- """
139
-
140
- use_cache = not bypass_cached
141
-
142
- try:
143
- token_from_cache = False
144
- token_struct = (use_cache and ServiceAccount.get_stored_token(iap_client_id)) or None
145
- if use_cache and token_struct:
146
- token_from_cache = True
147
- else:
148
- token_struct = ServiceAccount._get_fresh_token(iap_client_id)
149
-
150
- ServiceAccount._store_token(iap_client_id, token_struct.id_token, token_struct.expiry)
151
-
152
- token_refresh_struct = TokenRefreshStruct(
153
- id_token=token_struct.id_token, token_is_new=not token_from_cache
154
- )
155
- return token_refresh_struct
156
-
157
- except ServiceAccountTokenException as ex:
158
- attempts += 1
159
- if attempts > MAX_RECURSE or not ex.retryable:
160
- raise
161
- return ServiceAccount.get_token(iap_client_id, bypass_cached=False, attempts=attempts)
162
-
163
- except TokenStorageException as ex:
164
- if attempts > 1:
165
- raise
166
- attempts += 1
167
- # Try again without involving the cache
168
- return ServiceAccount.get_token(iap_client_id, bypass_cached=True, attempts=attempts)
169
-
170
-
171
- class GoogleServiceAccount(ServiceAccount):
172
- """For interacting with Google service accounts and OIDC tokens for Google IAP"""
173
-
174
- def __init__(self, iap_client_id: str) -> None:
175
- if not iap_client_id or not isinstance(iap_client_id, str):
176
- raise ServiceAccountTokenException(
177
- "Invalid iap_client_id for GoogleServiceAccount", google_exception=None
178
- )
179
- self._iap_client_id = iap_client_id
180
- super().__init__()
181
-
182
- def get_stored_token(self) -> t.Optional[TokenStruct]:
183
- return ServiceAccount.get_stored_token(self._iap_client_id)
184
-
185
- def get_token(self, bypass_cached: bool = False, attempts: int = 0) -> TokenRefreshStruct:
186
- return ServiceAccount.get_token(
187
- iap_client_id=self._iap_client_id, bypass_cached=bypass_cached, attempts=attempts
188
- )
1
+ import datetime
2
+ import typing as t
3
+
4
+ from google.auth.compute_engine import IDTokenCredentials as GoogleIDTokenCredentials
5
+ from google.auth.exceptions import DefaultCredentialsError as GoogleDefaultCredentialsError
6
+ from google.auth.exceptions import RefreshError as GoogleRefreshError
7
+ from google.auth.transport.requests import Request as GoogleRequest
8
+ from google.oauth2 import id_token as google_id_token_lib
9
+
10
+ from kvcommon.logger import get_logger
11
+ from kvcommon.datastore.backend import DictBackend
12
+
13
+ from iaptoolkit.exceptions import ServiceAccountTokenException
14
+ from iaptoolkit.exceptions import ServiceAccountTokenFailedRefresh
15
+ from iaptoolkit.exceptions import ServiceAccountNoDefaultCredentials
16
+ from iaptoolkit.exceptions import TokenStorageException
17
+
18
+ from iaptoolkit.tokens.base import BaseTokenInterface
19
+ from iaptoolkit.tokens.structs import TokenStruct
20
+ from iaptoolkit.tokens.structs import TokenRefreshStruct
21
+
22
+ from .datastore_oidc import TokenDatastore_OIDC
23
+
24
+
25
+
26
+ LOG = get_logger("iaptk-oidc")
27
+ MAX_RECURSE = 3
28
+
29
+
30
+ class OIDC(BaseTokenInterface):
31
+ """
32
+ Base class for interacting with service accounts and OIDC tokens for IAP
33
+
34
+ # TODO: Move Google-specific logic to GoogleServiceAccount
35
+ """
36
+ _datastore: TokenDatastore_OIDC
37
+
38
+ def __init__(self, iap_client_id: str) -> None:
39
+ super().__init__(
40
+ datastore=TokenDatastore_OIDC(DictBackend),
41
+ iap_client_id=iap_client_id,
42
+ )
43
+
44
+ def _store_token(self, id_token: str, token_expiry: datetime.datetime):
45
+ self._datastore.store_token(self._iap_client_id, id_token, token_expiry)
46
+
47
+ def _get_stored_token(self) -> dict | None:
48
+ return self._datastore.get_stored_token(iap_client_id=self._iap_client_id)
49
+
50
+ def get_stored_token(self) -> TokenStruct | None:
51
+ return super().get_stored_token()
52
+
53
+ # TODO: Unstatic
54
+ @staticmethod
55
+ def _get_fresh_credentials(iap_client_id: str) -> GoogleIDTokenCredentials:
56
+
57
+ try:
58
+ request = GoogleRequest()
59
+ credentials: GoogleIDTokenCredentials = google_id_token_lib.fetch_id_token_credentials(
60
+ iap_client_id, request
61
+ ) # type: ignore
62
+ credentials.refresh(request)
63
+
64
+ except GoogleDefaultCredentialsError as ex:
65
+ # The exceptions that google's libs raise in this case are somewhat vague; wrap them.
66
+ raise ServiceAccountNoDefaultCredentials(
67
+ message="Failed to get ServiceAccount token: Lacking default credentials.",
68
+ google_exception=ex,
69
+ )
70
+ except GoogleRefreshError as ex:
71
+ # Likely attempting to get a token for a service account in an environment that
72
+ # doesn't have one attached.
73
+ raise ServiceAccountTokenFailedRefresh(
74
+ message="Failed to get ServiceAccount token: Refreshing token failed.",
75
+ google_exception=ex,
76
+ )
77
+ return credentials
78
+
79
+ # TODO: Unstatic
80
+ @staticmethod
81
+ def _get_fresh_token(iap_client_id: str) -> TokenStruct:
82
+ google_credentials = OIDC._get_fresh_credentials(iap_client_id)
83
+ id_token: str = str(google_credentials.token)
84
+
85
+ # Google lib uses deprecated 'utcfromtimestamp' func as of v2.29.x
86
+ # e.g.: datetime.datetime.utcfromtimestamp(payload["exp"])
87
+ # This creates a TZ-naive datetime in UTC from a POSIX timestamp.
88
+ # Python datetimes assume local TZ, and we want to explicitly only work in UTC here.
89
+ token_expiry = google_credentials.expiry.replace(tzinfo=datetime.timezone.utc)
90
+
91
+ return TokenStruct(id_token=id_token, expiry=token_expiry)
92
+
93
+ # TODO: Unstatic
94
+ def get_token(
95
+ self, iap_client_id: str, bypass_cached: bool = False, attempts: int = 0
96
+ ) -> TokenRefreshStruct:
97
+ """Retrieves an OIDC token for the current environment either from environment variable or from
98
+ metadata service.
99
+
100
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
101
+ to the path of a valid service account JSON file, then ID token is
102
+ acquired using this service account credentials.
103
+ 2. If the application is running in Compute Engine, App Engine or Cloud Run,
104
+ then the ID token is obtained from the metadata server.
105
+
106
+ Args:
107
+ iap_client_id: The client ID used by IAP. Can be thought of as JWT audience.
108
+ bypass_cached: Force retrieval of fresh tokens, bypassing in-memory cache
109
+ attempts: For recursive retries
110
+
111
+ Returns:
112
+ A struct containing:
113
+ id_token: An OIDC auth token for use in connecting through IAP
114
+ token_is_new: A bool indicating if the refresh token is new (i.e.; the previous had expired)
115
+
116
+ Raises:
117
+ :class:`ServiceAccountTokenException` if a token could not be retrieved due to either
118
+ missing credentials from env-var/JSON or inability to talk to metadata server.
119
+ """
120
+
121
+ use_cache = not bypass_cached
122
+
123
+ try:
124
+ token_from_cache = False
125
+ token_struct = (use_cache and self.get_stored_token()) or None
126
+ if use_cache and token_struct:
127
+ token_from_cache = True
128
+ else:
129
+ token_struct = OIDC._get_fresh_token(iap_client_id)
130
+
131
+ self._store_token(token_struct.id_token, token_struct.expiry)
132
+
133
+ token_refresh_struct = TokenRefreshStruct(
134
+ id_token=token_struct.id_token, token_is_new=not token_from_cache
135
+ )
136
+ return token_refresh_struct
137
+
138
+ except ServiceAccountTokenException as ex:
139
+ attempts += 1
140
+ if attempts > MAX_RECURSE or not ex.retryable:
141
+ raise
142
+ return self.get_token(iap_client_id, bypass_cached=False, attempts=attempts)
143
+
144
+ except TokenStorageException as ex:
145
+ if attempts > 1:
146
+ raise
147
+ attempts += 1
148
+ # Try again without involving the cache
149
+ return self.get_token(iap_client_id, bypass_cached=True, attempts=attempts)
@@ -0,0 +1,35 @@
1
+ import datetime
2
+ import typing as t
3
+
4
+ from kvcommon import logger
5
+
6
+ from iaptoolkit.tokens.token_datastore import TokenDatastore
7
+
8
+
9
+ LOG = logger.get_logger("iaptk-ds-oidc")
10
+
11
+
12
+ class TokenDatastore_OIDC(TokenDatastore):
13
+ _tokens_key: str = "oidc_tokens"
14
+
15
+ def get_stored_token(self, iap_client_id: str) -> dict | None:
16
+ tokens_dict = self.get_or_create_nested_dict(self._tokens_key)
17
+ token_struct_dict = self._retrieve_hashed_dict_entry(
18
+ target=tokens_dict, source_key=iap_client_id
19
+ )
20
+ if not token_struct_dict:
21
+ LOG.debug("No stored service account token for current iap_client_id")
22
+ return
23
+ return token_struct_dict
24
+
25
+ def store_token(self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
26
+ tokens_dict = self.get_or_create_nested_dict(self._tokens_key)
27
+
28
+ # TODO: Encode/encrypt token?
29
+ value = dict(id_token=id_token, token_expiry=token_expiry.isoformat())
30
+ self._insert_hashed_dict_entry(target=tokens_dict, source_key=iap_client_id, value=value)
31
+
32
+ try:
33
+ self.update_data(oidc_tokens=tokens_dict)
34
+ except OSError as ex:
35
+ LOG.error("Failed to store service account token for re-use. exception=%s", ex)
@@ -0,0 +1,11 @@
1
+ from . import OIDC
2
+
3
+
4
+ class GoogleServiceAccount(OIDC):
5
+ """
6
+ For interacting with Google service accounts and OIDC tokens for Google IAP
7
+
8
+ # TODO: Move Google-specific logic into this class
9
+ """
10
+
11
+ pass
@@ -12,6 +12,7 @@ LOG = logger.get_logger("iaptk")
12
12
  class TokenStruct:
13
13
  id_token: str
14
14
  expiry: datetime.datetime
15
+ token_is_new: bool = True
15
16
 
16
17
  @property
17
18
  def expired(self):
@@ -33,6 +34,8 @@ class TokenStruct:
33
34
 
34
35
  @dataclass(kw_only=True)
35
36
  class TokenRefreshStruct:
37
+ """
38
+ """
36
39
  id_token: str
37
40
  token_is_new: bool = True
38
41
 
@@ -40,10 +43,10 @@ class TokenRefreshStruct:
40
43
  @dataclass(kw_only=True)
41
44
  class TokenStructOAuth2(TokenStruct):
42
45
  refresh_token: str
43
- new_refresh_token: bool = False
46
+ token_is_new: bool = False
44
47
 
45
48
 
46
49
  @dataclass(kw_only=True)
47
50
  class ResultAddTokenHeader:
48
51
  token_added: bool
49
- token_is_fresh: bool
52
+ token_is_new: bool
@@ -0,0 +1,63 @@
1
+ import datetime
2
+ import hashlib
3
+ import typing as t
4
+ from abc import abstractmethod
5
+
6
+ from kvcommon import logger
7
+ from kvcommon.datastore.backend import DatastoreBackend
8
+ from kvcommon.datastore import VersionedDatastore
9
+
10
+ from iaptoolkit.constants import IAPTOOLKIT_CONFIG_VERSION
11
+
12
+
13
+ LOG = logger.get_logger("iaptk-ds")
14
+
15
+
16
+ class TokenDatastore(VersionedDatastore):
17
+ _tokens_key: str
18
+
19
+ def __init__(self, backend: DatastoreBackend | type[DatastoreBackend]) -> None:
20
+ super().__init__(backend=backend, config_version=IAPTOOLKIT_CONFIG_VERSION)
21
+ self._ensure_tokens_dict()
22
+
23
+ def _migrate_version(self):
24
+ # Override
25
+ self.discard_existing_tokens()
26
+ return super()._migrate_version()
27
+
28
+ def _ensure_tokens_dict(self):
29
+ tokens_dict = self.get_or_create_nested_dict("tokens")
30
+ if "refresh" not in tokens_dict.keys():
31
+ tokens_dict["refresh"] = None
32
+ self.set_value("tokens", tokens_dict)
33
+
34
+ @staticmethod
35
+ def _insert_hashed_dict_entry(target: dict, source_key: str, value: t.Any):
36
+ hash_obj = hashlib.sha256(source_key.encode("utf-8"))
37
+ hex_digest = hash_obj.hexdigest()
38
+ target[hex_digest] = value
39
+
40
+ @staticmethod
41
+ def _retrieve_hashed_dict_entry(target: dict, source_key: str) -> t.Any:
42
+ hash_obj = hashlib.sha256(source_key.encode("utf-8"))
43
+ hex_digest = hash_obj.hexdigest()
44
+ # TODO: Check collisions/pre-existing keys?
45
+ target.get(hex_digest)
46
+
47
+ def discard_existing_tokens(self):
48
+ LOG.debug("Discarding existing tokens.")
49
+ self.update_data(tokens={})
50
+
51
+ @abstractmethod
52
+ def get_stored_token(self, iap_client_id: str) -> dict | None:
53
+ raise NotImplementedError
54
+
55
+ @abstractmethod
56
+ def store_token(self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
57
+ raise NotImplementedError
58
+
59
+
60
+ # datastore = TokenDatastore(DictBackend)
61
+
62
+ # if PERSISTENT_DATASTORE_ENABLED:
63
+ # datastore_toml = TokenDatastore(TOMLBackend(PERSISTENT_DATASTORE_PATH, PERSISTENT_DATASTORE_USERNAME))
File without changes
@@ -1,24 +0,0 @@
1
- from kvcommon import logger
2
-
3
- from iaptoolkit.exceptions import ServiceAccountTokenException
4
- from iaptoolkit.exceptions import TokenStorageException
5
- from iaptoolkit.exceptions import TokenException
6
-
7
- from .structs import TokenStruct
8
- from .structs import TokenRefreshStruct
9
-
10
- # from .structs import TokenStructOAuth2 # TODO: OAuth2
11
- # from .oauth2 import get_token_for_oauth2 # TODO: OAuth2
12
- # from .service_account import ServiceAccount
13
- from .service_account import GoogleServiceAccount
14
-
15
- LOG = logger.get_logger("iaptk")
16
-
17
-
18
- __all__ = [
19
- "TokenStruct",
20
- "TokenRefreshStruct",
21
- # "TokenStructOAuth2", # TODO: OAuth2
22
- "TokenException",
23
- "TokenStorageException",
24
- ]
@@ -1,68 +0,0 @@
1
- import datetime
2
- import typing as t
3
-
4
- from kvcommon import logger
5
- from kvcommon.datastore.backend import DatastoreBackend
6
- from kvcommon.datastore.backend import DictBackend
7
- # from kvcommon.datastore.backend import TOMLBackend
8
- from kvcommon.datastore import VersionedDatastore
9
-
10
- from iaptoolkit.constants import IAPTOOLKIT_CONFIG_VERSION
11
-
12
-
13
- LOG = logger.get_logger("iaptk-ds")
14
-
15
-
16
- class TokenDatastore(VersionedDatastore):
17
- _service_account_tokens_key = "service_account_tokens"
18
-
19
- def __init__(self, backend: DatastoreBackend | type[DatastoreBackend]) -> None:
20
- super().__init__(backend=backend, config_version=IAPTOOLKIT_CONFIG_VERSION)
21
- self._ensure_tokens_dict()
22
-
23
- def _ensure_tokens_dict(self):
24
- tokens_dict = self.get_or_create_nested_dict("tokens")
25
- if "refresh" not in tokens_dict.keys():
26
- tokens_dict["refresh"] = None
27
- self.set_value("tokens", tokens_dict)
28
-
29
- def discard_existing_tokens(self):
30
- LOG.debug("Discarding existing tokens.")
31
- self.update_data(tokens={})
32
-
33
- def get_stored_service_account_token(self, iap_client_id: str) -> t.Optional[dict]:
34
- tokens_dict = self.get_or_create_nested_dict(self._service_account_tokens_key)
35
- token_struct_dict = tokens_dict.get(iap_client_id, None)
36
- if not token_struct_dict:
37
- LOG.debug("No stored service account token for current iap_client_id")
38
- return
39
- return token_struct_dict
40
-
41
- def store_service_account_token(
42
- self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime
43
- ):
44
- tokens_dict = self.get_or_create_nested_dict(self._service_account_tokens_key)
45
- tokens_dict[iap_client_id] = dict(id_token=id_token, token_expiry=token_expiry.isoformat())
46
-
47
- try:
48
- self.update_data(service_account_tokens=tokens_dict)
49
- except OSError as ex:
50
- LOG.error("Failed to store service account token for re-use. exception=%s", ex)
51
-
52
- def _migrate_version(self):
53
- # Override
54
- self.discard_existing_tokens()
55
- return super()._migrate_version()
56
-
57
- # def get_stored_oauth2_token(self, iap_client_id: str):
58
- # # TODO: OAuth2
59
- # raise NotImplementedError()
60
-
61
- # def store_oauth2_token(self, iap_client_id: str):
62
- # # TODO: OAuth2
63
- # raise NotImplementedError()
64
-
65
- datastore = TokenDatastore(DictBackend)
66
-
67
- # if PERSISTENT_DATASTORE_ENABLED:
68
- # datastore_toml = TokenDatastore(TOMLBackend(PERSISTENT_DATASTORE_PATH, PERSISTENT_DATASTORE_USERNAME))
File without changes