dotstat_io 0.2.7__py3-none-any.whl → 1.0.0__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 +195 -190
- dotstat_io/client.py +392 -0
- {dotstat_io-0.2.7.dist-info → dotstat_io-1.0.0.dist-info}/METADATA +49 -33
- dotstat_io-1.0.0.dist-info/RECORD +6 -0
- {dotstat_io-0.2.7.dist-info → dotstat_io-1.0.0.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 constants
|
|
22
|
+
_ERROR_OCCURRED = "An error occurred: "
|
|
23
|
+
_SUCCESS = "Successful authentication"
|
|
24
|
+
|
|
25
|
+
# protected variables
|
|
26
|
+
_access_token = None
|
|
27
|
+
_refresh_token = None
|
|
28
|
+
_creation_time = None
|
|
29
|
+
_expiration_time = None
|
|
18
30
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
__SUCCESS = "Successful authentication"
|
|
31
|
+
# public variables
|
|
32
|
+
init_status = None
|
|
22
33
|
|
|
23
|
-
# Initialise
|
|
34
|
+
# Initialise authentication
|
|
24
35
|
def __init__(
|
|
25
36
|
self,
|
|
37
|
+
client_id: str,
|
|
26
38
|
mode: AuthenticationMode,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
client_secret: str = None,
|
|
40
|
+
scopes: list[str] = [],
|
|
41
|
+
token_url: str = None,
|
|
42
|
+
sdmx_resource_id: str = None,
|
|
30
43
|
authority_url: str = None,
|
|
31
|
-
token_url: str = None,
|
|
32
44
|
redirect_port: int = 3000,
|
|
33
|
-
|
|
45
|
+
user: str = None,
|
|
46
|
+
password: str = None,
|
|
47
|
+
proxy: str = None
|
|
34
48
|
):
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
self.app = msal.ConfidentialClientApplication(
|
|
57
|
-
self.client_id, authority=self.authority_url, client_credential=self.client_secret)
|
|
49
|
+
self._client_id = client_id
|
|
50
|
+
self._mode = mode
|
|
51
|
+
self._client_secret = client_secret
|
|
52
|
+
self._scopes = scopes
|
|
53
|
+
self._token_url = token_url
|
|
54
|
+
self._sdmx_resource_id = sdmx_resource_id
|
|
55
|
+
self._authority_url = authority_url
|
|
56
|
+
self._redirect_port = redirect_port
|
|
57
|
+
self._user = user
|
|
58
|
+
self._password = password
|
|
59
|
+
|
|
60
|
+
if proxy:
|
|
61
|
+
self._proxies = {
|
|
62
|
+
"http": proxy,
|
|
63
|
+
"https": proxy
|
|
64
|
+
}
|
|
65
|
+
else:
|
|
66
|
+
self._proxies = None
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
self._initialize_token()
|
|
58
70
|
|
|
71
|
+
#
|
|
59
72
|
def __enter__(self):
|
|
60
73
|
return self
|
|
61
74
|
|
|
75
|
+
#
|
|
62
76
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
-
self.
|
|
77
|
+
self._access_token = None
|
|
78
|
+
self._refresh_token = None
|
|
79
|
+
self._creation_time = None
|
|
80
|
+
self._expiration_time = None
|
|
81
|
+
self.init_status = None
|
|
67
82
|
|
|
83
|
+
#
|
|
84
|
+
def _initialize_token(self):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
def get_token(self):
|
|
89
|
+
if (self._access_token is None) \
|
|
90
|
+
or (datetime.fromtimestamp(self._expiration_time) is not None \
|
|
91
|
+
and datetime.now() >= datetime.fromtimestamp(self._expiration_time)):
|
|
92
|
+
self._initialize_token()
|
|
93
|
+
|
|
94
|
+
return self._access_token
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# sub class to manage ADFS authentication using OIDC flows
|
|
98
|
+
class AdfsAuthentication(Authentication):
|
|
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,120 @@ 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._SUCCESS
|
|
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._SUCCESS
|
|
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._SUCCESS
|
|
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
|
-
|
|
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._SUCCESS
|
|
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:
|
|
225
263
|
if len(response.text) > 0:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
self.result = message
|
|
231
|
-
else:
|
|
232
|
-
self.__access_token = response.json()['access_token']
|
|
233
|
-
self.__creation_time = time.time()
|
|
234
|
-
self.__expiration_time = time.time() + int(response.json()['expires_in']) - 60 # one minute margin
|
|
235
|
-
self.result = self.__SUCCESS
|
|
236
|
-
|
|
264
|
+
self.init_status = self._ERROR_OCCURRED + os.linesep + str(response.text)
|
|
265
|
+
else:
|
|
266
|
+
self.init_status = self._ERROR_OCCURRED + os.linesep + str(err)
|
|
237
267
|
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
|
-
user: str,
|
|
254
|
-
password: str,
|
|
255
|
-
client_id: str,
|
|
256
|
-
client_secret: str,
|
|
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
|
|
268
|
+
self.init_status = f'{self._ERROR_OCCURRED} {err}\n'
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# sub class to manage Keycloak authentication
|
|
272
|
+
class KeycloakAuthentication(Authentication):
|
|
273
|
+
#
|
|
274
|
+
def _initialize_token(self):
|
|
275
|
+
self._access_token = None
|
|
276
|
+
self._refresh_token = None
|
|
277
|
+
self._creation_time = None
|
|
278
|
+
self._expiration_time = None
|
|
279
|
+
self.init_status = None
|
|
280
|
+
match self._mode:
|
|
281
|
+
case AuthenticationMode.NONINTERACTIVE_WITH_SECRET:
|
|
282
|
+
self.__acquire_token_noninteractive_with_secret()
|
|
284
283
|
|
|
284
|
+
#
|
|
285
285
|
@classmethod
|
|
286
|
-
def
|
|
286
|
+
def noninteractive_with_secret(
|
|
287
287
|
cls,
|
|
288
288
|
token_url: str,
|
|
289
289
|
user: str,
|
|
@@ -305,31 +305,36 @@ class KeycloakAuthentication():
|
|
|
305
305
|
mode=mode
|
|
306
306
|
)
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
# Authentication non-interactively using any account - aka Client Credentials flow
|
|
309
|
+
def __acquire_token_noninteractive_with_secret(self):
|
|
310
|
+
try:
|
|
311
|
+
payload = {
|
|
312
|
+
'grant_type': 'password',
|
|
313
|
+
'client_id': self._client_id,
|
|
314
|
+
'client_secret': self._client_secret,
|
|
315
|
+
'username': self._user,
|
|
316
|
+
'password': self._password
|
|
317
|
+
}
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
316
320
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
321
|
+
response = requests.post(self._token_url, proxies=self._proxies, headers=headers, data=payload)
|
|
322
|
+
if response.status_code not in {200, 201}:
|
|
323
|
+
self.init_status = f'{self._ERROR_OCCURRED}{response}'
|
|
324
|
+
else:
|
|
325
|
+
# If the response object cannot be converted to json, return an error
|
|
326
|
+
results_json = None
|
|
327
|
+
try:
|
|
328
|
+
results_json = json.loads(response.text)
|
|
329
|
+
self._access_token = results_json['access_token']
|
|
330
|
+
self._refresh_token = results_json['refresh_token']
|
|
331
|
+
self._creation_time = time.time()
|
|
332
|
+
self._expiration_time = time.time() + int(results_json['expires_in']) - 60 # one minute margin
|
|
333
|
+
self.init_status = self._SUCCESS
|
|
334
|
+
except ValueError as err:
|
|
335
|
+
if len(response.text) > 0:
|
|
336
|
+
self.init_status = self._ERROR_OCCURRED + os.linesep + str(response.text)
|
|
337
|
+
else:
|
|
338
|
+
self.init_status = self._ERROR_OCCURRED + os.linesep + str(err)
|
|
339
|
+
except Exception as err:
|
|
340
|
+
self.init_status = f'{self._ERROR_OCCURRED}{err}\n'
|