cornflow 2.0.0a11__py3-none-any.whl → 2.0.0a12__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.
- airflow_config/airflow_local_settings.py +1 -1
- cornflow/app.py +8 -3
- cornflow/cli/migrations.py +23 -3
- cornflow/cli/service.py +18 -18
- cornflow/cli/utils.py +16 -1
- cornflow/commands/dag.py +1 -1
- cornflow/config.py +13 -8
- cornflow/endpoints/__init__.py +8 -2
- cornflow/endpoints/alarms.py +66 -2
- cornflow/endpoints/data_check.py +53 -26
- cornflow/endpoints/execution.py +387 -132
- cornflow/endpoints/login.py +81 -63
- cornflow/endpoints/meta_resource.py +11 -3
- cornflow/migrations/versions/999b98e24225.py +34 -0
- cornflow/models/base_data_model.py +4 -32
- cornflow/models/execution.py +2 -3
- cornflow/models/meta_models.py +28 -22
- cornflow/models/user.py +7 -10
- cornflow/schemas/alarms.py +8 -0
- cornflow/schemas/execution.py +1 -1
- cornflow/schemas/query.py +2 -1
- cornflow/schemas/user.py +5 -20
- cornflow/shared/authentication/auth.py +201 -264
- cornflow/shared/const.py +3 -14
- cornflow/shared/databricks.py +5 -1
- cornflow/tests/const.py +1 -0
- cornflow/tests/custom_test_case.py +77 -26
- cornflow/tests/unit/test_actions.py +2 -2
- cornflow/tests/unit/test_alarms.py +55 -1
- cornflow/tests/unit/test_apiview.py +108 -3
- cornflow/tests/unit/test_cases.py +20 -29
- cornflow/tests/unit/test_cli.py +6 -5
- cornflow/tests/unit/test_commands.py +3 -3
- cornflow/tests/unit/test_dags.py +5 -6
- cornflow/tests/unit/test_executions.py +443 -123
- cornflow/tests/unit/test_instances.py +14 -2
- cornflow/tests/unit/test_instances_file.py +1 -1
- cornflow/tests/unit/test_licenses.py +1 -1
- cornflow/tests/unit/test_log_in.py +230 -207
- cornflow/tests/unit/test_permissions.py +8 -8
- cornflow/tests/unit/test_roles.py +48 -10
- cornflow/tests/unit/test_schemas.py +1 -1
- cornflow/tests/unit/test_tables.py +7 -7
- cornflow/tests/unit/test_token.py +19 -5
- cornflow/tests/unit/test_users.py +22 -6
- cornflow/tests/unit/tools.py +75 -10
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/METADATA +16 -15
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/RECORD +51 -51
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/WHEEL +1 -1
- cornflow/endpoints/execution_databricks.py +0 -808
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/entry_points.txt +0 -0
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,9 @@ Unit test for the log in endpoint
|
|
5
5
|
import json
|
6
6
|
import logging as log
|
7
7
|
from unittest import mock
|
8
|
+
import requests
|
9
|
+
import jwt
|
10
|
+
from datetime import datetime, timedelta
|
8
11
|
|
9
12
|
from flask import current_app
|
10
13
|
|
@@ -14,7 +17,7 @@ from cornflow.commands.access import access_init_command
|
|
14
17
|
from cornflow.commands.dag import register_deployed_dags_command_test
|
15
18
|
from cornflow.models import UserModel
|
16
19
|
from cornflow.shared import db
|
17
|
-
from cornflow.shared.const import SERVICE_ROLE,
|
20
|
+
from cornflow.shared.const import SERVICE_ROLE, INTERNAL_TOKEN_ISSUER, AUTH_OID
|
18
21
|
from cornflow.tests.const import LOGIN_URL
|
19
22
|
from cornflow.tests.custom_test_case import CustomTestCase, LoginTestCases
|
20
23
|
|
@@ -65,7 +68,7 @@ class TestLogIn(LoginTestCases.LoginEndpoint):
|
|
65
68
|
self.assertIn("Error in generating user token", response.json["error"])
|
66
69
|
|
67
70
|
|
68
|
-
class
|
71
|
+
class TestLogInOpenAuth(CustomTestCase):
|
69
72
|
def create_app(self):
|
70
73
|
"""
|
71
74
|
Creates and configures a Flask application for testing.
|
@@ -92,9 +95,9 @@ class TestLogInOpenAuthNoConfig(CustomTestCase):
|
|
92
95
|
test_user.save()
|
93
96
|
|
94
97
|
self.user_data.pop("email")
|
95
|
-
|
96
98
|
self.test_user_id = test_user.id
|
97
99
|
|
100
|
+
# Setup service user
|
98
101
|
self.service_data = {
|
99
102
|
"username": "service_user",
|
100
103
|
"email": "service@test.com",
|
@@ -120,283 +123,294 @@ class TestLogInOpenAuthNoConfig(CustomTestCase):
|
|
120
123
|
)
|
121
124
|
|
122
125
|
self.assertEqual(400, response.status_code)
|
123
|
-
self.assertEqual(
|
124
|
-
|
125
|
-
|
126
|
-
"""
|
127
|
-
Tests that a user can not log in with username and password
|
128
|
-
"""
|
129
|
-
response = self.client.post(
|
130
|
-
LOGIN_URL,
|
131
|
-
data=json.dumps(self.user_data),
|
132
|
-
headers={"Content-Type": "application/json"},
|
126
|
+
self.assertEqual(
|
127
|
+
response.json["error"],
|
128
|
+
"Must provide a token in Authorization header. Cannot log in with username and password",
|
133
129
|
)
|
134
130
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
class TestLogInOpenAuthAzure(CustomTestCase):
|
140
|
-
def create_app(self):
|
131
|
+
@mock.patch("cornflow.shared.authentication.auth.Auth.get_public_keys")
|
132
|
+
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
133
|
+
def test_kid_not_in_public_keys(self, mock_jwt, mock_get_public_keys):
|
141
134
|
"""
|
142
|
-
|
143
|
-
|
144
|
-
:returns: A configured Flask application instance
|
145
|
-
:rtype: Flask
|
135
|
+
Tests token validation failure when the kid is not found in public keys
|
146
136
|
"""
|
147
|
-
|
148
|
-
|
149
|
-
app.config["OID_PROVIDER"] = OID_AZURE
|
150
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
151
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
152
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
153
|
-
return app
|
137
|
+
# Import the real exceptions to ensure they are preserved
|
138
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
154
139
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
access_init_command(verbose=False)
|
159
|
-
register_deployed_dags_command_test(verbose=False)
|
140
|
+
# Keep the real exception classes in the mock
|
141
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
142
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
160
143
|
|
161
|
-
|
162
|
-
"username": "service_user",
|
163
|
-
"email": "service@test.com",
|
164
|
-
"password": "Testpassword1!",
|
165
|
-
}
|
144
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
166
145
|
|
167
|
-
|
168
|
-
|
146
|
+
# Mock jwt.decode to return different results based on arguments
|
147
|
+
def decode_side_effect(*args, **kwargs):
|
148
|
+
if kwargs.get("options", {}).get("verify_signature") is False:
|
149
|
+
return {"iss": current_app.config.get("OID_PROVIDER", "valid_issuer")}
|
150
|
+
raise InvalidTokenError("Invalid token")
|
169
151
|
|
170
|
-
|
171
|
-
self.service_user_id = service_user.id
|
152
|
+
mock_jwt.decode.side_effect = decode_side_effect
|
172
153
|
|
173
|
-
|
154
|
+
# Mock get_public_keys to return keys that don't contain our kid
|
155
|
+
mock_get_public_keys.return_value = {"different_kid": "some_key"}
|
174
156
|
|
175
|
-
def test_service_user_login_first_fail(self):
|
176
|
-
"""
|
177
|
-
Tests that a service user can not log in with username and password
|
178
|
-
"""
|
179
157
|
response = self.client.post(
|
180
158
|
LOGIN_URL,
|
181
|
-
data=json.dumps({
|
182
|
-
headers={
|
159
|
+
data=json.dumps({}),
|
160
|
+
headers={
|
161
|
+
"Content-Type": "application/json",
|
162
|
+
"Authorization": "Bearer some_token",
|
163
|
+
},
|
183
164
|
)
|
184
165
|
|
185
166
|
self.assertEqual(400, response.status_code)
|
186
|
-
self.assertEqual(
|
167
|
+
self.assertEqual(
|
168
|
+
response.json["error"], "Invalid token: Unknown key identifier (kid)"
|
169
|
+
)
|
187
170
|
|
188
171
|
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
189
|
-
def
|
172
|
+
def test_missing_kid_in_token(self, mock_jwt):
|
190
173
|
"""
|
191
|
-
Tests
|
174
|
+
Tests token validation failure when the token header is missing the kid
|
192
175
|
"""
|
193
|
-
|
176
|
+
# Import the real exceptions to ensure they are preserved
|
177
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
178
|
+
|
179
|
+
# Keep the real exception classes in the mock
|
180
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
181
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
182
|
+
|
183
|
+
# Mock jwt.get_unverified_header to return a header without kid
|
184
|
+
mock_jwt.get_unverified_header.return_value = {"alg": "RS256"}
|
185
|
+
|
194
186
|
response = self.client.post(
|
195
187
|
LOGIN_URL,
|
196
|
-
data=json.dumps({
|
197
|
-
headers={
|
188
|
+
data=json.dumps({}),
|
189
|
+
headers={
|
190
|
+
"Content-Type": "application/json",
|
191
|
+
"Authorization": "Bearer some_token",
|
192
|
+
},
|
198
193
|
)
|
199
194
|
|
200
195
|
self.assertEqual(400, response.status_code)
|
201
|
-
self.assertEqual(
|
196
|
+
self.assertEqual(
|
197
|
+
response.json["error"],
|
198
|
+
"Invalid token: Missing key identifier (kid) in token header",
|
199
|
+
)
|
202
200
|
|
201
|
+
@mock.patch("cornflow.shared.authentication.auth.requests.get")
|
203
202
|
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
204
|
-
def
|
203
|
+
def test_public_keys_fetch_fail(self, mock_jwt, mock_get):
|
205
204
|
"""
|
206
|
-
Tests
|
205
|
+
Tests failure when trying to fetch public keys from the OIDC provider
|
207
206
|
"""
|
208
|
-
|
207
|
+
# Import the real exceptions to ensure they are preserved
|
208
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
209
|
+
|
210
|
+
# Keep the real exception classes in the mock
|
211
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
212
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
213
|
+
|
214
|
+
# Clear the cache
|
215
|
+
from cornflow.shared.authentication.auth import public_keys_cache
|
216
|
+
|
217
|
+
public_keys_cache.clear()
|
218
|
+
|
219
|
+
# Mock jwt to pass initial validation
|
220
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
221
|
+
mock_jwt.decode.side_effect = lambda *args, **kwargs: (
|
222
|
+
{"iss": current_app.config["OID_PROVIDER"]}
|
223
|
+
if kwargs.get("options", {}).get("verify_signature") is False
|
224
|
+
else {}
|
225
|
+
)
|
226
|
+
|
227
|
+
# Mock get to fail
|
228
|
+
mock_get.side_effect = requests.exceptions.RequestException(
|
229
|
+
"Failed to get keys"
|
230
|
+
)
|
231
|
+
|
209
232
|
response = self.client.post(
|
210
233
|
LOGIN_URL,
|
211
|
-
data=json.dumps({
|
212
|
-
headers={
|
234
|
+
data=json.dumps({}),
|
235
|
+
headers={
|
236
|
+
"Content-Type": "application/json",
|
237
|
+
"Authorization": "Bearer some_token",
|
238
|
+
},
|
213
239
|
)
|
214
240
|
|
215
241
|
self.assertEqual(400, response.status_code)
|
216
|
-
self.
|
217
|
-
|
242
|
+
self.assertEqual(
|
243
|
+
response.json["error"],
|
244
|
+
"Failed to fetch public keys from authentication provider",
|
218
245
|
)
|
219
246
|
|
220
|
-
@mock.patch("cornflow.
|
221
|
-
def test_service_user_login_no_fail(self,
|
222
|
-
|
247
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
248
|
+
def test_service_user_login_no_fail(self, mock_decode):
|
249
|
+
"""
|
250
|
+
Tests successful login for an existing service user with valid token
|
251
|
+
"""
|
252
|
+
mock_decode.return_value = {"sub": "service_user"}
|
223
253
|
response = self.client.post(
|
224
254
|
LOGIN_URL,
|
225
|
-
data=json.dumps({
|
226
|
-
headers={
|
255
|
+
data=json.dumps({}),
|
256
|
+
headers={
|
257
|
+
"Content-Type": "application/json",
|
258
|
+
"Authorization": "Bearer some_token",
|
259
|
+
},
|
227
260
|
)
|
228
261
|
|
229
|
-
print(response.json)
|
230
|
-
|
231
262
|
self.assertEqual(200, response.status_code)
|
232
263
|
self.assertEqual(self.service_user_id, response.json["id"])
|
233
264
|
|
234
|
-
@mock.patch("cornflow.
|
235
|
-
def test_new_user_login_no_fail(self,
|
236
|
-
|
265
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
266
|
+
def test_new_user_login_no_fail(self, mock_decode):
|
267
|
+
"""
|
268
|
+
Tests successful login and creation of a new user with valid token
|
269
|
+
"""
|
270
|
+
mock_decode.return_value = {"sub": "test_user"}
|
237
271
|
response = self.client.post(
|
238
272
|
LOGIN_URL,
|
239
|
-
data=json.dumps({
|
240
|
-
headers={
|
273
|
+
data=json.dumps({}),
|
274
|
+
headers={
|
275
|
+
"Content-Type": "application/json",
|
276
|
+
"Authorization": "Bearer some_token",
|
277
|
+
},
|
241
278
|
)
|
242
279
|
|
243
280
|
self.assertEqual(200, response.status_code)
|
244
281
|
self.assertEqual(self.service_user_id + 1, response.json["id"])
|
245
282
|
|
246
|
-
|
247
|
-
|
248
|
-
def
|
283
|
+
@mock.patch("cornflow.shared.authentication.Auth.verify_token")
|
284
|
+
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
285
|
+
def test_public_keys_caching(self, mock_jwt, mock_verify_token):
|
249
286
|
"""
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
:rtype: Flask
|
287
|
+
Tests that public keys are cached and reused for subsequent requests.
|
288
|
+
Also verifies that when a new kid is encountered that's not in the cache,
|
289
|
+
the system fetches fresh keys from the provider.
|
254
290
|
"""
|
255
|
-
|
256
|
-
|
257
|
-
app.config["OID_PROVIDER"] = OID_GOOGLE
|
258
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
259
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
260
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
261
|
-
return app
|
291
|
+
# Import the real exceptions to ensure they are preserved
|
292
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
262
293
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
access_init_command(verbose=False)
|
267
|
-
register_deployed_dags_command_test(verbose=False)
|
294
|
+
# Keep the real exception classes in the mock
|
295
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
296
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
268
297
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
"
|
273
|
-
|
298
|
+
# Mock jwt to return valid unverified header and payload
|
299
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
300
|
+
mock_jwt.decode.side_effect = lambda *args, **kwargs: (
|
301
|
+
{"iss": current_app.config["OID_PROVIDER"]}
|
302
|
+
if kwargs.get("options", {}).get("verify_signature") is False
|
303
|
+
else {"sub": "test_user", "email": "test_user@test.com"}
|
304
|
+
)
|
274
305
|
|
275
|
-
|
276
|
-
|
306
|
+
# Mock verify_token to always return a valid token payload
|
307
|
+
mock_verify_token.return_value = {
|
308
|
+
"sub": "test_user",
|
309
|
+
"email": "test_user@test.com",
|
310
|
+
}
|
277
311
|
|
278
|
-
|
279
|
-
|
312
|
+
# Make first request
|
313
|
+
response = self.client.post(
|
314
|
+
LOGIN_URL,
|
315
|
+
data=json.dumps({}),
|
316
|
+
headers={
|
317
|
+
"Content-Type": "application/json",
|
318
|
+
"Authorization": "Bearer some_token",
|
319
|
+
},
|
320
|
+
)
|
280
321
|
|
281
|
-
self.
|
322
|
+
self.assertEqual(200, response.status_code)
|
282
323
|
|
283
|
-
|
284
|
-
"""
|
285
|
-
Tests that a service user can not log in with username and password
|
286
|
-
"""
|
324
|
+
# Make second request
|
287
325
|
response = self.client.post(
|
288
326
|
LOGIN_URL,
|
289
|
-
data=json.dumps({
|
290
|
-
headers={
|
327
|
+
data=json.dumps({}),
|
328
|
+
headers={
|
329
|
+
"Content-Type": "application/json",
|
330
|
+
"Authorization": "Bearer some_token",
|
331
|
+
},
|
291
332
|
)
|
292
333
|
|
293
|
-
self.assertEqual(
|
294
|
-
self.assertEqual(
|
295
|
-
response.json["error"], "The selected OID provider is not implemented"
|
296
|
-
)
|
334
|
+
self.assertEqual(200, response.status_code)
|
297
335
|
|
336
|
+
# Verify token was verified twice
|
337
|
+
self.assertEqual(2, mock_verify_token.call_count)
|
298
338
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
339
|
+
# Now test with a different token
|
340
|
+
response = self.client.post(
|
341
|
+
LOGIN_URL,
|
342
|
+
data=json.dumps({}),
|
343
|
+
headers={
|
344
|
+
"Content-Type": "application/json",
|
345
|
+
"Authorization": "Bearer some_different_token",
|
346
|
+
},
|
347
|
+
)
|
303
348
|
|
304
|
-
|
305
|
-
:rtype: Flask
|
306
|
-
"""
|
307
|
-
app = create_app("testing-oauth")
|
308
|
-
app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 0
|
309
|
-
app.config["OID_PROVIDER"] = OID_NONE
|
310
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
311
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
312
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
313
|
-
return app
|
349
|
+
self.assertEqual(200, response.status_code)
|
314
350
|
|
315
|
-
|
316
|
-
|
317
|
-
db.create_all()
|
318
|
-
access_init_command(verbose=False)
|
319
|
-
register_deployed_dags_command_test(verbose=False)
|
351
|
+
# Verify token was verified a third time
|
352
|
+
self.assertEqual(3, mock_verify_token.call_count)
|
320
353
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
354
|
+
def test_old_token(self):
|
355
|
+
"""
|
356
|
+
Tests using an expired token.
|
357
|
+
"""
|
358
|
+
# Generate an expired token
|
359
|
+
payload = {
|
360
|
+
# Token expired 1 hour ago
|
361
|
+
"exp": datetime.utcnow() - timedelta(hours=1),
|
362
|
+
# Token created 2 hours ago
|
363
|
+
"iat": datetime.utcnow() - timedelta(hours=2),
|
364
|
+
"sub": "testname",
|
365
|
+
"iss": INTERNAL_TOKEN_ISSUER,
|
325
366
|
}
|
326
367
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
self.service_data.pop("email")
|
331
|
-
self.service_user_id = service_user.id
|
332
|
-
|
333
|
-
self.assign_role(self.service_user_id, SERVICE_ROLE)
|
368
|
+
expired_token = jwt.encode(
|
369
|
+
payload, current_app.config["SECRET_TOKEN_KEY"], algorithm="HS256"
|
370
|
+
)
|
334
371
|
|
335
|
-
def test_service_user_login(self):
|
336
|
-
"""
|
337
|
-
Tests that a service user can not log in with username and password
|
338
|
-
"""
|
339
372
|
response = self.client.post(
|
340
373
|
LOGIN_URL,
|
341
|
-
data=json.dumps({
|
342
|
-
headers={
|
374
|
+
data=json.dumps({}),
|
375
|
+
headers={
|
376
|
+
"Content-Type": "application/json",
|
377
|
+
"Authorization": f"Bearer {expired_token}",
|
378
|
+
},
|
343
379
|
)
|
344
380
|
|
345
|
-
self.assertEqual(
|
381
|
+
self.assertEqual(400, response.status_code)
|
346
382
|
self.assertEqual(
|
347
|
-
|
383
|
+
"The token has expired, please login again", response.json["error"]
|
348
384
|
)
|
349
385
|
|
350
|
-
|
351
|
-
class TestLogInOpenAuthOther(CustomTestCase):
|
352
|
-
def create_app(self):
|
386
|
+
def test_missing_auth_header(self):
|
353
387
|
"""
|
354
|
-
|
355
|
-
|
356
|
-
:returns: A configured Flask application instance
|
357
|
-
:rtype: Flask
|
388
|
+
Tests that missing Authorization header raises proper error
|
358
389
|
"""
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
363
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
364
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
365
|
-
return app
|
366
|
-
|
367
|
-
def setUp(self):
|
368
|
-
log.root.setLevel(current_app.config["LOG_LEVEL"])
|
369
|
-
db.create_all()
|
370
|
-
access_init_command(verbose=False)
|
371
|
-
register_deployed_dags_command_test(verbose=False)
|
372
|
-
|
373
|
-
self.service_data = {
|
374
|
-
"username": "service_user",
|
375
|
-
"email": "service@test.com",
|
376
|
-
"password": "Testpassword1!",
|
377
|
-
}
|
378
|
-
|
379
|
-
service_user = UserModel(data=self.service_data)
|
380
|
-
service_user.save()
|
381
|
-
|
382
|
-
self.service_data.pop("email")
|
383
|
-
self.service_user_id = service_user.id
|
390
|
+
response = self.client.post(
|
391
|
+
LOGIN_URL, data=json.dumps({}), headers={"Content-Type": "application/json"}
|
392
|
+
)
|
384
393
|
|
385
|
-
self.
|
394
|
+
self.assertEqual(400, response.status_code)
|
395
|
+
self.assertEqual(response.json["error"], "Authorization header is missing")
|
386
396
|
|
387
|
-
def
|
397
|
+
def test_invalid_auth_header(self):
|
388
398
|
"""
|
389
|
-
Tests that
|
399
|
+
Tests that malformed Authorization header raises proper error
|
390
400
|
"""
|
391
401
|
response = self.client.post(
|
392
402
|
LOGIN_URL,
|
393
|
-
data=json.dumps({
|
394
|
-
headers={
|
403
|
+
data=json.dumps({}),
|
404
|
+
headers={
|
405
|
+
"Content-Type": "application/json",
|
406
|
+
"Authorization": "Invalid Format",
|
407
|
+
},
|
395
408
|
)
|
396
409
|
|
397
|
-
self.assertEqual(
|
410
|
+
self.assertEqual(400, response.status_code)
|
398
411
|
self.assertEqual(
|
399
|
-
|
412
|
+
"Invalid Authorization header format. Must be 'Bearer <token>'",
|
413
|
+
response.json["error"],
|
400
414
|
)
|
401
415
|
|
402
416
|
|
@@ -410,6 +424,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
410
424
|
:rtype: Flask
|
411
425
|
"""
|
412
426
|
app = create_app("testing-oauth")
|
427
|
+
app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 1
|
413
428
|
return app
|
414
429
|
|
415
430
|
def setUp(self):
|
@@ -448,7 +463,6 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
448
463
|
"""
|
449
464
|
Tests that a service user can log in with username and password
|
450
465
|
"""
|
451
|
-
|
452
466
|
response = self.client.post(
|
453
467
|
LOGIN_URL,
|
454
468
|
data=json.dumps(self.service_data),
|
@@ -458,7 +472,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
458
472
|
self.assertEqual(200, response.status_code)
|
459
473
|
self.assertEqual(self.service_user_id, response.json["id"])
|
460
474
|
|
461
|
-
def
|
475
|
+
def test_no_credentials_error(self):
|
462
476
|
"""
|
463
477
|
Tests that a user can not log in without token or username and password
|
464
478
|
"""
|
@@ -469,6 +483,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
469
483
|
)
|
470
484
|
|
471
485
|
self.assertEqual(400, response.status_code)
|
486
|
+
self.assertEqual(response.json["error"], "Authorization header is missing")
|
472
487
|
|
473
488
|
def test_other_user_password(self):
|
474
489
|
"""
|
@@ -483,29 +498,37 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
483
498
|
self.assertEqual(400, response.status_code)
|
484
499
|
self.assertEqual(response.json["error"], "Invalid request")
|
485
500
|
|
486
|
-
|
501
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
502
|
+
def test_token_login(self, mock_decode):
|
487
503
|
"""
|
488
|
-
Tests that a user can log in with a token
|
504
|
+
Tests that a user can successfully log in with a valid token
|
489
505
|
"""
|
506
|
+
mock_decode.return_value = {"sub": "testname"}
|
507
|
+
|
490
508
|
response = self.client.post(
|
491
509
|
LOGIN_URL,
|
492
|
-
data=json.dumps({
|
493
|
-
headers={
|
510
|
+
data=json.dumps({}),
|
511
|
+
headers={
|
512
|
+
"Content-Type": "application/json",
|
513
|
+
"Authorization": "Bearer valid_token",
|
514
|
+
},
|
494
515
|
)
|
495
516
|
|
496
|
-
self.assertEqual(
|
497
|
-
self.assertEqual(
|
498
|
-
response.json["error"], "The OID provider configuration is not valid"
|
499
|
-
)
|
517
|
+
self.assertEqual(200, response.status_code)
|
518
|
+
self.assertEqual(self.test_user_id, response.json["id"])
|
500
519
|
|
501
|
-
def
|
520
|
+
def test_invalid_token_login(self):
|
502
521
|
"""
|
503
|
-
Tests that
|
522
|
+
Tests that login fails with an invalid token
|
504
523
|
"""
|
505
524
|
response = self.client.post(
|
506
525
|
LOGIN_URL,
|
507
|
-
data=json.dumps({
|
508
|
-
headers={
|
526
|
+
data=json.dumps({}),
|
527
|
+
headers={
|
528
|
+
"Content-Type": "application/json",
|
529
|
+
"Authorization": "Bearer invalid_token",
|
530
|
+
},
|
509
531
|
)
|
510
532
|
|
511
533
|
self.assertEqual(400, response.status_code)
|
534
|
+
self.assertEqual(response.json["error"], "Invalid token format or signature")
|