iaptoolkit 0.3.9__tar.gz → 0.4.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.3.9 → iaptoolkit-0.4.0}/PKG-INFO +46 -5
- iaptoolkit-0.4.0/README.md +98 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/pyproject.toml +3 -2
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/__init__.py +139 -30
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/exceptions.py +11 -2
- iaptoolkit-0.4.0/src/iaptoolkit/tokens/service_account.py +276 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/tokens/structs.py +6 -0
- iaptoolkit-0.4.0/src/iaptoolkit/tokens/token_datastore.py +133 -0
- iaptoolkit-0.3.9/README.md +0 -60
- iaptoolkit-0.3.9/src/iaptoolkit/tokens/service_account.py +0 -186
- iaptoolkit-0.3.9/src/iaptoolkit/tokens/token_datastore.py +0 -72
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/LICENSE +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/constants.py +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/headers.py +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/tokens/__init__.py +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/utils/__init__.py +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/utils/urls.py +0 -0
- {iaptoolkit-0.3.9 → iaptoolkit-0.4.0}/src/iaptoolkit/utils/verify.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: iaptoolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Library of common utils for interacting with Identity-Aware Proxies
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Author: Rob Voigt
|
|
6
7
|
Author-email: code@ravoigt.com
|
|
7
8
|
Requires-Python: >=3.11,<4.0
|
|
@@ -9,8 +10,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
14
|
Requires-Dist: google-auth (>=2.29.0,<3.0.0)
|
|
13
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: google-cloud-iam (>=2.20.0,<3.0.0)
|
|
16
|
+
Requires-Dist: kvcommon[k8s] (>=0.4.5,<0.5.0)
|
|
14
17
|
Requires-Dist: requests (>=2.32.4)
|
|
15
18
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
16
19
|
Project-URL: Homepage, https://github.com/RAVoigt/iaptoolkit
|
|
@@ -48,6 +51,7 @@ def example1(url: str):
|
|
|
48
51
|
result = iaptk.check_url_and_add_token_header(
|
|
49
52
|
url=url,
|
|
50
53
|
request_headers=headers,
|
|
54
|
+
iap_audience="some_iap_client_id_string" # OAuth Client ID for the IAP-protected resource as 'audience'
|
|
51
55
|
valid_domains=allowed_domains
|
|
52
56
|
)
|
|
53
57
|
# result.token_added (bool) indicates if the token was added, depending on whether or not URL was valid
|
|
@@ -58,20 +62,57 @@ def example1(url: str):
|
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
# Example #2 - Separate Calls - Functionally the same as Example 1 but more flexibility in URL validation
|
|
61
|
-
def
|
|
65
|
+
def example2(url: str):
|
|
62
66
|
is_url_safe: bool = iaptk.is_url_safe_for_token(url=url, valid_domains=valid_domains)
|
|
63
67
|
|
|
64
68
|
if not is_url_safe:
|
|
65
69
|
raise ExampleBadURLException("This URL isn't safe to send token headers to!")
|
|
66
70
|
|
|
67
71
|
headers = dict()
|
|
68
|
-
token_is_fresh: bool = iaptk.get_token_and_add_to_headers(
|
|
72
|
+
token_is_fresh: bool = iaptk.get_token_and_add_to_headers(
|
|
73
|
+
request_headers=headers,
|
|
74
|
+
iap_audience="some_iap_client_id_string" # OAuth Client ID for the IAP-protected resource as 'audience'
|
|
75
|
+
)
|
|
69
76
|
# token_is_fresh indicates if token was newly retrieved (True), or if a cached token was reused (False)
|
|
70
77
|
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
71
78
|
|
|
72
79
|
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
73
80
|
response = requests.request("GET", url, headers=headers)
|
|
74
81
|
|
|
82
|
+
# Example #3 - Service Account JWT (instead of OIDC Token)
|
|
83
|
+
def example3(url: str):
|
|
84
|
+
headers = dict()
|
|
85
|
+
result = iaptk.check_url_and_add_jwt_header(
|
|
86
|
+
url=url,
|
|
87
|
+
request_headers=headers,
|
|
88
|
+
service_account_email="service-account@PROJECT_ID.iam.gserviceaccount.com",
|
|
89
|
+
url_audience="https://some-iap-protected.resource/path",
|
|
90
|
+
valid_domains=allowed_domains
|
|
91
|
+
)
|
|
92
|
+
# result.token_added (bool) indicates if the token was added, depending on whether or not URL was valid
|
|
93
|
+
# headers dict now contains the appropriate Bearer JWT header for Google IAP
|
|
94
|
+
|
|
95
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
96
|
+
response = requests.request("GET", url, headers=headers)
|
|
97
|
+
|
|
98
|
+
# Example #4 - Separate Calls - Service Account JWT - Functionally the same as Example 3 but more flexibility in URL validation
|
|
99
|
+
def example4(url: str):
|
|
100
|
+
is_url_safe: bool = iaptk.is_url_safe_for_token(url=url, valid_domains=valid_domains)
|
|
101
|
+
|
|
102
|
+
if not is_url_safe:
|
|
103
|
+
raise ExampleBadURLException("This URL isn't safe to send token headers to!")
|
|
104
|
+
|
|
105
|
+
headers = dict()
|
|
106
|
+
token_is_fresh: bool = iaptk.get_jwt_and_add_to_headers(
|
|
107
|
+
request_headers=headers,
|
|
108
|
+
service_account_email="service-account@PROJECT_ID.iam.gserviceaccount.com",
|
|
109
|
+
url_audience="https://some-iap-protected.resource/path"
|
|
110
|
+
)
|
|
111
|
+
# token_is_fresh indicates if token was newly retrieved (True), or if a cached token was reused (False)
|
|
112
|
+
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
113
|
+
|
|
114
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
115
|
+
response = requests.request("GET", url, headers=headers)
|
|
75
116
|
```
|
|
76
117
|
|
|
77
118
|
## Disclaimer
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# IAP Toolkit
|
|
2
|
+
|
|
3
|
+
A library of utils to ease programmatic authentication with Google IAP (and ideally other IAPs in future).
|
|
4
|
+
|
|
5
|
+
# PyPi
|
|
6
|
+
https://pypi.org/project/iaptoolkit/
|
|
7
|
+
|
|
8
|
+
# Installation
|
|
9
|
+
### With Poetry:
|
|
10
|
+
`poetry add iaptoolkit`
|
|
11
|
+
|
|
12
|
+
### With pip:
|
|
13
|
+
`pip install iaptoolkit`
|
|
14
|
+
|
|
15
|
+
## Quick Start / Example Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from iaptoolkit import IAPToolkit
|
|
21
|
+
|
|
22
|
+
iaptk = IAPToolkit(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
|
|
23
|
+
allowed_domains = ["example.com", ]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Example #1 - Combined Calls
|
|
27
|
+
def example1(url: str):
|
|
28
|
+
headers = dict()
|
|
29
|
+
result = iaptk.check_url_and_add_token_header(
|
|
30
|
+
url=url,
|
|
31
|
+
request_headers=headers,
|
|
32
|
+
iap_audience="some_iap_client_id_string" # OAuth Client ID for the IAP-protected resource as 'audience'
|
|
33
|
+
valid_domains=allowed_domains
|
|
34
|
+
)
|
|
35
|
+
# result.token_added (bool) indicates if the token was added, depending on whether or not URL was valid
|
|
36
|
+
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
37
|
+
|
|
38
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
39
|
+
response = requests.request("GET", url, headers=headers)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Example #2 - Separate Calls - Functionally the same as Example 1 but more flexibility in URL validation
|
|
43
|
+
def example2(url: str):
|
|
44
|
+
is_url_safe: bool = iaptk.is_url_safe_for_token(url=url, valid_domains=valid_domains)
|
|
45
|
+
|
|
46
|
+
if not is_url_safe:
|
|
47
|
+
raise ExampleBadURLException("This URL isn't safe to send token headers to!")
|
|
48
|
+
|
|
49
|
+
headers = dict()
|
|
50
|
+
token_is_fresh: bool = iaptk.get_token_and_add_to_headers(
|
|
51
|
+
request_headers=headers,
|
|
52
|
+
iap_audience="some_iap_client_id_string" # OAuth Client ID for the IAP-protected resource as 'audience'
|
|
53
|
+
)
|
|
54
|
+
# token_is_fresh indicates if token was newly retrieved (True), or if a cached token was reused (False)
|
|
55
|
+
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
56
|
+
|
|
57
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
58
|
+
response = requests.request("GET", url, headers=headers)
|
|
59
|
+
|
|
60
|
+
# Example #3 - Service Account JWT (instead of OIDC Token)
|
|
61
|
+
def example3(url: str):
|
|
62
|
+
headers = dict()
|
|
63
|
+
result = iaptk.check_url_and_add_jwt_header(
|
|
64
|
+
url=url,
|
|
65
|
+
request_headers=headers,
|
|
66
|
+
service_account_email="service-account@PROJECT_ID.iam.gserviceaccount.com",
|
|
67
|
+
url_audience="https://some-iap-protected.resource/path",
|
|
68
|
+
valid_domains=allowed_domains
|
|
69
|
+
)
|
|
70
|
+
# result.token_added (bool) indicates if the token was added, depending on whether or not URL was valid
|
|
71
|
+
# headers dict now contains the appropriate Bearer JWT header for Google IAP
|
|
72
|
+
|
|
73
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
74
|
+
response = requests.request("GET", url, headers=headers)
|
|
75
|
+
|
|
76
|
+
# Example #4 - Separate Calls - Service Account JWT - Functionally the same as Example 3 but more flexibility in URL validation
|
|
77
|
+
def example4(url: str):
|
|
78
|
+
is_url_safe: bool = iaptk.is_url_safe_for_token(url=url, valid_domains=valid_domains)
|
|
79
|
+
|
|
80
|
+
if not is_url_safe:
|
|
81
|
+
raise ExampleBadURLException("This URL isn't safe to send token headers to!")
|
|
82
|
+
|
|
83
|
+
headers = dict()
|
|
84
|
+
token_is_fresh: bool = iaptk.get_jwt_and_add_to_headers(
|
|
85
|
+
request_headers=headers,
|
|
86
|
+
service_account_email="service-account@PROJECT_ID.iam.gserviceaccount.com",
|
|
87
|
+
url_audience="https://some-iap-protected.resource/path"
|
|
88
|
+
)
|
|
89
|
+
# token_is_fresh indicates if token was newly retrieved (True), or if a cached token was reused (False)
|
|
90
|
+
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
91
|
+
|
|
92
|
+
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
93
|
+
response = requests.request("GET", url, headers=headers)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Disclaimer
|
|
97
|
+
|
|
98
|
+
This project is not affiliated with Google. No trademark infringement intended.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "iaptoolkit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
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"
|
|
@@ -23,9 +23,10 @@ include = '\.pyi?$'
|
|
|
23
23
|
[tool.poetry.dependencies]
|
|
24
24
|
python = "^3.11"
|
|
25
25
|
google-auth = "^2.29.0"
|
|
26
|
+
google-cloud-iam = "^2.20.0"
|
|
26
27
|
requests = ">=2.32.4"
|
|
27
28
|
toml = "^0.10.2"
|
|
28
|
-
kvcommon = {extras = ["k8s"], version = "^0.4.
|
|
29
|
+
kvcommon = {extras = ["k8s"], version = "^0.4.5"}
|
|
29
30
|
|
|
30
31
|
[tool.poetry.group.dev.dependencies]
|
|
31
32
|
black = "*"
|
|
@@ -27,39 +27,54 @@ class IAPToolkit:
|
|
|
27
27
|
Class to encapsulate client-specific vars and forward them to static functions
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
_GOOGLE_IAP_CLIENT_ID: str
|
|
31
|
-
|
|
32
|
-
def __init__(self, google_iap_client_id: str) -> None:
|
|
33
|
-
self._GOOGLE_IAP_CLIENT_ID = google_iap_client_id
|
|
34
|
-
|
|
35
30
|
@staticmethod
|
|
36
31
|
def sanitize_request_headers(request_headers: dict) -> dict:
|
|
37
32
|
return headers.sanitize_request_headers(request_headers)
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_service_account_jwt(
|
|
36
|
+
service_account_email: str, url_audience: str, bypass_cached: bool = False
|
|
37
|
+
) -> TokenStruct:
|
|
38
|
+
try:
|
|
39
|
+
return ServiceAccount.get_jwt(
|
|
40
|
+
service_account_email=service_account_email,
|
|
41
|
+
url_audience=url_audience,
|
|
42
|
+
bypass_cached=bypass_cached,
|
|
43
|
+
)
|
|
44
|
+
except ServiceAccountTokenException as ex:
|
|
45
|
+
LOG.warning(ex)
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get_token_oidc(iap_audience: str, bypass_cached: bool = False) -> TokenStruct:
|
|
40
50
|
try:
|
|
41
51
|
return ServiceAccount.get_token(
|
|
42
|
-
|
|
52
|
+
iap_audience=iap_audience,
|
|
53
|
+
bypass_cached=bypass_cached,
|
|
43
54
|
)
|
|
44
55
|
except ServiceAccountTokenException as ex:
|
|
45
|
-
LOG.
|
|
56
|
+
LOG.warning(ex)
|
|
46
57
|
raise
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
@staticmethod
|
|
60
|
+
def get_token_oidc_str(iap_audience: str, bypass_cached: bool = False) -> str:
|
|
61
|
+
struct = IAPToolkit.get_token_oidc(iap_audience=iap_audience, bypass_cached=bypass_cached)
|
|
50
62
|
return struct.id_token
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_token_oauth2(bypass_cached: bool = False) -> TokenRefreshStruct:
|
|
53
66
|
# TODO
|
|
54
67
|
raise NotImplementedError()
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
@staticmethod
|
|
70
|
+
def get_token_oauth2_str(bypass_cached: bool = False) -> str:
|
|
71
|
+
struct = IAPToolkit.get_token_oauth2(bypass_cached=bypass_cached)
|
|
58
72
|
return struct.id_token
|
|
59
73
|
|
|
74
|
+
@staticmethod
|
|
60
75
|
def get_token_and_add_to_headers(
|
|
61
|
-
self,
|
|
62
76
|
request_headers: dict,
|
|
77
|
+
iap_audience: str,
|
|
63
78
|
use_oauth2: bool = False,
|
|
64
79
|
use_auth_header: bool = False,
|
|
65
80
|
bypass_cached: bool = False,
|
|
@@ -82,23 +97,68 @@ class IAPToolkit:
|
|
|
82
97
|
id_token = None
|
|
83
98
|
from_cache = False
|
|
84
99
|
if use_oauth2:
|
|
85
|
-
token_refresh_struct: TokenRefreshStruct =
|
|
100
|
+
token_refresh_struct: TokenRefreshStruct = IAPToolkit.get_token_oauth2(bypass_cached=bypass_cached)
|
|
86
101
|
id_token = token_refresh_struct.id_token
|
|
87
102
|
from_cache = token_refresh_struct.from_cache
|
|
88
103
|
else:
|
|
89
|
-
token_struct: TokenStruct =
|
|
104
|
+
token_struct: TokenStruct = IAPToolkit.get_token_oidc(
|
|
105
|
+
iap_audience=iap_audience, bypass_cached=bypass_cached
|
|
106
|
+
)
|
|
90
107
|
id_token = token_struct.id_token
|
|
91
108
|
from_cache = token_struct.from_cache
|
|
92
109
|
|
|
93
110
|
headers.add_token_to_request_headers(
|
|
94
|
-
request_headers=request_headers,
|
|
111
|
+
request_headers=request_headers,
|
|
112
|
+
id_token=id_token,
|
|
113
|
+
use_auth_header=use_auth_header,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return from_cache
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def get_jwt_and_add_to_headers(
|
|
120
|
+
request_headers: dict,
|
|
121
|
+
service_account_email: str,
|
|
122
|
+
url_audience: str,
|
|
123
|
+
use_auth_header: bool = False,
|
|
124
|
+
bypass_cached: bool = False,
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
Retrieves a Service Account JWT and inserts it into the supplied request_headers dict.
|
|
128
|
+
|
|
129
|
+
request_headers is modified in-place
|
|
130
|
+
|
|
131
|
+
Params:
|
|
132
|
+
request_headers: dict of headers to insert into
|
|
133
|
+
service_account_email: Email address of the Service Account requesting access to IAP resource
|
|
134
|
+
url_audience: Target URL/Audience of IAP resource
|
|
135
|
+
use_auth_header: If true, use the 'Authorization' header instead of 'Proxy-Authorization'
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if token retrieved from cache, False if fresh from API
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
from_cache = False
|
|
143
|
+
|
|
144
|
+
token_struct: TokenStruct = IAPToolkit.get_service_account_jwt(
|
|
145
|
+
service_account_email=service_account_email, url_audience=url_audience, bypass_cached=bypass_cached
|
|
146
|
+
)
|
|
147
|
+
signed_jwt = token_struct.id_token
|
|
148
|
+
from_cache = token_struct.from_cache
|
|
149
|
+
|
|
150
|
+
headers.add_token_to_request_headers(
|
|
151
|
+
request_headers=request_headers,
|
|
152
|
+
id_token=signed_jwt,
|
|
153
|
+
use_auth_header=use_auth_header,
|
|
95
154
|
)
|
|
96
155
|
|
|
97
156
|
return from_cache
|
|
98
157
|
|
|
99
158
|
@staticmethod
|
|
100
159
|
def is_url_safe_for_token(
|
|
101
|
-
url: str | ParseResult,
|
|
160
|
+
url: str | ParseResult,
|
|
161
|
+
valid_domains: t.Optional[t.List[str] | t.Set[str] | t.Tuple[str]] = None,
|
|
102
162
|
):
|
|
103
163
|
if not isinstance(url, ParseResult):
|
|
104
164
|
url = urlparse(url)
|
|
@@ -109,6 +169,7 @@ class IAPToolkit:
|
|
|
109
169
|
self,
|
|
110
170
|
url: str | ParseResult,
|
|
111
171
|
request_headers: dict,
|
|
172
|
+
iap_audience: str,
|
|
112
173
|
valid_domains: t.List[str] | None = None,
|
|
113
174
|
use_oauth2: bool = False,
|
|
114
175
|
use_auth_header: bool = False,
|
|
@@ -130,18 +191,60 @@ class IAPToolkit:
|
|
|
130
191
|
if self.is_url_safe_for_token(url=url, valid_domains=valid_domains):
|
|
131
192
|
token_is_fresh = self.get_token_and_add_to_headers(
|
|
132
193
|
request_headers=request_headers,
|
|
194
|
+
iap_audience=iap_audience,
|
|
133
195
|
use_oauth2=use_oauth2,
|
|
134
196
|
use_auth_header=use_auth_header,
|
|
197
|
+
bypass_cached=bypass_cached
|
|
198
|
+
)
|
|
199
|
+
return ResultAddTokenHeader(token_added=True, token_is_fresh=token_is_fresh, token_is_jwt=False)
|
|
200
|
+
else:
|
|
201
|
+
LOG.warning(
|
|
202
|
+
"URL is not approved: %s - Token will not be added to headers. Valid domains are: %s",
|
|
203
|
+
url,
|
|
204
|
+
valid_domains,
|
|
205
|
+
)
|
|
206
|
+
return ResultAddTokenHeader(token_added=False, token_is_fresh=False, token_is_jwt=False)
|
|
207
|
+
|
|
208
|
+
def check_url_and_add_jwt_header(
|
|
209
|
+
self,
|
|
210
|
+
url: str | ParseResult,
|
|
211
|
+
request_headers: dict,
|
|
212
|
+
service_account_email: str,
|
|
213
|
+
url_audience: str,
|
|
214
|
+
valid_domains: t.List[str] | None = None,
|
|
215
|
+
use_auth_header: bool = False,
|
|
216
|
+
bypass_cached: bool = False,
|
|
217
|
+
) -> ResultAddTokenHeader:
|
|
218
|
+
"""
|
|
219
|
+
Checks that the supplied URL is valid (i.e.; in valid_domains) and if so, retrieves a
|
|
220
|
+
Service Account JWT and adds it to request_headers.
|
|
221
|
+
|
|
222
|
+
i.e.; A convenience wrapper with logging for is_url_safe_for_token() and get_jwt_and_add_to_headers()
|
|
223
|
+
|
|
224
|
+
Params:
|
|
225
|
+
url: URL string or urllib.ParseResult to check for validity
|
|
226
|
+
service_account_email: Email address of the Service Account requesting access to IAP resource
|
|
227
|
+
url_audience: Target URL/Audience of IAP resource - Should be the same as url or a substring thereof
|
|
228
|
+
request_headers: Dict of headers to insert into
|
|
229
|
+
valid_domains: List of domains to validate URL against
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if self.is_url_safe_for_token(url=url, valid_domains=valid_domains):
|
|
233
|
+
token_is_fresh = self.get_jwt_and_add_to_headers(
|
|
234
|
+
request_headers=request_headers,
|
|
235
|
+
service_account_email=service_account_email,
|
|
236
|
+
url_audience=url_audience,
|
|
237
|
+
use_auth_header=use_auth_header,
|
|
135
238
|
bypass_cached=bypass_cached,
|
|
136
239
|
)
|
|
137
|
-
return ResultAddTokenHeader(token_added=True, token_is_fresh=token_is_fresh)
|
|
240
|
+
return ResultAddTokenHeader(token_added=True, token_is_fresh=token_is_fresh, token_is_jwt=True)
|
|
138
241
|
else:
|
|
139
242
|
LOG.warning(
|
|
140
243
|
"URL is not approved: %s - Token will not be added to headers. Valid domains are: %s",
|
|
141
244
|
url,
|
|
142
245
|
valid_domains,
|
|
143
246
|
)
|
|
144
|
-
return ResultAddTokenHeader(token_added=False, token_is_fresh=False)
|
|
247
|
+
return ResultAddTokenHeader(token_added=False, token_is_fresh=False, token_is_jwt=True)
|
|
145
248
|
|
|
146
249
|
|
|
147
250
|
class IAPToolkit_OIDC(IAPToolkit):
|
|
@@ -158,13 +261,14 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
158
261
|
def get_token_and_add_to_headers(
|
|
159
262
|
self,
|
|
160
263
|
request_headers: dict,
|
|
264
|
+
iap_audience: str,
|
|
161
265
|
use_auth_header: bool = False,
|
|
162
|
-
use_oauth2: bool = False,
|
|
163
266
|
bypass_cached: bool = False,
|
|
164
267
|
) -> bool:
|
|
165
268
|
return super().get_token_and_add_to_headers(
|
|
269
|
+
iap_audience=iap_audience,
|
|
166
270
|
request_headers=request_headers,
|
|
167
|
-
use_oauth2=
|
|
271
|
+
use_oauth2=False,
|
|
168
272
|
use_auth_header=use_auth_header,
|
|
169
273
|
bypass_cached=bypass_cached,
|
|
170
274
|
)
|
|
@@ -173,6 +277,7 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
173
277
|
self,
|
|
174
278
|
url: str | ParseResult,
|
|
175
279
|
request_headers: dict,
|
|
280
|
+
iap_audience: str,
|
|
176
281
|
valid_domains: t.List[str] | None = None,
|
|
177
282
|
use_auth_header: bool = False,
|
|
178
283
|
bypass_cached: bool = False,
|
|
@@ -180,6 +285,7 @@ class IAPToolkit_OIDC(IAPToolkit):
|
|
|
180
285
|
return super().check_url_and_add_token_header(
|
|
181
286
|
url,
|
|
182
287
|
request_headers=request_headers,
|
|
288
|
+
iap_audience=iap_audience,
|
|
183
289
|
valid_domains=valid_domains,
|
|
184
290
|
use_oauth2=False,
|
|
185
291
|
use_auth_header=use_auth_header,
|
|
@@ -195,8 +301,12 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
195
301
|
_GOOGLE_CLIENT_ID: str
|
|
196
302
|
_GOOGLE_CLIENT_SECRET: str
|
|
197
303
|
|
|
198
|
-
def __init__(
|
|
199
|
-
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
google_client_id: str,
|
|
307
|
+
google_client_secret: str,
|
|
308
|
+
) -> None:
|
|
309
|
+
super().__init__()
|
|
200
310
|
self._GOOGLE_CLIENT_ID = google_client_id
|
|
201
311
|
self._GOOGLE_CLIENT_SECRET = google_client_secret
|
|
202
312
|
|
|
@@ -207,15 +317,12 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
207
317
|
raise NotImplementedError("Cannot call OIDC methods on OAuth2-only instance of IAPToolkit.")
|
|
208
318
|
|
|
209
319
|
def get_token_and_add_to_headers(
|
|
210
|
-
self,
|
|
211
|
-
request_headers: dict,
|
|
212
|
-
use_auth_header: bool = False,
|
|
213
|
-
use_oauth2: bool = True,
|
|
214
|
-
bypass_cached: bool = False,
|
|
320
|
+
self, request_headers: dict, iap_audience: str, use_auth_header: bool = False, bypass_cached: bool = False
|
|
215
321
|
) -> bool:
|
|
216
322
|
return super().get_token_and_add_to_headers(
|
|
217
323
|
request_headers=request_headers,
|
|
218
|
-
|
|
324
|
+
iap_audience=iap_audience,
|
|
325
|
+
use_oauth2=True,
|
|
219
326
|
use_auth_header=use_auth_header,
|
|
220
327
|
bypass_cached=bypass_cached,
|
|
221
328
|
)
|
|
@@ -224,6 +331,7 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
224
331
|
self,
|
|
225
332
|
url: str | ParseResult,
|
|
226
333
|
request_headers: dict,
|
|
334
|
+
iap_audience: str,
|
|
227
335
|
valid_domains: t.List[str] | None = None,
|
|
228
336
|
use_auth_header: bool = False,
|
|
229
337
|
bypass_cached: bool = False,
|
|
@@ -231,6 +339,7 @@ class IAPToolkit_OAuth2(IAPToolkit):
|
|
|
231
339
|
return super().check_url_and_add_token_header(
|
|
232
340
|
url=url,
|
|
233
341
|
request_headers=request_headers,
|
|
342
|
+
iap_audience=iap_audience,
|
|
234
343
|
valid_domains=valid_domains,
|
|
235
344
|
use_oauth2=True,
|
|
236
345
|
use_auth_header=use_auth_header,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import typing as t
|
|
3
3
|
|
|
4
|
+
from google.api_core.exceptions import PermissionDenied
|
|
4
5
|
from google.auth.environment_vars import CREDENTIALS as GOOGLE_CREDENTIALS_FILE_PATH
|
|
5
6
|
from google.auth.exceptions import DefaultCredentialsError
|
|
6
7
|
from google.auth.exceptions import RefreshError
|
|
@@ -27,7 +28,9 @@ class IAPClientIDException(IAPToolkitBaseException):
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class ServiceAccountTokenException(TokenException):
|
|
30
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, message: str, google_exception: t.Union[DefaultCredentialsError, RefreshError, PermissionDenied] | None
|
|
33
|
+
):
|
|
31
34
|
self.google_exception = google_exception
|
|
32
35
|
credentials_env_var_value = os.environ.get(GOOGLE_CREDENTIALS_FILE_PATH)
|
|
33
36
|
metadata_server_attempted = not credentials_env_var_value
|
|
@@ -42,7 +45,9 @@ class ServiceAccountTokenException(TokenException):
|
|
|
42
45
|
|
|
43
46
|
@property
|
|
44
47
|
def retryable(self):
|
|
45
|
-
|
|
48
|
+
if not self.google_exception:
|
|
49
|
+
return False
|
|
50
|
+
return getattr(self.google_exception, "_retryable", False)
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
class ServiceAccountNoDefaultCredentials(ServiceAccountTokenException):
|
|
@@ -53,6 +58,10 @@ class ServiceAccountTokenFailedRefresh(ServiceAccountTokenException):
|
|
|
53
58
|
pass
|
|
54
59
|
|
|
55
60
|
|
|
61
|
+
class JWTPermissionException(ServiceAccountTokenException):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
56
65
|
class InvalidDomain(IAPToolkitBaseException):
|
|
57
66
|
pass
|
|
58
67
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import google.auth
|
|
6
|
+
import google.api_core.exceptions
|
|
7
|
+
from google.auth.compute_engine import IDTokenCredentials as GoogleIDTokenCredentials
|
|
8
|
+
from google.auth.exceptions import DefaultCredentialsError as GoogleDefaultCredentialsError
|
|
9
|
+
from google.auth.exceptions import RefreshError as GoogleRefreshError
|
|
10
|
+
from google.auth.transport.requests import Request as GoogleRequest
|
|
11
|
+
from google.cloud import iam_credentials_v1
|
|
12
|
+
from google.oauth2 import id_token as google_id_token_lib
|
|
13
|
+
|
|
14
|
+
from kvcommon import logger
|
|
15
|
+
|
|
16
|
+
from iaptoolkit import exceptions
|
|
17
|
+
from iaptoolkit.tokens.token_datastore import datastore
|
|
18
|
+
|
|
19
|
+
from .structs import TokenStruct
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LOG = logger.get_logger("iaptk")
|
|
23
|
+
MAX_RECURSE = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _utcnow() -> datetime.datetime:
|
|
27
|
+
return datetime.datetime.now(tz=datetime.UTC)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ServiceAccount(object):
|
|
31
|
+
"""Base class for interacting with service accounts and OIDC tokens for IAP"""
|
|
32
|
+
|
|
33
|
+
# TODO: This is a static namespace for SA functions. Turn it into a per-iap-client-id client
|
|
34
|
+
# TODO: Move Google-specific logic to GoogleServiceAccount
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _store_token(iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
|
|
38
|
+
try:
|
|
39
|
+
datastore.store_service_account_token(iap_client_id, id_token, token_expiry)
|
|
40
|
+
except Exception as ex: # Err on the side of not letting token-caching break requests.
|
|
41
|
+
raise exceptions.TokenStorageException(f"Exception when trying to store token. exception={ex}")
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _store_jwt(service_account_email: str, url_audience: str, signed_jwt: str, expiry: datetime.datetime):
|
|
45
|
+
try:
|
|
46
|
+
datastore.store_service_account_jwt(
|
|
47
|
+
service_account_email=service_account_email,
|
|
48
|
+
url_audience=url_audience,
|
|
49
|
+
signed_jwt=signed_jwt,
|
|
50
|
+
expiry=expiry,
|
|
51
|
+
)
|
|
52
|
+
except Exception as ex: # Err on the side of not letting token-caching break requests.
|
|
53
|
+
raise exceptions.TokenStorageException(f"Exception when trying to store token. exception={ex}")
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_stored_token(iap_client_id: str) -> TokenStruct | None:
|
|
57
|
+
try:
|
|
58
|
+
return datastore.get_stored_service_account_token(iap_client_id)
|
|
59
|
+
|
|
60
|
+
except Exception as ex:
|
|
61
|
+
# Err on the side of not letting token-caching break requests, hence blanket except
|
|
62
|
+
raise exceptions.TokenStorageException(f"Exception when trying to retrieve stored token. exception={ex}")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_stored_jwt(service_account_email: str, url_audience: str) -> TokenStruct | None:
|
|
66
|
+
try:
|
|
67
|
+
return datastore.get_stored_service_account_jwt(
|
|
68
|
+
service_account_email=service_account_email, url_audience=url_audience
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
except Exception as ex:
|
|
72
|
+
# Err on the side of not letting token-caching break requests, hence blanket except
|
|
73
|
+
raise exceptions.TokenStorageException(f"Exception when trying to retrieve stored token. exception={ex}")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _get_fresh_credentials(iap_client_id: str) -> GoogleIDTokenCredentials:
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
request = GoogleRequest()
|
|
80
|
+
credentials: GoogleIDTokenCredentials = google_id_token_lib.fetch_id_token_credentials(
|
|
81
|
+
iap_client_id, request
|
|
82
|
+
) # type: ignore
|
|
83
|
+
credentials.refresh(request)
|
|
84
|
+
|
|
85
|
+
except GoogleDefaultCredentialsError as ex:
|
|
86
|
+
# The exceptions that google's libs raise in this case are somewhat vague; wrap them.
|
|
87
|
+
raise exceptions.ServiceAccountNoDefaultCredentials(
|
|
88
|
+
message="Failed to get ServiceAccount token: Lacking default credentials.",
|
|
89
|
+
google_exception=ex,
|
|
90
|
+
)
|
|
91
|
+
except GoogleRefreshError as ex:
|
|
92
|
+
# Likely attempting to get a token for a service account in an environment that
|
|
93
|
+
# doesn't have one attached.
|
|
94
|
+
raise exceptions.ServiceAccountTokenFailedRefresh(
|
|
95
|
+
message="Failed to get ServiceAccount token: Refreshing token failed.",
|
|
96
|
+
google_exception=ex,
|
|
97
|
+
)
|
|
98
|
+
return credentials
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _get_fresh_token(iap_client_id: str, use_jwt: bool = False) -> TokenStruct:
|
|
102
|
+
google_credentials = ServiceAccount._get_fresh_credentials(iap_client_id)
|
|
103
|
+
id_token: str = str(google_credentials.token)
|
|
104
|
+
if not id_token:
|
|
105
|
+
raise exceptions.TokenException("Invalid [empty] token retrieved for Service Account.")
|
|
106
|
+
|
|
107
|
+
# Google lib uses deprecated 'utcfromtimestamp' func as of v2.29.x
|
|
108
|
+
# e.g.: datetime.datetime.utcfromtimestamp(payload["exp"])
|
|
109
|
+
# This creates a TZ-naive datetime in UTC from a POSIX timestamp.
|
|
110
|
+
# Python datetimes assume local TZ, and we want to explicitly only work in UTC here.
|
|
111
|
+
token_expiry = google_credentials.expiry.replace(tzinfo=datetime.timezone.utc)
|
|
112
|
+
|
|
113
|
+
return TokenStruct(id_token=id_token, expiry=token_expiry, from_cache=False)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _get_jwt(service_account_email: str, url_audience: str) -> TokenStruct:
|
|
117
|
+
"""
|
|
118
|
+
Returns a signed JWT for the specified service account
|
|
119
|
+
"""
|
|
120
|
+
now = _utcnow()
|
|
121
|
+
expiration_delta = datetime.timedelta(seconds=3595)
|
|
122
|
+
expiry_dt = now + expiration_delta
|
|
123
|
+
|
|
124
|
+
issued_at = int(now.timestamp())
|
|
125
|
+
expiry = int(expiry_dt.timestamp())
|
|
126
|
+
|
|
127
|
+
jwt_payload = {
|
|
128
|
+
"iss": service_account_email,
|
|
129
|
+
"sub": service_account_email,
|
|
130
|
+
"aud": url_audience,
|
|
131
|
+
"iat": issued_at,
|
|
132
|
+
"exp": expiry,
|
|
133
|
+
}
|
|
134
|
+
jwt_payload_str = json.dumps(jwt_payload)
|
|
135
|
+
|
|
136
|
+
source_credentials, project_id = google.auth.default()
|
|
137
|
+
iam_client = iam_credentials_v1.IAMCredentialsClient(credentials=source_credentials) # type: ignore
|
|
138
|
+
name = iam_client.service_account_path("-", service_account_email)
|
|
139
|
+
response = iam_client.sign_jwt(name=name, payload=jwt_payload_str)
|
|
140
|
+
# return response.signed_jwt
|
|
141
|
+
return TokenStruct.for_jwt(signed_jwt=response.signed_jwt, expiry=expiry_dt, from_cache=False)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def get_jwt(
|
|
145
|
+
service_account_email: str, url_audience: str, bypass_cached: bool = False, attempts: int = 0
|
|
146
|
+
) -> TokenStruct:
|
|
147
|
+
use_cache = not bypass_cached
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
token_struct: TokenStruct | None = None
|
|
151
|
+
|
|
152
|
+
if use_cache:
|
|
153
|
+
token_struct = ServiceAccount.get_stored_jwt(
|
|
154
|
+
service_account_email=service_account_email, url_audience=url_audience
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not token_struct:
|
|
158
|
+
token_struct = ServiceAccount._get_jwt(
|
|
159
|
+
service_account_email=service_account_email, url_audience=url_audience
|
|
160
|
+
)
|
|
161
|
+
if use_cache:
|
|
162
|
+
ServiceAccount._store_jwt(
|
|
163
|
+
service_account_email=service_account_email,
|
|
164
|
+
url_audience=url_audience,
|
|
165
|
+
signed_jwt=token_struct.id_token,
|
|
166
|
+
expiry=token_struct.expiry,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return token_struct
|
|
170
|
+
|
|
171
|
+
except google.api_core.exceptions.PermissionDenied as ex:
|
|
172
|
+
raise exceptions.JWTPermissionException(
|
|
173
|
+
"Permission denied while retrieving signed JWT for service account. "
|
|
174
|
+
"Service Account requires IAM Role: 'roles/iam.serviceAccountTokenCreator'",
|
|
175
|
+
google_exception=ex,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
except exceptions.ServiceAccountTokenException as ex:
|
|
179
|
+
attempts += 1
|
|
180
|
+
if attempts > MAX_RECURSE or not ex.retryable:
|
|
181
|
+
raise
|
|
182
|
+
return ServiceAccount.get_jwt(
|
|
183
|
+
service_account_email=service_account_email,
|
|
184
|
+
url_audience=url_audience,
|
|
185
|
+
bypass_cached=False,
|
|
186
|
+
attempts=attempts,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
except exceptions.TokenStorageException as ex:
|
|
190
|
+
if attempts > 1:
|
|
191
|
+
raise
|
|
192
|
+
attempts += 1
|
|
193
|
+
# Try again without involving the cache
|
|
194
|
+
return ServiceAccount.get_jwt(
|
|
195
|
+
service_account_email=service_account_email,
|
|
196
|
+
url_audience=url_audience,
|
|
197
|
+
bypass_cached=True,
|
|
198
|
+
attempts=attempts,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def get_token(iap_audience: str, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
203
|
+
"""Retrieves an OIDC token for the current environment either from environment variable or from
|
|
204
|
+
metadata service.
|
|
205
|
+
|
|
206
|
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
|
207
|
+
to the path of a valid service account JSON file, then ID token is
|
|
208
|
+
acquired using this service account credentials.
|
|
209
|
+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
|
210
|
+
then the ID token is obtained from the metadata server.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
iap_client_id: The client ID used by IAP. Can be thought of as JWT audience.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
An OIDC token for use in connecting through IAP.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
:class:`ServiceAccountTokenException` if a token could not be retrieved due to either
|
|
220
|
+
missing credentials from env-var/JSON or inability to talk to metadata server.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
use_cache = not bypass_cached
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
token_struct: TokenStruct | None = None
|
|
227
|
+
|
|
228
|
+
if use_cache:
|
|
229
|
+
token_struct = ServiceAccount.get_stored_token(iap_audience)
|
|
230
|
+
|
|
231
|
+
if not token_struct:
|
|
232
|
+
token_struct = ServiceAccount._get_fresh_token(iap_audience)
|
|
233
|
+
if use_cache:
|
|
234
|
+
ServiceAccount._store_token(iap_audience, token_struct.id_token, token_struct.expiry)
|
|
235
|
+
|
|
236
|
+
return token_struct
|
|
237
|
+
|
|
238
|
+
except exceptions.ServiceAccountTokenException as ex:
|
|
239
|
+
attempts += 1
|
|
240
|
+
if attempts > MAX_RECURSE or not ex.retryable:
|
|
241
|
+
raise
|
|
242
|
+
return ServiceAccount.get_token(iap_audience, bypass_cached=False, attempts=attempts)
|
|
243
|
+
|
|
244
|
+
except exceptions.TokenStorageException as ex:
|
|
245
|
+
if attempts > 1:
|
|
246
|
+
raise
|
|
247
|
+
attempts += 1
|
|
248
|
+
# Try again without involving the cache
|
|
249
|
+
return ServiceAccount.get_token(iap_audience, bypass_cached=True, attempts=attempts)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class GoogleServiceAccount(ServiceAccount):
|
|
253
|
+
"""
|
|
254
|
+
For interacting with Google service accounts, Service Account JWTs and OIDC tokens for Google IAP
|
|
255
|
+
|
|
256
|
+
Service Account requires IAM Role: 'roles/iam.serviceAccountTokenCreator'
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(self, service_account_email: str) -> None:
|
|
260
|
+
if not service_account_email or not isinstance(service_account_email, str):
|
|
261
|
+
raise exceptions.ServiceAccountTokenException(
|
|
262
|
+
"Invalid iap_client_id for GoogleServiceAccount", google_exception=None
|
|
263
|
+
)
|
|
264
|
+
self._service_account_email = service_account_email
|
|
265
|
+
super().__init__()
|
|
266
|
+
|
|
267
|
+
def get_stored_jwt(self, url_audience: str) -> t.Optional[TokenStruct]:
|
|
268
|
+
return ServiceAccount.get_stored_jwt(self._service_account_email, url_audience=url_audience)
|
|
269
|
+
|
|
270
|
+
def get_jwt(self, url_audience: str, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
271
|
+
return ServiceAccount.get_jwt(
|
|
272
|
+
service_account_email=self._service_account_email,
|
|
273
|
+
url_audience=url_audience,
|
|
274
|
+
bypass_cached=bypass_cached,
|
|
275
|
+
attempts=attempts,
|
|
276
|
+
)
|
|
@@ -20,6 +20,11 @@ class TokenStruct:
|
|
|
20
20
|
id_token: str
|
|
21
21
|
expiry: datetime.datetime
|
|
22
22
|
from_cache: bool = False
|
|
23
|
+
is_jwt: bool = False
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def for_jwt(cls, signed_jwt: str, expiry: datetime.datetime, from_cache: bool = False):
|
|
27
|
+
return TokenStruct(id_token=signed_jwt, is_jwt=True, from_cache=from_cache, expiry=expiry)
|
|
23
28
|
|
|
24
29
|
@property
|
|
25
30
|
def expired(self):
|
|
@@ -67,3 +72,4 @@ class TokenStructOAuth2(TokenStruct):
|
|
|
67
72
|
class ResultAddTokenHeader:
|
|
68
73
|
token_added: bool
|
|
69
74
|
token_is_fresh: bool
|
|
75
|
+
token_is_jwt: bool
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
|
|
8
|
+
from kvcommon.datastore import VersionedDatastore
|
|
9
|
+
|
|
10
|
+
from iaptoolkit.exceptions import TokenStorageException
|
|
11
|
+
from iaptoolkit.constants import IAPTOOLKIT_CONFIG_VERSION
|
|
12
|
+
|
|
13
|
+
from .structs import TokenStruct
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
LOG = logger.get_logger("iaptk-ds")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TokenDatastore(VersionedDatastore):
|
|
20
|
+
_service_account_jwts_email_limit = 1000
|
|
21
|
+
|
|
22
|
+
def __init__(self, backend: DatastoreBackend | type[DatastoreBackend]) -> None:
|
|
23
|
+
super().__init__(backend=backend, config_version=IAPTOOLKIT_CONFIG_VERSION)
|
|
24
|
+
self._ensure_tokens_dict()
|
|
25
|
+
|
|
26
|
+
def _ensure_tokens_dict(self):
|
|
27
|
+
tokens_dict = self.get_or_create_nested_dict("tokens")
|
|
28
|
+
if "refresh" not in tokens_dict.keys():
|
|
29
|
+
tokens_dict["refresh"] = None
|
|
30
|
+
self.set_value("tokens", tokens_dict)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def service_account_tokens(self) -> dict:
|
|
34
|
+
return self.get_or_create_nested_dict("service_account_tokens")
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def service_account_jwts(self) -> dict:
|
|
38
|
+
return self.get_or_create_nested_dict("service_account_jwts")
|
|
39
|
+
|
|
40
|
+
def discard_existing_tokens(self):
|
|
41
|
+
LOG.debug("Discarding existing tokens.")
|
|
42
|
+
self.update_data(tokens={})
|
|
43
|
+
|
|
44
|
+
def get_stored_service_account_token(self, iap_client_id: str) -> TokenStruct | None:
|
|
45
|
+
token_data = self.service_account_tokens.get(iap_client_id, None)
|
|
46
|
+
if not token_data or not token_data.id_token or not token_data.expiry:
|
|
47
|
+
LOG.debug("No stored service account token for current iap_client_id")
|
|
48
|
+
return
|
|
49
|
+
return self._dict_to_tokenstruct(token_data)
|
|
50
|
+
|
|
51
|
+
def store_service_account_token(self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
|
|
52
|
+
if not id_token:
|
|
53
|
+
raise TokenStorageException("TokenDatastore: Attempting to store invalid [empty] token")
|
|
54
|
+
|
|
55
|
+
tokens_dict = self.service_account_tokens
|
|
56
|
+
self.service_account_tokens[iap_client_id] = dict(id_token=id_token, token_expiry=token_expiry.isoformat())
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
self.update_data(service_account_tokens=tokens_dict)
|
|
60
|
+
except Exception as ex:
|
|
61
|
+
LOG.error("Failed to store service account token for re-use. exception=%s", ex)
|
|
62
|
+
|
|
63
|
+
def _get_or_create_dict_for_service_account_and_url(self, service_account_email: str, url_audience: str):
|
|
64
|
+
jwts_dict = self.service_account_jwts
|
|
65
|
+
jwts_dict_for_email = jwts_dict.get(service_account_email, dict())
|
|
66
|
+
jwts_dict[service_account_email] = jwts_dict_for_email
|
|
67
|
+
|
|
68
|
+
token_dict = jwts_dict_for_email.get(url_audience, dict())
|
|
69
|
+
return token_dict
|
|
70
|
+
|
|
71
|
+
def get_stored_service_account_jwt(self, service_account_email: str, url_audience: str) -> TokenStruct | None:
|
|
72
|
+
jwts_dict_for_email = self._get_or_create_dict_for_service_account_and_url(service_account_email, url_audience)
|
|
73
|
+
token_data = jwts_dict_for_email.get(url_audience, None)
|
|
74
|
+
if not token_data:
|
|
75
|
+
LOG.debug("No stored service account JWT for service account '%s'", service_account_email)
|
|
76
|
+
return
|
|
77
|
+
return self._dict_to_tokenstruct(token_data, is_jwt=True)
|
|
78
|
+
|
|
79
|
+
def store_service_account_jwt(
|
|
80
|
+
self, service_account_email: str, url_audience: str, signed_jwt: str, expiry: datetime.datetime
|
|
81
|
+
):
|
|
82
|
+
if not signed_jwt:
|
|
83
|
+
raise TokenStorageException("TokenDatastore: Attempting to store invalid [empty] jwt")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
token_dict = self._get_or_create_dict_for_service_account_and_url(service_account_email, url_audience)
|
|
87
|
+
token_dict["signed_jwt"] = signed_jwt
|
|
88
|
+
token_dict["token_expiry"] = expiry.isoformat()
|
|
89
|
+
except Exception as ex:
|
|
90
|
+
LOG.error("Failed to store service account JWT for re-use. exception=%s", ex)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _dict_to_tokenstruct(token_data: dict, is_jwt: bool = False) -> TokenStruct | None:
|
|
94
|
+
if not token_data:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
id_token_from_dict: str = token_data.get("id_token", "")
|
|
98
|
+
token_expiry_from_dict: str = token_data.get("token_expiry", "")
|
|
99
|
+
if not id_token_from_dict or not token_expiry_from_dict:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
token_expiry = ""
|
|
103
|
+
try:
|
|
104
|
+
token_expiry = datetime.datetime.fromisoformat(token_expiry_from_dict)
|
|
105
|
+
except (ValueError, TypeError) as ex:
|
|
106
|
+
LOG.warning("Invalid token expiry for stored token - Could not parse from ISO format to datetime.")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry, from_cache=True, is_jwt=is_jwt)
|
|
110
|
+
if not token_struct.valid:
|
|
111
|
+
LOG.warning("Stored service account token is INVALID")
|
|
112
|
+
return
|
|
113
|
+
if token_struct.expired:
|
|
114
|
+
LOG.debug("Stored service account token has EXPIRED")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
return token_struct
|
|
118
|
+
|
|
119
|
+
def _migrate_version(self):
|
|
120
|
+
# Override
|
|
121
|
+
self.discard_existing_tokens()
|
|
122
|
+
return super()._migrate_version()
|
|
123
|
+
|
|
124
|
+
# def get_stored_oauth2_token(self, iap_client_id: str):
|
|
125
|
+
# # TODO: OAuth2
|
|
126
|
+
# raise NotImplementedError()
|
|
127
|
+
|
|
128
|
+
# def store_oauth2_token(self, iap_client_id: str):
|
|
129
|
+
# # TODO: OAuth2
|
|
130
|
+
# raise NotImplementedError()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
datastore = TokenDatastore(DictBackend)
|
iaptoolkit-0.3.9/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# IAP Toolkit
|
|
2
|
-
|
|
3
|
-
A library of utils to ease programmatic authentication with Google IAP (and ideally other IAPs in future).
|
|
4
|
-
|
|
5
|
-
# PyPi
|
|
6
|
-
https://pypi.org/project/iaptoolkit/
|
|
7
|
-
|
|
8
|
-
# Installation
|
|
9
|
-
### With Poetry:
|
|
10
|
-
`poetry add iaptoolkit`
|
|
11
|
-
|
|
12
|
-
### With pip:
|
|
13
|
-
`pip install iaptoolkit`
|
|
14
|
-
|
|
15
|
-
## Quick Start / Example Usage
|
|
16
|
-
|
|
17
|
-
```python
|
|
18
|
-
import requests
|
|
19
|
-
|
|
20
|
-
from iaptoolkit import IAPToolkit
|
|
21
|
-
|
|
22
|
-
iaptk = IAPToolkit(google_iap_client_id="EXAMPLE_ID_123456789ABCDEF")
|
|
23
|
-
allowed_domains = ["example.com", ]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Example #1 - Combined Calls
|
|
27
|
-
def example1(url: str):
|
|
28
|
-
headers = dict()
|
|
29
|
-
result = iaptk.check_url_and_add_token_header(
|
|
30
|
-
url=url,
|
|
31
|
-
request_headers=headers,
|
|
32
|
-
valid_domains=allowed_domains
|
|
33
|
-
)
|
|
34
|
-
# result.token_added (bool) indicates if the token was added, depending on whether or not URL was valid
|
|
35
|
-
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
36
|
-
|
|
37
|
-
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
38
|
-
response = requests.request("GET", url, headers=headers)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Example #2 - Separate Calls - Functionally the same as Example 1 but more flexibility in URL validation
|
|
42
|
-
def example1(url: str):
|
|
43
|
-
is_url_safe: bool = iaptk.is_url_safe_for_token(url=url, valid_domains=valid_domains)
|
|
44
|
-
|
|
45
|
-
if not is_url_safe:
|
|
46
|
-
raise ExampleBadURLException("This URL isn't safe to send token headers to!")
|
|
47
|
-
|
|
48
|
-
headers = dict()
|
|
49
|
-
token_is_fresh: bool = iaptk.get_token_and_add_to_headers(request_headers=headers)
|
|
50
|
-
# token_is_fresh indicates if token was newly retrieved (True), or if a cached token was reused (False)
|
|
51
|
-
# headers dict now contains the appropriate Bearer Token header for Google IAP
|
|
52
|
-
|
|
53
|
-
# Make HTTP GET request with requests lib, with our headers containing bearer token to auth with IAP
|
|
54
|
-
response = requests.request("GET", url, headers=headers)
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Disclaimer
|
|
59
|
-
|
|
60
|
-
This project is not affiliated with Google. No trademark infringement intended.
|
|
@@ -1,186 +0,0 @@
|
|
|
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 TokenException
|
|
18
|
-
from iaptoolkit.exceptions import TokenStorageException
|
|
19
|
-
|
|
20
|
-
from .structs import TokenStruct
|
|
21
|
-
from .structs import TokenRefreshStruct
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
LOG = logger.get_logger("iaptk")
|
|
25
|
-
MAX_RECURSE = 3
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ServiceAccount(object):
|
|
29
|
-
"""Base class for interacting with service accounts and OIDC tokens for IAP"""
|
|
30
|
-
|
|
31
|
-
# TODO: This is a static namespace for SA functions. Turn it into a per-iap-client-id client
|
|
32
|
-
# TODO: Move Google-specific logic to GoogleServiceAccount
|
|
33
|
-
|
|
34
|
-
@staticmethod
|
|
35
|
-
def _store_token(iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
|
|
36
|
-
try:
|
|
37
|
-
datastore.store_service_account_token(iap_client_id, id_token, token_expiry)
|
|
38
|
-
except Exception as ex: # Err on the side of not letting token-caching break requests.
|
|
39
|
-
raise TokenStorageException(f"Exception when trying to store token. exception={ex}")
|
|
40
|
-
|
|
41
|
-
@staticmethod
|
|
42
|
-
def get_stored_token(iap_client_id: str) -> t.Optional[TokenStruct]:
|
|
43
|
-
try:
|
|
44
|
-
token_dict = datastore.get_stored_service_account_token(iap_client_id)
|
|
45
|
-
if (
|
|
46
|
-
not token_dict
|
|
47
|
-
or not token_dict.get("id_token", None)
|
|
48
|
-
or not token_dict.get("token_expiry", None)
|
|
49
|
-
):
|
|
50
|
-
LOG.debug("No stored service account token for current iap_client_id")
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
id_token_from_dict: str = token_dict.get("id_token", "")
|
|
54
|
-
token_expiry_from_dict: str = token_dict.get("token_expiry", "")
|
|
55
|
-
|
|
56
|
-
token_expiry = ""
|
|
57
|
-
try:
|
|
58
|
-
token_expiry = datetime.datetime.fromisoformat(token_expiry_from_dict)
|
|
59
|
-
except (ValueError, TypeError) as ex:
|
|
60
|
-
LOG.debug(
|
|
61
|
-
"Invalid token expiry for stored token - Could not parse from ISO format to datetime."
|
|
62
|
-
)
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
token_struct = TokenStruct(id_token=id_token_from_dict, expiry=token_expiry, from_cache=True)
|
|
66
|
-
if not token_struct.valid:
|
|
67
|
-
LOG.debug("Stored service account token for current iap_client_id is INVALID")
|
|
68
|
-
return
|
|
69
|
-
if token_struct.expired:
|
|
70
|
-
LOG.debug("Stored service account token for current iap_client_id has EXPIRED")
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
return token_struct
|
|
74
|
-
|
|
75
|
-
except Exception as ex:
|
|
76
|
-
# Err on the side of not letting token-caching break requests, hence blanket except
|
|
77
|
-
raise TokenStorageException(f"Exception when trying to retrieve stored token. exception={ex}")
|
|
78
|
-
|
|
79
|
-
@staticmethod
|
|
80
|
-
def _get_fresh_credentials(iap_client_id: str) -> GoogleIDTokenCredentials:
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
request = GoogleRequest()
|
|
84
|
-
credentials: GoogleIDTokenCredentials = google_id_token_lib.fetch_id_token_credentials(
|
|
85
|
-
iap_client_id, request
|
|
86
|
-
) # type: ignore
|
|
87
|
-
credentials.refresh(request)
|
|
88
|
-
|
|
89
|
-
except GoogleDefaultCredentialsError as ex:
|
|
90
|
-
# The exceptions that google's libs raise in this case are somewhat vague; wrap them.
|
|
91
|
-
raise ServiceAccountNoDefaultCredentials(
|
|
92
|
-
message="Failed to get ServiceAccount token: Lacking default credentials.",
|
|
93
|
-
google_exception=ex,
|
|
94
|
-
)
|
|
95
|
-
except GoogleRefreshError as ex:
|
|
96
|
-
# Likely attempting to get a token for a service account in an environment that
|
|
97
|
-
# doesn't have one attached.
|
|
98
|
-
raise ServiceAccountTokenFailedRefresh(
|
|
99
|
-
message="Failed to get ServiceAccount token: Refreshing token failed.", 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
|
-
if not id_token:
|
|
108
|
-
raise TokenException("Invalid [empty] token retrieved for Service Account.")
|
|
109
|
-
|
|
110
|
-
# Google lib uses deprecated 'utcfromtimestamp' func as of v2.29.x
|
|
111
|
-
# e.g.: datetime.datetime.utcfromtimestamp(payload["exp"])
|
|
112
|
-
# This creates a TZ-naive datetime in UTC from a POSIX timestamp.
|
|
113
|
-
# Python datetimes assume local TZ, and we want to explicitly only work in UTC here.
|
|
114
|
-
token_expiry = google_credentials.expiry.replace(tzinfo=datetime.timezone.utc)
|
|
115
|
-
|
|
116
|
-
return TokenStruct(id_token=id_token, expiry=token_expiry, from_cache=False)
|
|
117
|
-
|
|
118
|
-
@staticmethod
|
|
119
|
-
def get_token(iap_client_id: str, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
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_struct: TokenStruct | None = None
|
|
144
|
-
|
|
145
|
-
if use_cache:
|
|
146
|
-
token_struct = ServiceAccount.get_stored_token(iap_client_id)
|
|
147
|
-
|
|
148
|
-
if not token_struct:
|
|
149
|
-
token_struct = ServiceAccount._get_fresh_token(iap_client_id)
|
|
150
|
-
if use_cache:
|
|
151
|
-
ServiceAccount._store_token(iap_client_id, token_struct.id_token, token_struct.expiry)
|
|
152
|
-
|
|
153
|
-
return token_struct
|
|
154
|
-
|
|
155
|
-
except ServiceAccountTokenException as ex:
|
|
156
|
-
attempts += 1
|
|
157
|
-
if attempts > MAX_RECURSE or not ex.retryable:
|
|
158
|
-
raise
|
|
159
|
-
return ServiceAccount.get_token(iap_client_id, bypass_cached=False, attempts=attempts)
|
|
160
|
-
|
|
161
|
-
except TokenStorageException as ex:
|
|
162
|
-
if attempts > 1:
|
|
163
|
-
raise
|
|
164
|
-
attempts += 1
|
|
165
|
-
# Try again without involving the cache
|
|
166
|
-
return ServiceAccount.get_token(iap_client_id, bypass_cached=True, attempts=attempts)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class GoogleServiceAccount(ServiceAccount):
|
|
170
|
-
"""For interacting with Google service accounts and OIDC tokens for Google IAP"""
|
|
171
|
-
|
|
172
|
-
def __init__(self, iap_client_id: str) -> None:
|
|
173
|
-
if not iap_client_id or not isinstance(iap_client_id, str):
|
|
174
|
-
raise ServiceAccountTokenException(
|
|
175
|
-
"Invalid iap_client_id for GoogleServiceAccount", google_exception=None
|
|
176
|
-
)
|
|
177
|
-
self._iap_client_id = iap_client_id
|
|
178
|
-
super().__init__()
|
|
179
|
-
|
|
180
|
-
def get_stored_token(self) -> t.Optional[TokenStruct]:
|
|
181
|
-
return ServiceAccount.get_stored_token(self._iap_client_id)
|
|
182
|
-
|
|
183
|
-
def get_token(self, bypass_cached: bool = False, attempts: int = 0) -> TokenStruct:
|
|
184
|
-
return ServiceAccount.get_token(
|
|
185
|
-
iap_client_id=self._iap_client_id, bypass_cached=bypass_cached, attempts=attempts
|
|
186
|
-
)
|
|
@@ -1,72 +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
|
-
|
|
8
|
-
# from kvcommon.datastore.backend import TOMLBackend
|
|
9
|
-
from kvcommon.datastore import VersionedDatastore
|
|
10
|
-
|
|
11
|
-
from iaptoolkit.exceptions import TokenException
|
|
12
|
-
from iaptoolkit.constants import IAPTOOLKIT_CONFIG_VERSION
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
LOG = logger.get_logger("iaptk-ds")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TokenDatastore(VersionedDatastore):
|
|
19
|
-
_service_account_tokens_key = "service_account_tokens"
|
|
20
|
-
|
|
21
|
-
def __init__(self, backend: DatastoreBackend | type[DatastoreBackend]) -> None:
|
|
22
|
-
super().__init__(backend=backend, config_version=IAPTOOLKIT_CONFIG_VERSION)
|
|
23
|
-
self._ensure_tokens_dict()
|
|
24
|
-
|
|
25
|
-
def _ensure_tokens_dict(self):
|
|
26
|
-
tokens_dict = self.get_or_create_nested_dict("tokens")
|
|
27
|
-
if "refresh" not in tokens_dict.keys():
|
|
28
|
-
tokens_dict["refresh"] = None
|
|
29
|
-
self.set_value("tokens", tokens_dict)
|
|
30
|
-
|
|
31
|
-
def discard_existing_tokens(self):
|
|
32
|
-
LOG.debug("Discarding existing tokens.")
|
|
33
|
-
self.update_data(tokens={})
|
|
34
|
-
|
|
35
|
-
def get_stored_service_account_token(self, iap_client_id: str) -> t.Optional[dict]:
|
|
36
|
-
tokens_dict = self.get_or_create_nested_dict(self._service_account_tokens_key)
|
|
37
|
-
token_struct_dict = tokens_dict.get(iap_client_id, None)
|
|
38
|
-
if not token_struct_dict:
|
|
39
|
-
LOG.debug("No stored service account token for current iap_client_id")
|
|
40
|
-
return
|
|
41
|
-
return token_struct_dict
|
|
42
|
-
|
|
43
|
-
def store_service_account_token(self, iap_client_id: str, id_token: str, token_expiry: datetime.datetime):
|
|
44
|
-
if not id_token:
|
|
45
|
-
raise TokenException("TokenDatastore: Attempting to store invalid [empty] token")
|
|
46
|
-
|
|
47
|
-
tokens_dict = self.get_or_create_nested_dict(self._service_account_tokens_key)
|
|
48
|
-
tokens_dict[iap_client_id] = dict(id_token=id_token, token_expiry=token_expiry.isoformat())
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
self.update_data(service_account_tokens=tokens_dict)
|
|
52
|
-
except OSError as ex:
|
|
53
|
-
LOG.error("Failed to store service account token for re-use. exception=%s", ex)
|
|
54
|
-
|
|
55
|
-
def _migrate_version(self):
|
|
56
|
-
# Override
|
|
57
|
-
self.discard_existing_tokens()
|
|
58
|
-
return super()._migrate_version()
|
|
59
|
-
|
|
60
|
-
# def get_stored_oauth2_token(self, iap_client_id: str):
|
|
61
|
-
# # TODO: OAuth2
|
|
62
|
-
# raise NotImplementedError()
|
|
63
|
-
|
|
64
|
-
# def store_oauth2_token(self, iap_client_id: str):
|
|
65
|
-
# # TODO: OAuth2
|
|
66
|
-
# raise NotImplementedError()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
datastore = TokenDatastore(DictBackend)
|
|
70
|
-
|
|
71
|
-
# if PERSISTENT_DATASTORE_ENABLED:
|
|
72
|
-
# datastore_toml = TokenDatastore(TOMLBackend(PERSISTENT_DATASTORE_PATH, PERSISTENT_DATASTORE_USERNAME))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|