dotstat_io 0.2.7__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
Potentially problematic release.
This version of dotstat_io might be problematic. Click here for more details.
- dotstat_io/authentication.py +210 -191
- dotstat_io/client.py +402 -0
- {dotstat_io-0.2.7.dist-info → dotstat_io-1.0.1.dist-info}/METADATA +49 -33
- dotstat_io-1.0.1.dist-info/RECORD +6 -0
- {dotstat_io-0.2.7.dist-info → dotstat_io-1.0.1.dist-info}/WHEEL +1 -1
- dotstat_io/download_upload.py +0 -376
- dotstat_io-0.2.7.dist-info/RECORD +0 -6
dotstat_io/authentication.py
CHANGED
|
@@ -1,70 +1,121 @@
|
|
|
1
1
|
import requests
|
|
2
|
+
import os
|
|
2
3
|
import msal
|
|
3
4
|
import requests_kerberos
|
|
4
5
|
import json
|
|
5
6
|
import time
|
|
6
7
|
|
|
8
|
+
from abc import ABC
|
|
7
9
|
from enum import IntEnum
|
|
8
10
|
from datetime import datetime
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
#
|
|
11
13
|
class AuthenticationMode(IntEnum):
|
|
12
14
|
INTERACTIVE = 1
|
|
13
15
|
NONINTERACTIVE_WITH_SECRET = 2
|
|
14
16
|
NONINTERACTIVE_WITH_ADFS = 3
|
|
15
17
|
|
|
16
|
-
# class to manage
|
|
17
|
-
class
|
|
18
|
+
# super class to manage authentication
|
|
19
|
+
class Authentication(ABC):
|
|
20
|
+
|
|
21
|
+
# protected variables
|
|
22
|
+
_access_token = None
|
|
23
|
+
_refresh_token = None
|
|
24
|
+
_creation_time = None
|
|
25
|
+
_expiration_time = None
|
|
18
26
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
__SUCCESS = "Successful authentication"
|
|
27
|
+
# public variables
|
|
28
|
+
init_status = None
|
|
22
29
|
|
|
23
|
-
# Initialise
|
|
30
|
+
# Initialise authentication
|
|
24
31
|
def __init__(
|
|
25
32
|
self,
|
|
33
|
+
client_id: str,
|
|
26
34
|
mode: AuthenticationMode,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
client_secret: str = None,
|
|
36
|
+
scopes: list[str] = [],
|
|
37
|
+
token_url: str = None,
|
|
38
|
+
sdmx_resource_id: str = None,
|
|
30
39
|
authority_url: str = None,
|
|
31
|
-
token_url: str = None,
|
|
32
40
|
redirect_port: int = 3000,
|
|
33
|
-
|
|
41
|
+
user: str = None,
|
|
42
|
+
password: str = None,
|
|
43
|
+
proxy: str = None
|
|
34
44
|
):
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
self.scopes = scopes
|
|
46
|
-
self.authority_url = authority_url
|
|
47
|
-
self.token_url = token_url
|
|
48
|
-
self.redirect_port = redirect_port
|
|
49
|
-
self.client_secret = client_secret
|
|
50
|
-
|
|
51
|
-
match self.mode:
|
|
52
|
-
case AuthenticationMode.INTERACTIVE:
|
|
53
|
-
self.app = msal.PublicClientApplication(
|
|
54
|
-
self.client_id, authority=self.authority_url)
|
|
55
|
-
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
56
|
-
self.app = msal.ConfidentialClientApplication(
|
|
57
|
-
self.client_id, authority=self.authority_url, client_credential=self.client_secret)
|
|
45
|
+
self._client_id = client_id
|
|
46
|
+
self._mode = mode
|
|
47
|
+
self._client_secret = client_secret
|
|
48
|
+
self._scopes = scopes
|
|
49
|
+
self._token_url = token_url
|
|
50
|
+
self._sdmx_resource_id = sdmx_resource_id
|
|
51
|
+
self._authority_url = authority_url
|
|
52
|
+
self._redirect_port = redirect_port
|
|
53
|
+
self._user = user
|
|
54
|
+
self._password = password
|
|
58
55
|
|
|
56
|
+
if proxy:
|
|
57
|
+
self._proxies = {
|
|
58
|
+
"http": proxy,
|
|
59
|
+
"https": proxy
|
|
60
|
+
}
|
|
61
|
+
else:
|
|
62
|
+
self._proxies = None
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
self._initialize_token()
|
|
66
|
+
|
|
67
|
+
#
|
|
59
68
|
def __enter__(self):
|
|
60
69
|
return self
|
|
61
70
|
|
|
71
|
+
#
|
|
62
72
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
-
self.
|
|
73
|
+
self._access_token = None
|
|
74
|
+
self._refresh_token = None
|
|
75
|
+
self._creation_time = None
|
|
76
|
+
self._expiration_time = None
|
|
77
|
+
self.init_status = None
|
|
67
78
|
|
|
79
|
+
#
|
|
80
|
+
def _initialize_token(self):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
def get_token(self):
|
|
85
|
+
if (self._access_token is None) \
|
|
86
|
+
or (datetime.fromtimestamp(self._expiration_time) is not None \
|
|
87
|
+
and datetime.now() >= datetime.fromtimestamp(self._expiration_time)):
|
|
88
|
+
self._initialize_token()
|
|
89
|
+
|
|
90
|
+
return self._access_token
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# sub class to manage ADFS authentication using OIDC flows
|
|
94
|
+
class AdfsAuthentication(Authentication):
|
|
95
|
+
# private constants
|
|
96
|
+
__ERROR_OCCURRED = "An error occurred: "
|
|
97
|
+
__SUCCESSFUL_AUTHENTICATION = "Successful authentication"
|
|
98
|
+
|
|
99
|
+
#
|
|
100
|
+
def _initialize_token(self):
|
|
101
|
+
self._access_token = None
|
|
102
|
+
self._refresh_token = None
|
|
103
|
+
self._creation_time = None
|
|
104
|
+
self._expiration_time = None
|
|
105
|
+
self.init_status = None
|
|
106
|
+
match self._mode:
|
|
107
|
+
case AuthenticationMode.INTERACTIVE:
|
|
108
|
+
self._app = msal.PublicClientApplication(
|
|
109
|
+
self._client_id, authority=self._authority_url)
|
|
110
|
+
self.__acquire_token_interactive()
|
|
111
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
112
|
+
self._app = msal.ConfidentialClientApplication(
|
|
113
|
+
self._client_id, authority=self._authority_url, client_credential=self._client_secret)
|
|
114
|
+
self.__acquire_token_noninteractive_with_secret()
|
|
115
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_ADFS:
|
|
116
|
+
self.__acquire_token_noninteractive_with_adfs()
|
|
117
|
+
|
|
118
|
+
#
|
|
68
119
|
@classmethod
|
|
69
120
|
def interactive(
|
|
70
121
|
cls,
|
|
@@ -84,8 +135,9 @@ class AdfsAuthentication():
|
|
|
84
135
|
mode=mode
|
|
85
136
|
)
|
|
86
137
|
|
|
138
|
+
#
|
|
87
139
|
@classmethod
|
|
88
|
-
def
|
|
140
|
+
def noninteractive_with_secret(
|
|
89
141
|
cls,
|
|
90
142
|
client_id: str,
|
|
91
143
|
sdmx_resource_id: str,
|
|
@@ -103,8 +155,9 @@ class AdfsAuthentication():
|
|
|
103
155
|
mode=mode
|
|
104
156
|
)
|
|
105
157
|
|
|
158
|
+
#
|
|
106
159
|
@classmethod
|
|
107
|
-
def
|
|
160
|
+
def noninteractive_with_adfs(
|
|
108
161
|
cls,
|
|
109
162
|
client_id: str,
|
|
110
163
|
sdmx_resource_id: str,
|
|
@@ -117,173 +170,129 @@ class AdfsAuthentication():
|
|
|
117
170
|
token_url=token_url,
|
|
118
171
|
mode=mode)
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
def get_token(self):
|
|
122
|
-
self.__access_token = None
|
|
123
|
-
self.__refresh_token = None
|
|
124
|
-
self.__creation_time = None
|
|
125
|
-
self.__expiration_time = None
|
|
126
|
-
self.result = None
|
|
127
|
-
match self.mode:
|
|
128
|
-
case AuthenticationMode.INTERACTIVE:
|
|
129
|
-
self._acquire_token_interactive()
|
|
130
|
-
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
131
|
-
self._acquire_token_noninteractive_with_secret()
|
|
132
|
-
case AuthenticationMode.NONINTERACTIVE_WITH_ADFS:
|
|
133
|
-
self._acquire_token_noninteractive_with_adfs()
|
|
134
|
-
|
|
135
|
-
return self.__access_token
|
|
136
|
-
|
|
137
|
-
# Check the token is not expired
|
|
138
|
-
def is_access_token_expired(self):
|
|
139
|
-
# convert the timestamp to a datetime object
|
|
140
|
-
if datetime.fromtimestamp(self.__expiration_time) is not None:
|
|
141
|
-
if datetime.now() <= datetime.fromtimestamp(self.__expiration_time):
|
|
142
|
-
return False
|
|
143
|
-
else:
|
|
144
|
-
return True
|
|
145
|
-
else:
|
|
146
|
-
return True
|
|
147
|
-
|
|
148
173
|
# Authentication interactively - aka Authorization Code flow
|
|
149
|
-
def
|
|
174
|
+
def __acquire_token_interactive(self):
|
|
150
175
|
try:
|
|
151
|
-
|
|
152
176
|
# We now check the cache to see
|
|
153
177
|
# whether we already have some accounts that the end user already used to sign in before.
|
|
154
|
-
accounts = self.
|
|
178
|
+
accounts = self._app.get_accounts()
|
|
155
179
|
if accounts:
|
|
156
180
|
account = accounts[0]
|
|
157
181
|
else:
|
|
158
182
|
account = None
|
|
159
183
|
|
|
160
184
|
# Firstly, looks up a access_token from cache, or using a refresh token
|
|
161
|
-
response_silent = self.
|
|
162
|
-
self.
|
|
185
|
+
response_silent = self._app.acquire_token_silent(
|
|
186
|
+
self._scopes, account=account)
|
|
163
187
|
if not response_silent:
|
|
164
188
|
# Prompt the user to sign in interactively
|
|
165
|
-
response_interactive = self.
|
|
166
|
-
scopes=self.
|
|
189
|
+
response_interactive = self._app.acquire_token_interactive(
|
|
190
|
+
scopes=self._scopes, port=self._redirect_port)
|
|
167
191
|
if "access_token" in response_interactive:
|
|
168
|
-
self.
|
|
169
|
-
self.
|
|
170
|
-
self.
|
|
171
|
-
self.
|
|
172
|
-
self.
|
|
192
|
+
self._access_token = response_interactive.get("access_token")
|
|
193
|
+
self._refresh_token = response_interactive.get("refresh_token")
|
|
194
|
+
self._creation_time = time.time()
|
|
195
|
+
self._expiration_time = time.time() + int(response_interactive.get("expires_in")) - 60 # one minute margin
|
|
196
|
+
self.init_status = self.__SUCCESSFUL_AUTHENTICATION
|
|
173
197
|
else:
|
|
174
|
-
self.
|
|
198
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{response_interactive.get("error")} Error description: {response_interactive.get("error_description")}'
|
|
175
199
|
else:
|
|
176
200
|
if "access_token" in response_silent:
|
|
177
|
-
self.
|
|
178
|
-
self.
|
|
179
|
-
self.
|
|
180
|
-
self.
|
|
181
|
-
self.
|
|
201
|
+
self._access_token = response_silent.get("access_token")
|
|
202
|
+
self._refresh_token = response_silent.get("refresh_token")
|
|
203
|
+
self._creation_time = time.time()
|
|
204
|
+
self._expiration_time = time.time() + int(response_silent.get("expires_in")) - 60 # one minute margin
|
|
205
|
+
self.init_status = self.__SUCCESSFUL_AUTHENTICATION
|
|
182
206
|
else:
|
|
183
|
-
self.
|
|
207
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{response_silent.get("error")} Error description: {response_silent.get("error_description")}'
|
|
184
208
|
except Exception as err:
|
|
185
|
-
self.
|
|
186
|
-
|
|
209
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
|
|
187
210
|
|
|
188
211
|
# Authentication non-interactively using any account - aka Client Credentials flow
|
|
189
|
-
def
|
|
212
|
+
def __acquire_token_noninteractive_with_secret(self):
|
|
190
213
|
try:
|
|
191
|
-
response = self.
|
|
214
|
+
response = self._app.acquire_token_for_client(scopes=self._scopes)
|
|
192
215
|
if "access_token" in response:
|
|
193
|
-
self.
|
|
194
|
-
self.
|
|
195
|
-
self.
|
|
196
|
-
self.
|
|
216
|
+
self._access_token = response.get("access_token")
|
|
217
|
+
self._creation_time = time.time()
|
|
218
|
+
self._expiration_time = time.time() + int(response.get("expires_in")) - 60 # one minute margin
|
|
219
|
+
self.init_status = self.__SUCCESSFUL_AUTHENTICATION
|
|
197
220
|
else:
|
|
198
|
-
self.
|
|
221
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{response.get("error")} Error description: {response.get("error_description")}'
|
|
199
222
|
|
|
200
223
|
except Exception as err:
|
|
201
|
-
self.
|
|
202
|
-
|
|
224
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
|
|
203
225
|
|
|
204
226
|
# Authentication non-interactively using service account - aka Windows Client Authentication
|
|
205
|
-
def
|
|
227
|
+
def __acquire_token_noninteractive_with_adfs(self):
|
|
206
228
|
try:
|
|
207
229
|
headers = {
|
|
208
230
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
payload = {
|
|
212
|
-
'client_id': self.
|
|
234
|
+
'client_id': self._client_id,
|
|
213
235
|
'use_windows_client_authentication': 'true',
|
|
214
236
|
'grant_type': 'client_credentials',
|
|
215
237
|
'scope': 'openid',
|
|
216
|
-
'resource': self.
|
|
238
|
+
'resource': self._sdmx_resource_id
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
kerberos_auth = requests_kerberos.HTTPKerberosAuth(
|
|
220
242
|
mutual_authentication=requests_kerberos.OPTIONAL, force_preemptive=True)
|
|
221
|
-
response = requests.post(url=self.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
response = requests.post(url=self._token_url, data=payload, auth=kerberos_auth)
|
|
244
|
+
|
|
245
|
+
# If the response object cannot be converted to json, return an error
|
|
246
|
+
results_json = None
|
|
247
|
+
try:
|
|
248
|
+
results_json = json.loads(response.text)
|
|
249
|
+
if response.status_code == 200:
|
|
250
|
+
self._access_token = results_json['access_token']
|
|
251
|
+
self._creation_time = time.time()
|
|
252
|
+
self._expiration_time = time.time() + int(results_json['expires_in']) - 60 # one minute margin
|
|
253
|
+
self.init_status = self.__SUCCESSFUL_AUTHENTICATION
|
|
254
|
+
else:
|
|
255
|
+
message = f'{self.__ERROR_OCCURRED} Error code: {response.status_code}'
|
|
256
|
+
if len(str(response.reason)) > 0:
|
|
257
|
+
message += os.linesep + 'Reason: ' + str(response.reason) + os.linesep
|
|
258
|
+
if len(response.text) > 0:
|
|
259
|
+
message += f'{self.__ERROR_OCCURRED}{results_json.get("error")} Error description: {results_json.get("error_description")}\n'
|
|
260
|
+
|
|
261
|
+
self.init_status = message
|
|
262
|
+
except ValueError as err:
|
|
263
|
+
self.init_status = self.__ERROR_OCCURRED + os.linesep
|
|
264
|
+
if len(str(response.status_code)) > 0:
|
|
265
|
+
self.init_status += 'Error code: ' + str(response.status_code) + os.linesep
|
|
266
|
+
if len(str(response.reason)) > 0:
|
|
267
|
+
self.init_status += 'Reason: ' + str(response.reason) + os.linesep
|
|
268
|
+
if len(response.text) > 0:
|
|
269
|
+
self.init_status += str(response.text)
|
|
270
|
+
else:
|
|
271
|
+
self.init_status += str(err)
|
|
237
272
|
except Exception as err:
|
|
238
|
-
self.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# class to manage
|
|
242
|
-
class KeycloakAuthentication():
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
self
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
proxy: str | None,
|
|
258
|
-
scopes: list[str] = []
|
|
259
|
-
):
|
|
260
|
-
self.__token = None
|
|
261
|
-
self.result = None
|
|
262
|
-
self.mode = mode
|
|
263
|
-
self.client_id = client_id
|
|
264
|
-
self.client_secret = client_secret
|
|
265
|
-
self.token_url = token_url
|
|
266
|
-
self.user = user
|
|
267
|
-
self.password = password
|
|
268
|
-
self.scopes = scopes #TODO check if needs explicit adjustment
|
|
269
|
-
|
|
270
|
-
if proxy:
|
|
271
|
-
self.proxies = {
|
|
272
|
-
"http": proxy,
|
|
273
|
-
"https": proxy
|
|
274
|
-
}
|
|
275
|
-
else:
|
|
276
|
-
self.proxies = None
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def __enter__(self):
|
|
280
|
-
return self
|
|
281
|
-
|
|
282
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
|
283
|
-
self.__token = None
|
|
273
|
+
self.init_status = f'{self.__ERROR_OCCURRED} {err}\n'
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# sub class to manage Keycloak authentication
|
|
277
|
+
class KeycloakAuthentication(Authentication):
|
|
278
|
+
# private constants
|
|
279
|
+
__ERROR_OCCURRED = "An error occurred: "
|
|
280
|
+
__SUCCESSFUL_AUTHENTICATION = "Successful authentication"
|
|
281
|
+
|
|
282
|
+
#
|
|
283
|
+
def _initialize_token(self):
|
|
284
|
+
self._access_token = None
|
|
285
|
+
self._refresh_token = None
|
|
286
|
+
self._creation_time = None
|
|
287
|
+
self._expiration_time = None
|
|
288
|
+
self.init_status = None
|
|
289
|
+
match self._mode:
|
|
290
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
291
|
+
self.__acquire_token_noninteractive_with_secret()
|
|
284
292
|
|
|
293
|
+
#
|
|
285
294
|
@classmethod
|
|
286
|
-
def
|
|
295
|
+
def noninteractive_with_secret(
|
|
287
296
|
cls,
|
|
288
297
|
token_url: str,
|
|
289
298
|
user: str,
|
|
@@ -305,31 +314,41 @@ class KeycloakAuthentication():
|
|
|
305
314
|
mode=mode
|
|
306
315
|
)
|
|
307
316
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
317
|
+
# Authentication non-interactively using any account - aka Client Credentials flow
|
|
318
|
+
def __acquire_token_noninteractive_with_secret(self):
|
|
319
|
+
try:
|
|
320
|
+
payload = {
|
|
321
|
+
'grant_type': 'password',
|
|
322
|
+
'client_id': self._client_id,
|
|
323
|
+
'client_secret': self._client_secret,
|
|
324
|
+
'username': self._user,
|
|
325
|
+
'password': self._password
|
|
326
|
+
}
|
|
314
327
|
|
|
315
|
-
|
|
328
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
316
329
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
response = requests.post(self._token_url, proxies=self._proxies, headers=headers, data=payload)
|
|
331
|
+
if response.status_code not in {200, 201}:
|
|
332
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{response}'
|
|
333
|
+
else:
|
|
334
|
+
# If the response object cannot be converted to json, return an error
|
|
335
|
+
results_json = None
|
|
336
|
+
try:
|
|
337
|
+
results_json = json.loads(response.text)
|
|
338
|
+
self._access_token = results_json['access_token']
|
|
339
|
+
self._refresh_token = results_json['refresh_token']
|
|
340
|
+
self._creation_time = time.time()
|
|
341
|
+
self._expiration_time = time.time() + int(results_json['expires_in']) - 60 # one minute margin
|
|
342
|
+
self.init_status = self.__SUCCESSFUL_AUTHENTICATION
|
|
343
|
+
except ValueError as err:
|
|
344
|
+
self.init_status = self.__ERROR_OCCURRED + os.linesep
|
|
345
|
+
if len(str(response.status_code)) > 0:
|
|
346
|
+
self.init_status += 'Error code: ' + str(response.status_code) + os.linesep
|
|
347
|
+
if len(str(response.reason)) > 0:
|
|
348
|
+
self.init_status += 'Reason: ' + str(response.reason) + os.linesep
|
|
349
|
+
if len(response.text) > 0:
|
|
350
|
+
self.init_status += str(response.text)
|
|
351
|
+
else:
|
|
352
|
+
self.init_status += str(err)
|
|
353
|
+
except Exception as err:
|
|
354
|
+
self.init_status = f'{self.__ERROR_OCCURRED}{err}\n'
|