iaptoolkit 0.2.6__tar.gz → 0.3.0__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.6 → iaptoolkit-0.3.0}/PKG-INFO +2 -1
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/pyproject.toml +1 -1
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/__init__.py +47 -15
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/tokens/service_account.py +20 -20
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/tokens/structs.py +21 -2
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/LICENSE +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/README.md +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/constants.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/exceptions.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/headers.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/tokens/__init__.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/tokens/token_datastore.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/src/iaptoolkit/utils/__init__.py +0 -0
- {iaptoolkit-0.2.6 → iaptoolkit-0.3.0}/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.0
|
|
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
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.11,<4.0
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
12
|
Requires-Dist: google-auth (>=2.29.0,<3.0.0)
|
|
12
13
|
Requires-Dist: kvcommon (>=0.1.3,<0.2.0)
|
|
13
14
|
Requires-Dist: pytest (>=7.4.4,<8.0.0)
|
|
@@ -16,6 +16,7 @@ from iaptoolkit.tokens.service_account import ServiceAccount
|
|
|
16
16
|
from iaptoolkit.tokens.structs import ResultAddTokenHeader
|
|
17
17
|
|
|
18
18
|
from iaptoolkit.tokens.structs import TokenRefreshStruct
|
|
19
|
+
from iaptoolkit.tokens.structs import TokenStruct
|
|
19
20
|
from iaptoolkit.utils.urls import is_url_safe_for_token
|
|
20
21
|
|
|
21
22
|
LOG = logger.get_logger("iaptk")
|
|
@@ -38,11 +39,9 @@ class IAPToolkit:
|
|
|
38
39
|
def sanitize_request_headers(request_headers: dict) -> dict:
|
|
39
40
|
return headers.sanitize_request_headers(request_headers)
|
|
40
41
|
|
|
41
|
-
def get_token_oidc(self, bypass_cached: bool = False) ->
|
|
42
|
+
def get_token_oidc(self, bypass_cached: bool = False) -> TokenStruct:
|
|
42
43
|
try:
|
|
43
|
-
return ServiceAccount.get_token(
|
|
44
|
-
iap_client_id=self._GOOGLE_IAP_CLIENT_ID, bypass_cached=bypass_cached
|
|
45
|
-
)
|
|
44
|
+
return ServiceAccount.get_token(iap_client_id=self._GOOGLE_IAP_CLIENT_ID, bypass_cached=bypass_cached)
|
|
46
45
|
except ServiceAccountTokenException as ex:
|
|
47
46
|
LOG.debug(ex)
|
|
48
47
|
raise
|
|
@@ -60,7 +59,11 @@ class IAPToolkit:
|
|
|
60
59
|
return struct.id_token
|
|
61
60
|
|
|
62
61
|
def get_token_and_add_to_headers(
|
|
63
|
-
self,
|
|
62
|
+
self,
|
|
63
|
+
request_headers: dict,
|
|
64
|
+
use_oauth2: bool = False,
|
|
65
|
+
use_auth_header: bool = False,
|
|
66
|
+
bypass_cached: bool = False,
|
|
64
67
|
) -> bool:
|
|
65
68
|
"""
|
|
66
69
|
Retrieves an auth token and inserts it into the supplied request_headers dict.
|
|
@@ -72,20 +75,29 @@ class IAPToolkit:
|
|
|
72
75
|
As a general guideline, OIDC is the assumed default approach for ServiceAccounts.
|
|
73
76
|
use_auth_header: If true, use the 'Authorization' header instead of 'Proxy-Authorization'
|
|
74
77
|
|
|
78
|
+
Returns:
|
|
79
|
+
True if token retrieved from cache, False if fresh from API
|
|
80
|
+
|
|
75
81
|
|
|
76
82
|
"""
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
id_token = None
|
|
84
|
+
from_cache = False
|
|
85
|
+
if use_oauth2:
|
|
86
|
+
token_refresh_struct: TokenRefreshStruct = self.get_token_oauth2(bypass_cached=bypass_cached)
|
|
87
|
+
id_token = token_refresh_struct.id_token
|
|
88
|
+
from_cache = token_refresh_struct.from_cache
|
|
79
89
|
else:
|
|
80
|
-
|
|
90
|
+
token_struct: TokenStruct = self.get_token_oidc(bypass_cached=bypass_cached)
|
|
91
|
+
id_token = token_struct.id_token
|
|
92
|
+
from_cache = token_struct.from_cache
|
|
81
93
|
|
|
82
94
|
headers.add_token_to_request_headers(
|
|
83
95
|
request_headers=request_headers,
|
|
84
|
-
id_token=
|
|
96
|
+
id_token=id_token,
|
|
85
97
|
use_auth_header=use_auth_header,
|
|
86
98
|
)
|
|
87
99
|
|
|
88
|
-
return
|
|
100
|
+
return from_cache
|
|
89
101
|
|
|
90
102
|
@staticmethod
|
|
91
103
|
def is_url_safe_for_token(
|
|
@@ -104,6 +116,7 @@ class IAPToolkit:
|
|
|
104
116
|
valid_domains: t.List[str] | None = None,
|
|
105
117
|
use_oauth2: bool = False,
|
|
106
118
|
use_auth_header: bool = False,
|
|
119
|
+
bypass_cached: bool = False,
|
|
107
120
|
) -> ResultAddTokenHeader:
|
|
108
121
|
"""
|
|
109
122
|
Checks that the supplied URL is valid (i.e.; in valid_domains) and if so, retrieves a
|
|
@@ -123,10 +136,11 @@ class IAPToolkit:
|
|
|
123
136
|
request_headers=request_headers,
|
|
124
137
|
use_oauth2=use_oauth2,
|
|
125
138
|
use_auth_header=use_auth_header,
|
|
139
|
+
bypass_cached=bypass_cached,
|
|
126
140
|
)
|
|
127
141
|
return ResultAddTokenHeader(token_added=True, token_is_fresh=token_is_fresh)
|
|
128
142
|
else:
|
|
129
|
-
LOG.
|
|
143
|
+
LOG.warning(
|
|
130
144
|
"URL is not approved: %s - Token will not be added to headers. Valid domains are: %s",
|
|
131
145
|
url,
|
|
132
146
|
valid_domains,
|
|
@@ -146,10 +160,17 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
146
160
|
raise NotImplementedError("Cannot call OAuth2 methods on OIDC-only instance of IAPToolkit.")
|
|
147
161
|
|
|
148
162
|
def get_token_and_add_to_headers(
|
|
149
|
-
self,
|
|
163
|
+
self,
|
|
164
|
+
request_headers: dict,
|
|
165
|
+
use_auth_header: bool = False,
|
|
166
|
+
use_oauth2: bool = False,
|
|
167
|
+
bypass_cached: bool = False,
|
|
150
168
|
) -> bool:
|
|
151
169
|
return super().get_token_and_add_to_headers(
|
|
152
|
-
request_headers=request_headers,
|
|
170
|
+
request_headers=request_headers,
|
|
171
|
+
use_oauth2=use_oauth2,
|
|
172
|
+
use_auth_header=use_auth_header,
|
|
173
|
+
bypass_cached=bypass_cached,
|
|
153
174
|
)
|
|
154
175
|
|
|
155
176
|
def check_url_and_add_token_header(
|
|
@@ -158,6 +179,7 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
158
179
|
request_headers: dict,
|
|
159
180
|
valid_domains: t.List[str] | None = None,
|
|
160
181
|
use_auth_header: bool = False,
|
|
182
|
+
bypass_cached: bool = False,
|
|
161
183
|
) -> ResultAddTokenHeader:
|
|
162
184
|
return super().check_url_and_add_token_header(
|
|
163
185
|
url,
|
|
@@ -165,6 +187,7 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
165
187
|
valid_domains=valid_domains,
|
|
166
188
|
use_oauth2=False,
|
|
167
189
|
use_auth_header=use_auth_header,
|
|
190
|
+
bypass_cached=bypass_cached,
|
|
168
191
|
)
|
|
169
192
|
|
|
170
193
|
|
|
@@ -193,10 +216,17 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
193
216
|
raise NotImplementedError("Cannot call OIDC methods on OAuth2-only instance of IAPToolkit.")
|
|
194
217
|
|
|
195
218
|
def get_token_and_add_to_headers(
|
|
196
|
-
self,
|
|
219
|
+
self,
|
|
220
|
+
request_headers: dict,
|
|
221
|
+
use_auth_header: bool = False,
|
|
222
|
+
use_oauth2: bool = True,
|
|
223
|
+
bypass_cached: bool = False,
|
|
197
224
|
) -> bool:
|
|
198
225
|
return super().get_token_and_add_to_headers(
|
|
199
|
-
request_headers=request_headers,
|
|
226
|
+
request_headers=request_headers,
|
|
227
|
+
use_oauth2=use_oauth2,
|
|
228
|
+
use_auth_header=use_auth_header,
|
|
229
|
+
bypass_cached=bypass_cached,
|
|
200
230
|
)
|
|
201
231
|
|
|
202
232
|
def check_url_and_add_token_header(
|
|
@@ -205,6 +235,7 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
205
235
|
request_headers: dict,
|
|
206
236
|
valid_domains: t.List[str] | None = None,
|
|
207
237
|
use_auth_header: bool = False,
|
|
238
|
+
bypass_cached: bool = False,
|
|
208
239
|
) -> ResultAddTokenHeader:
|
|
209
240
|
return super().check_url_and_add_token_header(
|
|
210
241
|
url=url,
|
|
@@ -212,4 +243,5 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
212
243
|
valid_domains=valid_domains,
|
|
213
244
|
use_oauth2=True,
|
|
214
245
|
use_auth_header=use_auth_header,
|
|
246
|
+
bypass_cached=bypass_cached,
|
|
215
247
|
)
|
|
@@ -46,24 +46,24 @@ class ServiceAccount(object):
|
|
|
46
46
|
LOG.debug("No stored service account token for current iap_client_id")
|
|
47
47
|
return
|
|
48
48
|
|
|
49
|
-
id_token_from_dict = token_dict.get("id_token")
|
|
50
|
-
token_expiry_from_dict = token_dict.get("token_expiry", "")
|
|
51
|
-
|
|
52
|
-
if not id_token_from_dict:
|
|
53
|
-
LOG.warning("Invalid stored ID token")
|
|
54
|
-
return
|
|
49
|
+
id_token_from_dict: str = token_dict.get("id_token", "")
|
|
50
|
+
token_expiry_from_dict: str = token_dict.get("token_expiry", "")
|
|
55
51
|
|
|
56
52
|
token_expiry = ""
|
|
57
53
|
try:
|
|
58
54
|
token_expiry = datetime.datetime.fromisoformat(token_expiry_from_dict)
|
|
59
55
|
except (ValueError, TypeError) as ex:
|
|
60
|
-
LOG.debug("Invalid token expiry for
|
|
56
|
+
LOG.debug("Invalid token expiry for stored token - Could not parse from ISO format to datetime.")
|
|
61
57
|
return
|
|
62
58
|
|
|
63
|
-
token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry)
|
|
59
|
+
token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry, from_cache=True)
|
|
60
|
+
if not token_struct.valid:
|
|
61
|
+
LOG.debug("Stored service account token for current iap_client_id is INVALID")
|
|
62
|
+
return
|
|
64
63
|
if token_struct.expired:
|
|
65
64
|
LOG.debug("Stored service account token for current iap_client_id has EXPIRED")
|
|
66
65
|
return
|
|
66
|
+
|
|
67
67
|
return token_struct
|
|
68
68
|
|
|
69
69
|
except Exception as ex:
|
|
@@ -108,10 +108,10 @@ class ServiceAccount(object):
|
|
|
108
108
|
# Python datetimes assume local TZ, and we want to explicitly only work in UTC here.
|
|
109
109
|
token_expiry = google_credentials.expiry.replace(tzinfo=datetime.timezone.utc)
|
|
110
110
|
|
|
111
|
-
return TokenStruct(id_token=id_token, expiry=token_expiry)
|
|
111
|
+
return TokenStruct(id_token=id_token, expiry=token_expiry, from_cache=False)
|
|
112
112
|
|
|
113
113
|
@staticmethod
|
|
114
|
-
def get_token(iap_client_id: str, bypass_cached: bool = False, attempts: int = 0) ->
|
|
114
|
+
def get_token(iap_client_id: str, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
115
115
|
"""Retrieves an OIDC token for the current environment either from environment variable or from
|
|
116
116
|
metadata service.
|
|
117
117
|
|
|
@@ -135,17 +135,17 @@ class ServiceAccount(object):
|
|
|
135
135
|
use_cache = not bypass_cached
|
|
136
136
|
|
|
137
137
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if use_cache
|
|
141
|
-
|
|
142
|
-
else:
|
|
143
|
-
token_struct = ServiceAccount._get_fresh_token(iap_client_id)
|
|
138
|
+
token_struct: TokenStruct | None = None
|
|
139
|
+
|
|
140
|
+
if use_cache:
|
|
141
|
+
token_struct = ServiceAccount.get_stored_token(iap_client_id)
|
|
144
142
|
|
|
145
|
-
|
|
143
|
+
if not token_struct:
|
|
144
|
+
token_struct = ServiceAccount._get_fresh_token(iap_client_id)
|
|
145
|
+
if use_cache:
|
|
146
|
+
ServiceAccount._store_token(iap_client_id, token_struct.id_token, token_struct.expiry)
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
return token_refresh_struct
|
|
148
|
+
return token_struct
|
|
149
149
|
|
|
150
150
|
except ServiceAccountTokenException as ex:
|
|
151
151
|
attempts += 1
|
|
@@ -173,7 +173,7 @@ class GoogleServiceAccount(ServiceAccount):
|
|
|
173
173
|
def get_stored_token(self) -> t.Optional[TokenStruct]:
|
|
174
174
|
return ServiceAccount.get_stored_token(self._iap_client_id)
|
|
175
175
|
|
|
176
|
-
def get_token(self, bypass_cached: bool = False, attempts: int = 0) ->
|
|
176
|
+
def get_token(self, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
177
177
|
return ServiceAccount.get_token(
|
|
178
178
|
iap_client_id=self._iap_client_id, bypass_cached=bypass_cached, attempts=attempts
|
|
179
179
|
)
|
|
@@ -8,10 +8,18 @@ from kvcommon import logger
|
|
|
8
8
|
LOG = logger.get_logger("iaptk")
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def validate_token(token: str | None) -> bool:
|
|
12
|
+
if not isinstance(token, str) or token.strip() == "":
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
|
|
11
18
|
@dataclass(kw_only=True)
|
|
12
19
|
class TokenStruct:
|
|
13
20
|
id_token: str
|
|
14
21
|
expiry: datetime.datetime
|
|
22
|
+
from_cache: bool = False
|
|
15
23
|
|
|
16
24
|
@property
|
|
17
25
|
def expired(self):
|
|
@@ -30,17 +38,28 @@ class TokenStruct:
|
|
|
30
38
|
LOG.error("Exception when checking token expiry. exception=%s", ex)
|
|
31
39
|
return True
|
|
32
40
|
|
|
41
|
+
@property
|
|
42
|
+
def valid(self):
|
|
43
|
+
return validate_token(self.id_token)
|
|
44
|
+
|
|
33
45
|
|
|
34
46
|
@dataclass(kw_only=True)
|
|
35
47
|
class TokenRefreshStruct:
|
|
36
48
|
id_token: str
|
|
37
|
-
|
|
49
|
+
from_cache: bool = False
|
|
38
50
|
|
|
51
|
+
@property
|
|
52
|
+
def valid(self):
|
|
53
|
+
return validate_token(self.id_token)
|
|
39
54
|
|
|
40
55
|
@dataclass(kw_only=True)
|
|
41
56
|
class TokenStructOAuth2(TokenStruct):
|
|
42
57
|
refresh_token: str
|
|
43
|
-
|
|
58
|
+
from_cache: bool = False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def valid(self):
|
|
62
|
+
return validate_token(self.refresh_token)
|
|
44
63
|
|
|
45
64
|
|
|
46
65
|
@dataclass(kw_only=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|