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.
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/PKG-INFO +4 -4
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/README.md +2 -2
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/pyproject.toml +2 -2
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/__init__.py +64 -78
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/exceptions.py +20 -3
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/headers.py +3 -1
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/base.py +68 -0
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/__init__.py +110 -0
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/datastore_oauth2.py +39 -0
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oauth2/gua.py +11 -0
- iaptoolkit-0.2.2/src/iaptoolkit/tokens/service_account.py → iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/__init__.py +149 -188
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/datastore_oidc.py +35 -0
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/oidc/gsa.py +11 -0
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/tokens/structs.py +5 -2
- iaptoolkit-0.3.0a0/src/iaptoolkit/tokens/token_datastore.py +63 -0
- iaptoolkit-0.3.0a0/src/iaptoolkit/utils/__init__.py +0 -0
- iaptoolkit-0.2.2/src/iaptoolkit/tokens/__init__.py +0 -24
- iaptoolkit-0.2.2/src/iaptoolkit/tokens/token_datastore.py +0 -68
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/LICENSE +0 -0
- {iaptoolkit-0.2.2 → iaptoolkit-0.3.0a0}/src/iaptoolkit/constants.py +0 -0
- {iaptoolkit-0.2.2/src/iaptoolkit/utils → iaptoolkit-0.3.0a0/src/iaptoolkit/tokens}/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
37
|
+
from iaptoolkit import IAPToolkit_OIDC
|
|
38
38
|
|
|
39
|
-
|
|
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
|
|
20
|
+
from iaptoolkit import IAPToolkit_OIDC
|
|
21
21
|
|
|
22
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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 =
|
|
23
|
+
LOG = get_logger("iaptk")
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
class IAPToolkit:
|
|
26
|
+
class IAPToolkit(ABC):
|
|
25
27
|
"""
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
59
|
-
struct = self.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
123
|
+
return ResultAddTokenHeader(token_added=False, token_is_new=False)
|
|
135
124
|
|
|
136
125
|
|
|
137
126
|
class IAPToolkit_OIDC(IAPToolkit):
|
|
138
127
|
"""
|
|
139
|
-
|
|
128
|
+
OIDC-only implementation of IAPToolkit
|
|
140
129
|
"""
|
|
130
|
+
_interface: OIDC
|
|
141
131
|
|
|
142
|
-
def
|
|
143
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
166
|
+
def get_refresh_token(self, bypass_cached: bool = False) -> t.Any:
|
|
167
|
+
pass
|
|
191
168
|
|
|
192
|
-
def
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from iaptoolkit.
|
|
14
|
-
from iaptoolkit.exceptions import
|
|
15
|
-
from iaptoolkit.exceptions import
|
|
16
|
-
from iaptoolkit.exceptions import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from .structs import TokenStruct
|
|
20
|
-
from .structs import TokenRefreshStruct
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
{iaptoolkit-0.2.2/src/iaptoolkit/utils → iaptoolkit-0.3.0a0/src/iaptoolkit/tokens}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|