cornflow 1.1.5__py3-none-any.whl → 1.2.0a1__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.
- cornflow/app.py +8 -3
- cornflow/cli/service.py +14 -6
- cornflow/config.py +9 -10
- cornflow/endpoints/login.py +54 -57
- cornflow/schemas/user.py +3 -17
- cornflow/shared/authentication/auth.py +211 -254
- cornflow/shared/const.py +3 -14
- cornflow/tests/custom_test_case.py +42 -21
- cornflow/tests/unit/test_actions.py +2 -2
- cornflow/tests/unit/test_apiview.py +2 -2
- cornflow/tests/unit/test_cases.py +20 -29
- cornflow/tests/unit/test_dags.py +5 -5
- cornflow/tests/unit/test_instances.py +2 -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 +227 -207
- cornflow/tests/unit/test_permissions.py +8 -8
- cornflow/tests/unit/test_roles.py +10 -10
- cornflow/tests/unit/test_tables.py +7 -7
- cornflow/tests/unit/test_token.py +12 -6
- {cornflow-1.1.5.dist-info → cornflow-1.2.0a1.dist-info}/METADATA +3 -2
- {cornflow-1.1.5.dist-info → cornflow-1.2.0a1.dist-info}/RECORD +25 -25
- {cornflow-1.1.5.dist-info → cornflow-1.2.0a1.dist-info}/WHEEL +1 -1
- {cornflow-1.1.5.dist-info → cornflow-1.2.0a1.dist-info}/entry_points.txt +0 -0
- {cornflow-1.1.5.dist-info → cornflow-1.2.0a1.dist-info}/top_level.txt +0 -0
@@ -27,12 +27,12 @@ import jwt
|
|
27
27
|
|
28
28
|
# Import from internal modules
|
29
29
|
from cornflow.app import create_app
|
30
|
-
from cornflow.models import UserRoleModel
|
30
|
+
from cornflow.models import UserRoleModel, UserModel
|
31
31
|
from cornflow.commands.access import access_init_command
|
32
32
|
from cornflow.commands.dag import register_deployed_dags_command_test
|
33
33
|
from cornflow.commands.permissions import register_dag_permissions_command
|
34
34
|
from cornflow.shared.authentication import Auth
|
35
|
-
from cornflow.shared.const import ADMIN_ROLE, PLANNER_ROLE, SERVICE_ROLE
|
35
|
+
from cornflow.shared.const import ADMIN_ROLE, PLANNER_ROLE, SERVICE_ROLE, INTERNAL_TOKEN_ISSUER
|
36
36
|
from cornflow.shared import db
|
37
37
|
from cornflow.tests.const import (
|
38
38
|
LOGIN_URL,
|
@@ -121,7 +121,8 @@ class CustomTestCase(TestCase):
|
|
121
121
|
headers={"Content-Type": "application/json"},
|
122
122
|
).json["token"]
|
123
123
|
|
124
|
-
|
124
|
+
data = Auth().decode_token(self.token)
|
125
|
+
self.user = UserModel.get_one_object(username=data["username"])
|
125
126
|
self.url = None
|
126
127
|
self.model = None
|
127
128
|
self.copied_items = set()
|
@@ -828,7 +829,7 @@ class CheckTokenTestCase:
|
|
828
829
|
follow_redirects=True,
|
829
830
|
headers={
|
830
831
|
"Content-Type": "application/json",
|
831
|
-
"Authorization": "Bearer
|
832
|
+
"Authorization": f"Bearer {self.token}",
|
832
833
|
},
|
833
834
|
)
|
834
835
|
else:
|
@@ -982,34 +983,45 @@ class LoginTestCases:
|
|
982
983
|
"""
|
983
984
|
Tests using an expired token.
|
984
985
|
"""
|
985
|
-
|
986
|
-
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTA1MzYwNjUsImlhdCI6MTYxMDQ0OTY2NSwic3ViIjoxfQ"
|
987
|
-
".QEfmO-hh55PjtecnJ1RJT3aW2brGLadkg5ClH9yrRnc "
|
988
|
-
)
|
989
|
-
|
986
|
+
# First log in to get a valid user
|
990
987
|
payload = self.data
|
991
|
-
|
992
988
|
response = self.client.post(
|
993
989
|
LOGIN_URL,
|
994
990
|
data=json.dumps(payload),
|
995
991
|
follow_redirects=True,
|
996
992
|
headers={"Content-Type": "application/json"},
|
997
993
|
)
|
998
|
-
|
999
994
|
self.idx = response.json["id"]
|
1000
995
|
|
996
|
+
# Generate an expired token
|
997
|
+
expired_payload = {
|
998
|
+
# Token expired 1 hour ago
|
999
|
+
"exp": datetime.utcnow() - timedelta(hours=1),
|
1000
|
+
# Token created 2 hours ago
|
1001
|
+
"iat": datetime.utcnow() - timedelta(hours=2),
|
1002
|
+
"sub": self.idx,
|
1003
|
+
"iss": INTERNAL_TOKEN_ISSUER,
|
1004
|
+
}
|
1005
|
+
expired_token = jwt.encode(
|
1006
|
+
expired_payload,
|
1007
|
+
current_app.config["SECRET_TOKEN_KEY"],
|
1008
|
+
algorithm="HS256"
|
1009
|
+
)
|
1010
|
+
|
1011
|
+
# Try to use the expired token
|
1001
1012
|
response = self.client.get(
|
1002
1013
|
USER_URL + str(self.idx) + "/",
|
1003
1014
|
follow_redirects=True,
|
1004
1015
|
headers={
|
1005
1016
|
"Content-Type": "application/json",
|
1006
|
-
"Authorization": "Bearer "
|
1017
|
+
"Authorization": f"Bearer {expired_token}",
|
1007
1018
|
},
|
1008
1019
|
)
|
1009
1020
|
|
1010
1021
|
self.assertEqual(400, response.status_code)
|
1011
1022
|
self.assertEqual(
|
1012
|
-
"The token has expired, please login again",
|
1023
|
+
"The token has expired, please login again",
|
1024
|
+
response.json["error"]
|
1013
1025
|
)
|
1014
1026
|
|
1015
1027
|
def test_bad_format_token(self):
|
@@ -1040,13 +1052,8 @@ class LoginTestCases:
|
|
1040
1052
|
"""
|
1041
1053
|
Tests using an invalid token.
|
1042
1054
|
"""
|
1043
|
-
|
1044
|
-
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTA1Mzk5NTMsImlhdCI6MTYxMDQ1MzU1Mywic3ViIjoxfQ"
|
1045
|
-
".g3Gh7k7twXZ4K2MnQpgpSr76Sl9VX6TkDWusX5YzImo"
|
1046
|
-
)
|
1047
|
-
|
1055
|
+
# First log in to get a valid user
|
1048
1056
|
payload = self.data
|
1049
|
-
|
1050
1057
|
response = self.client.post(
|
1051
1058
|
LOGIN_URL,
|
1052
1059
|
data=json.dumps(payload),
|
@@ -1055,18 +1062,32 @@ class LoginTestCases:
|
|
1055
1062
|
)
|
1056
1063
|
self.idx = response.json["id"]
|
1057
1064
|
|
1065
|
+
# Generate an invalid token with wrong issuer
|
1066
|
+
invalid_payload = {
|
1067
|
+
"exp": datetime.utcnow() + timedelta(hours=1),
|
1068
|
+
"iat": datetime.utcnow(),
|
1069
|
+
"sub": self.idx,
|
1070
|
+
"iss": "invalid_issuer",
|
1071
|
+
}
|
1072
|
+
invalid_token = jwt.encode(
|
1073
|
+
invalid_payload,
|
1074
|
+
current_app.config["SECRET_TOKEN_KEY"],
|
1075
|
+
algorithm="HS256"
|
1076
|
+
)
|
1077
|
+
|
1078
|
+
# Try to use the invalid token
|
1058
1079
|
response = self.client.get(
|
1059
1080
|
USER_URL + str(self.idx) + "/",
|
1060
1081
|
follow_redirects=True,
|
1061
1082
|
headers={
|
1062
1083
|
"Content-Type": "application/json",
|
1063
|
-
"Authorization": "Bearer "
|
1084
|
+
"Authorization": f"Bearer {invalid_token}",
|
1064
1085
|
},
|
1065
1086
|
)
|
1066
1087
|
|
1067
1088
|
self.assertEqual(400, response.status_code)
|
1068
1089
|
self.assertEqual(
|
1069
|
-
"Invalid token
|
1090
|
+
"Invalid token issuer. Token must be issued by a valid provider",
|
1070
1091
|
response.json["error"],
|
1071
1092
|
)
|
1072
1093
|
|
@@ -67,7 +67,7 @@ class TestActionsListEndpoint(CustomTestCase):
|
|
67
67
|
follow_redirects=True,
|
68
68
|
headers={
|
69
69
|
"Content-Type": "application/json",
|
70
|
-
"Authorization": "Bearer
|
70
|
+
"Authorization": f"Bearer {self.token}",
|
71
71
|
},
|
72
72
|
)
|
73
73
|
self.assertEqual(200, response.status_code)
|
@@ -89,7 +89,7 @@ class TestActionsListEndpoint(CustomTestCase):
|
|
89
89
|
follow_redirects=True,
|
90
90
|
headers={
|
91
91
|
"Content-Type": "application/json",
|
92
|
-
"Authorization": "Bearer
|
92
|
+
"Authorization": f"Bearer {self.token}",
|
93
93
|
},
|
94
94
|
)
|
95
95
|
|
@@ -71,7 +71,7 @@ class TestApiViewListEndpoint(CustomTestCase):
|
|
71
71
|
follow_redirects=True,
|
72
72
|
headers={
|
73
73
|
"Content-Type": "application/json",
|
74
|
-
"Authorization": "Bearer
|
74
|
+
"Authorization": f"Bearer {self.token}",
|
75
75
|
},
|
76
76
|
)
|
77
77
|
self.assertEqual(200, response.status_code)
|
@@ -97,7 +97,7 @@ class TestApiViewListEndpoint(CustomTestCase):
|
|
97
97
|
follow_redirects=True,
|
98
98
|
headers={
|
99
99
|
"Content-Type": "application/json",
|
100
|
-
"Authorization": "Bearer
|
100
|
+
"Authorization": f"Bearer {self.token}",
|
101
101
|
},
|
102
102
|
)
|
103
103
|
|
@@ -90,11 +90,10 @@ class TestCasesModels(CustomTestCase):
|
|
90
90
|
self.payload = load_file(INSTANCE_PATH)
|
91
91
|
self.payloads = [load_file(f) for f in INSTANCES_LIST]
|
92
92
|
parents = [None, 1, 1, 3, 3, 3, 1, 7, 7, 9, 7]
|
93
|
-
|
94
|
-
data = {**self.payload, **dict(user_id=user.id)}
|
93
|
+
data = {**self.payload, **dict(user_id=self.user.id)}
|
95
94
|
for parent in parents:
|
96
95
|
if parent is not None:
|
97
|
-
parent = CaseModel.get_one_object(user=user, idx=parent)
|
96
|
+
parent = CaseModel.get_one_object(user=self.user, idx=parent)
|
98
97
|
node = CaseModel(data=data, parent=parent)
|
99
98
|
node.save()
|
100
99
|
|
@@ -106,10 +105,9 @@ class TestCasesModels(CustomTestCase):
|
|
106
105
|
- Correct path generation for cases
|
107
106
|
- Proper parent-child relationships
|
108
107
|
"""
|
109
|
-
|
110
|
-
case = CaseModel.get_one_object(user=user, idx=6)
|
108
|
+
case = CaseModel.get_one_object(user=self.user, idx=6)
|
111
109
|
self.assertEqual(case.path, "1/3/")
|
112
|
-
case = CaseModel.get_one_object(user=user, idx=11)
|
110
|
+
case = CaseModel.get_one_object(user=self.user, idx=11)
|
113
111
|
self.assertEqual(case.path, "1/7/")
|
114
112
|
|
115
113
|
def test_move_case(self):
|
@@ -120,9 +118,8 @@ class TestCasesModels(CustomTestCase):
|
|
120
118
|
- Cases can be moved to new parents
|
121
119
|
- Path updates correctly after move
|
122
120
|
"""
|
123
|
-
|
124
|
-
|
125
|
-
case11 = CaseModel.get_one_object(user=user, idx=11)
|
121
|
+
case6 = CaseModel.get_one_object(user=self.user, idx=6)
|
122
|
+
case11 = CaseModel.get_one_object(user=self.user, idx=11)
|
126
123
|
case6.move_to(case11)
|
127
124
|
self.assertEqual(case6.path, "1/7/11/")
|
128
125
|
|
@@ -135,11 +132,10 @@ class TestCasesModels(CustomTestCase):
|
|
135
132
|
- Nested path updates
|
136
133
|
- Path integrity after moves
|
137
134
|
"""
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
case10 = CaseModel.get_one_object(user=user, idx=10)
|
135
|
+
case3 = CaseModel.get_one_object(user=self.user, idx=3)
|
136
|
+
case11 = CaseModel.get_one_object(user=self.user, idx=11)
|
137
|
+
case9 = CaseModel.get_one_object(user=self.user, idx=9)
|
138
|
+
case10 = CaseModel.get_one_object(user=self.user, idx=10)
|
143
139
|
case3.move_to(case11)
|
144
140
|
case9.move_to(case3)
|
145
141
|
self.assertEqual(case10.path, "1/7/11/3/9/")
|
@@ -152,10 +148,9 @@ class TestCasesModels(CustomTestCase):
|
|
152
148
|
- Case deletion removes the case
|
153
149
|
- Child cases are properly handled
|
154
150
|
"""
|
155
|
-
|
156
|
-
case7 = CaseModel.get_one_object(user=user, idx=7)
|
151
|
+
case7 = CaseModel.get_one_object(user=self.user, idx=7)
|
157
152
|
case7.delete()
|
158
|
-
case11 = CaseModel.get_one_object(user=user, idx=11)
|
153
|
+
case11 = CaseModel.get_one_object(user=self.user, idx=11)
|
159
154
|
self.assertIsNone(case11)
|
160
155
|
|
161
156
|
def test_descendants(self):
|
@@ -166,8 +161,7 @@ class TestCasesModels(CustomTestCase):
|
|
166
161
|
- Correct counting of descendants
|
167
162
|
- Proper descendant relationships
|
168
163
|
"""
|
169
|
-
|
170
|
-
case7 = CaseModel.get_one_object(user=user, idx=7)
|
164
|
+
case7 = CaseModel.get_one_object(user=self.user, idx=7)
|
171
165
|
self.assertEqual(len(case7.descendants), 4)
|
172
166
|
|
173
167
|
def test_depth(self):
|
@@ -178,8 +172,7 @@ class TestCasesModels(CustomTestCase):
|
|
178
172
|
- Correct depth calculation in case tree
|
179
173
|
- Proper nesting level determination
|
180
174
|
"""
|
181
|
-
|
182
|
-
case10 = CaseModel.get_one_object(user=user, idx=10)
|
175
|
+
case10 = CaseModel.get_one_object(user=self.user, idx=10)
|
183
176
|
self.assertEqual(case10.depth, 4)
|
184
177
|
|
185
178
|
|
@@ -234,12 +227,11 @@ class TestCasesFromInstanceExecutionEndpoint(CustomTestCase):
|
|
234
227
|
"execution_id": execution_id,
|
235
228
|
"schema": "solve_model_dag",
|
236
229
|
}
|
237
|
-
self.user_object = UserModel.get_one_user(self.user)
|
238
230
|
self.instance = InstanceModel.get_one_object(
|
239
|
-
user=self.
|
231
|
+
user=self.user, idx=instance_id
|
240
232
|
)
|
241
233
|
self.execution = ExecutionModel.get_one_object(
|
242
|
-
user=self.
|
234
|
+
user=self.user, idx=execution_id
|
243
235
|
)
|
244
236
|
|
245
237
|
def test_new_case_execution(self):
|
@@ -266,7 +258,7 @@ class TestCasesFromInstanceExecutionEndpoint(CustomTestCase):
|
|
266
258
|
self.payload["schema"] = self.instance.schema
|
267
259
|
self.payload["solution"] = self.execution.data
|
268
260
|
self.payload["solution_hash"] = self.execution.data_hash
|
269
|
-
self.payload["user_id"] = self.user
|
261
|
+
self.payload["user_id"] = self.user.id
|
270
262
|
self.payload["indicators"] = ""
|
271
263
|
|
272
264
|
for key in self.response_items:
|
@@ -293,7 +285,7 @@ class TestCasesFromInstanceExecutionEndpoint(CustomTestCase):
|
|
293
285
|
self.payload["data"] = self.instance.data
|
294
286
|
self.payload["data_hash"] = self.instance.data_hash
|
295
287
|
self.payload["schema"] = self.instance.schema
|
296
|
-
self.payload["user_id"] = self.user
|
288
|
+
self.payload["user_id"] = self.user.id
|
297
289
|
self.payload["solution"] = None
|
298
290
|
self.payload["solution_hash"] = hash_json_256(None)
|
299
291
|
self.payload["indicators"] = ""
|
@@ -488,10 +480,9 @@ class TestCaseCopyEndpoint(CustomTestCase):
|
|
488
480
|
new_case = self.create_new_row(
|
489
481
|
self.url + str(self.case_id) + "/copy/", self.model, {}, check_payload=False
|
490
482
|
)
|
491
|
-
user = UserModel.get_one_user(self.user)
|
492
483
|
|
493
|
-
original_case = CaseModel.get_one_object(user=user, idx=self.case_id)
|
494
|
-
new_case = CaseModel.get_one_object(user=user, idx=new_case["id"])
|
484
|
+
original_case = CaseModel.get_one_object(user=self.user, idx=self.case_id)
|
485
|
+
new_case = CaseModel.get_one_object(user=self.user, idx=new_case["id"])
|
495
486
|
|
496
487
|
for key in self.copied_items:
|
497
488
|
self.assertEqual(getattr(original_case, key), getattr(new_case, key))
|
cornflow/tests/unit/test_dags.py
CHANGED
@@ -372,7 +372,7 @@ class TestDeployedDAG(TestCase):
|
|
372
372
|
follow_redirects=True,
|
373
373
|
headers={
|
374
374
|
"Content-Type": "application/json",
|
375
|
-
"Authorization": "Bearer
|
375
|
+
"Authorization": f"Bearer {self.token}",
|
376
376
|
},
|
377
377
|
)
|
378
378
|
|
@@ -403,7 +403,7 @@ class TestDeployedDAG(TestCase):
|
|
403
403
|
follow_redirects=True,
|
404
404
|
headers={
|
405
405
|
"Content-Type": "application/json",
|
406
|
-
"Authorization": "Bearer
|
406
|
+
"Authorization": f"Bearer {self.token}",
|
407
407
|
},
|
408
408
|
)
|
409
409
|
|
@@ -428,7 +428,7 @@ class TestDeployedDAG(TestCase):
|
|
428
428
|
follow_redirects=True,
|
429
429
|
headers={
|
430
430
|
"Content-Type": "application/json",
|
431
|
-
"Authorization": "Bearer
|
431
|
+
"Authorization": f"Bearer {self.token}",
|
432
432
|
},
|
433
433
|
)
|
434
434
|
|
@@ -440,7 +440,7 @@ class TestDeployedDAG(TestCase):
|
|
440
440
|
follow_redirects=True,
|
441
441
|
headers={
|
442
442
|
"Content-Type": "application/json",
|
443
|
-
"Authorization": "Bearer
|
443
|
+
"Authorization": f"Bearer {self.token}",
|
444
444
|
},
|
445
445
|
)
|
446
446
|
|
@@ -455,7 +455,7 @@ class TestDeployedDAG(TestCase):
|
|
455
455
|
follow_redirects=True,
|
456
456
|
headers={
|
457
457
|
"Content-Type": "application/json",
|
458
|
-
"Authorization": "Bearer
|
458
|
+
"Authorization": f"Bearer {self.token}",
|
459
459
|
},
|
460
460
|
)
|
461
461
|
self.assertEqual(response.status_code, 400)
|
@@ -67,7 +67,7 @@ class TestInstancesListEndpoint(BaseTestCases.ListFilters):
|
|
67
67
|
follow_redirects=True,
|
68
68
|
headers={
|
69
69
|
"Content-Type": "application/json",
|
70
|
-
"Authorization": "Bearer
|
70
|
+
"Authorization": f"Bearer {self.token}",
|
71
71
|
},
|
72
72
|
)
|
73
73
|
self.assertEqual(400, response.status_code)
|
@@ -203,7 +203,7 @@ class TestInstancesDataEndpoint(TestInstancesDetailEndpointBase):
|
|
203
203
|
idx = self.create_new_row(self.url, self.model, self.payload)
|
204
204
|
headers = {
|
205
205
|
"Content-Type": "application/json",
|
206
|
-
"Authorization": "Bearer
|
206
|
+
"Authorization": f"Bearer {self.token}",
|
207
207
|
"Accept-Encoding": "gzip",
|
208
208
|
}
|
209
209
|
response = self.client.get(INSTANCE_URL + idx + "/data/", headers=headers)
|