vantage6-algorithm-store 5.0.3rc6__tar.gz → 5.1.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.
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/PKG-INFO +4 -8
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/pyproject.toml +4 -11
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/base/unittest_base.py +13 -5
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_algorithm.py +62 -0
- vantage6_algorithm_store-5.1.0/tests_store/test_db_session_release.py +99 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/__init__.py +24 -13
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/base.py +37 -3
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/__init__.py +36 -19
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/algorithm.py +6 -6
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/.gitignore +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/Makefile +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/README.md +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/dev_store.sh +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/server.sh +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/base/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_policy.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_review.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_role.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_rule.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_user.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_version.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test___init__.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test_argument_schema.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test_link_algorithms.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/_data/unittest_config.yaml +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/_data/unittest_fixtures.yaml +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/algorithm_create.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/db.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/default_roles.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/globals.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/init_db.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/link_algorithms.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/algorithm.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/allowed_argument_value.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/argument.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/common/enums.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/common/ui_visualization_schemas.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/database.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/function.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/permission.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/policy.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/review.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/role.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/role_rule_association.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/rule.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/ui_visualization.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/user.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/permission.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/policy.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/review.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/role.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/rule.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/schema/input_schema.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/schema/output_schema.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/user.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/version.py +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/static/robots.txt +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.html +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.txt +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_algorithm.html +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_algorithm.txt +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_review.html +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_review.txt +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/review_no_longer_required.html +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/review_no_longer_required.txt +0 -0
- {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/wsgi.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vantage6-algorithm-store
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: Vantage6 algorithm store
|
|
5
5
|
Author: Vantage6 Team
|
|
6
6
|
Maintainer-email: Frank Martin <f.martin@iknl.nl>, Bart van Beusekom <b.vanbeusekom@iknl.nl>
|
|
7
|
-
License:
|
|
7
|
+
License: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: flask-jwt-extended==4.7.1
|
|
10
10
|
Requires-Dist: flask-principal==0.4.0
|
|
@@ -18,12 +18,8 @@ Requires-Dist: python-keycloak==5.5.0
|
|
|
18
18
|
Requires-Dist: requests==2.33.1
|
|
19
19
|
Requires-Dist: schema==0.7.5
|
|
20
20
|
Requires-Dist: sqlalchemy==2.0.37
|
|
21
|
-
Requires-Dist: vantage6-common==5.0
|
|
22
|
-
Requires-Dist: vantage6==5.0
|
|
23
|
-
Provides-Extra: dev
|
|
24
|
-
Requires-Dist: black; extra == 'dev'
|
|
25
|
-
Requires-Dist: coverage==7.10.2; extra == 'dev'
|
|
26
|
-
Requires-Dist: pre-commit; extra == 'dev'
|
|
21
|
+
Requires-Dist: vantage6-common==5.1.0
|
|
22
|
+
Requires-Dist: vantage6==5.1.0
|
|
27
23
|
Description-Content-Type: text/markdown
|
|
28
24
|
|
|
29
25
|
<h1 align="center">
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vantage6-algorithm-store"
|
|
7
|
-
version = "5.0
|
|
7
|
+
version = "5.1.0"
|
|
8
8
|
description = "Vantage6 algorithm store"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license = {text = "
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Vantage6 Team"}
|
|
13
13
|
]
|
|
@@ -30,15 +30,8 @@ dependencies = [
|
|
|
30
30
|
"requests==2.33.1",
|
|
31
31
|
"schema==0.7.5",
|
|
32
32
|
"sqlalchemy==2.0.37",
|
|
33
|
-
"vantage6-common==5.0
|
|
34
|
-
"vantage6==5.0
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
[project.optional-dependencies]
|
|
38
|
-
dev = [
|
|
39
|
-
"coverage==7.10.2",
|
|
40
|
-
"black",
|
|
41
|
-
"pre-commit",
|
|
33
|
+
"vantage6-common==5.1.0",
|
|
34
|
+
"vantage6==5.1.0",
|
|
42
35
|
]
|
|
43
36
|
|
|
44
37
|
[tool.hatch.build.targets.wheel]
|
|
@@ -9,7 +9,7 @@ from cryptography.hazmat.backends import default_backend
|
|
|
9
9
|
from cryptography.hazmat.primitives import serialization
|
|
10
10
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
11
11
|
|
|
12
|
-
from vantage6.common.globals import InstanceType
|
|
12
|
+
from vantage6.common.globals import STORE_TOKEN_AUDIENCE, InstanceType
|
|
13
13
|
|
|
14
14
|
from vantage6.backend.common import test_context
|
|
15
15
|
|
|
@@ -53,8 +53,6 @@ class TestResources(unittest.TestCase):
|
|
|
53
53
|
# set required environment variables *before* creating the app
|
|
54
54
|
os.environ["KEYCLOAK_URL"] = "dummy-keycloak-url"
|
|
55
55
|
os.environ["KEYCLOAK_REALM"] = "dummy-keycloak-realm"
|
|
56
|
-
os.environ["KEYCLOAK_ADMIN_USERNAME"] = "dummy-keycloak-admin-username"
|
|
57
|
-
os.environ["KEYCLOAK_ADMIN_PASSWORD"] = "dummy-keycloak-admin-password"
|
|
58
56
|
os.environ["KEYCLOAK_ADMIN_CLIENT"] = "dummy-keycloak-admin-client"
|
|
59
57
|
os.environ["KEYCLOAK_ADMIN_CLIENT_SECRET"] = (
|
|
60
58
|
"dummy-keycloak-admin-client-secret"
|
|
@@ -150,12 +148,21 @@ class TestResources(unittest.TestCase):
|
|
|
150
148
|
)
|
|
151
149
|
new_role.save()
|
|
152
150
|
|
|
153
|
-
def login_node(
|
|
151
|
+
def login_node(
|
|
152
|
+
self, client_type: str = "node", audience: str = STORE_TOKEN_AUDIENCE
|
|
153
|
+
) -> dict:
|
|
154
154
|
"""
|
|
155
155
|
Build headers with a real node JWT (mirrors vantage6-hq's `login_node` test
|
|
156
156
|
helper), for testing endpoints that recognize nodes via the
|
|
157
157
|
`vantage6_client_type` claim rather than a registered store `User`.
|
|
158
158
|
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
client_type : str
|
|
162
|
+
Value of the `vantage6_client_type` claim, e.g. "node" or "hq"
|
|
163
|
+
audience : str
|
|
164
|
+
Audience of the token
|
|
165
|
+
|
|
159
166
|
Returns
|
|
160
167
|
-------
|
|
161
168
|
dict
|
|
@@ -163,7 +170,8 @@ class TestResources(unittest.TestCase):
|
|
|
163
170
|
"""
|
|
164
171
|
mock_claims = {
|
|
165
172
|
"sub": str(uuid1()),
|
|
166
|
-
"vantage6_client_type":
|
|
173
|
+
"vantage6_client_type": client_type,
|
|
174
|
+
"aud": audience,
|
|
167
175
|
"exp": 9999999999,
|
|
168
176
|
"iat": 0,
|
|
169
177
|
}
|
|
@@ -174,6 +174,68 @@ class TestAlgorithmResources(TestResources):
|
|
|
174
174
|
# cleanup
|
|
175
175
|
policy.delete()
|
|
176
176
|
|
|
177
|
+
def test_view_algorithm_decorator_hq(self):
|
|
178
|
+
"""
|
|
179
|
+
Test that HQ's own token may list approved algorithms, like a node, but no
|
|
180
|
+
non-approved algorithms and nothing that requires a store user.
|
|
181
|
+
"""
|
|
182
|
+
policy = Policy(
|
|
183
|
+
key=StorePolicies.ALGORITHM_VIEW.value,
|
|
184
|
+
value=AlgorithmViewPolicies.ONLY_WITH_EXPLICIT_PERMISSION.value,
|
|
185
|
+
)
|
|
186
|
+
policy.save()
|
|
187
|
+
algorithm = Algorithm(
|
|
188
|
+
name="test_algorithm", status=AlgorithmStatus.APPROVED.value
|
|
189
|
+
)
|
|
190
|
+
algorithm.save()
|
|
191
|
+
hq_headers = self.login_node(client_type="hq")
|
|
192
|
+
|
|
193
|
+
rv = self.app.get("/api/algorithm", headers=hq_headers)
|
|
194
|
+
self.assertEqual(rv.status_code, HTTPStatus.OK)
|
|
195
|
+
|
|
196
|
+
rv = self.app.get("/api/algorithm?under_review=1", headers=hq_headers)
|
|
197
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
198
|
+
rv = self.app.get(f"/api/algorithm/{algorithm.id}", headers=hq_headers)
|
|
199
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
200
|
+
rv = self.app.get("/api/user", headers=hq_headers)
|
|
201
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
202
|
+
|
|
203
|
+
# cleanup
|
|
204
|
+
algorithm.delete()
|
|
205
|
+
policy.delete()
|
|
206
|
+
|
|
207
|
+
def test_tokens_for_hq_refused(self):
|
|
208
|
+
"""
|
|
209
|
+
Test that the store refuses tokens issued for HQ, so clients cannot send
|
|
210
|
+
their HQ token to a store.
|
|
211
|
+
"""
|
|
212
|
+
policy = Policy(
|
|
213
|
+
key=StorePolicies.ALGORITHM_VIEW.value,
|
|
214
|
+
value=AlgorithmViewPolicies.ONLY_WITH_EXPLICIT_PERMISSION.value,
|
|
215
|
+
)
|
|
216
|
+
policy.save()
|
|
217
|
+
|
|
218
|
+
for client_type in ["node", "hq"]:
|
|
219
|
+
headers = self.login_node(client_type=client_type, audience="vantage6-hq")
|
|
220
|
+
rv = self.app.get("/api/algorithm", headers=headers)
|
|
221
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
222
|
+
|
|
223
|
+
# cleanup
|
|
224
|
+
policy.delete()
|
|
225
|
+
|
|
226
|
+
def test_tokens_for_hq_and_stores_refused(self):
|
|
227
|
+
"""
|
|
228
|
+
Test that the store refuses tokens that are also valid at HQ, such as the
|
|
229
|
+
token of a login that asks for both audiences.
|
|
230
|
+
"""
|
|
231
|
+
headers = self.login_node(audience="vantage6-store")
|
|
232
|
+
rv = self.app.get("/api/algorithm", headers=headers)
|
|
233
|
+
self.assertEqual(rv.status_code, HTTPStatus.OK)
|
|
234
|
+
|
|
235
|
+
headers = self.login_node(audience=["vantage6-store", "vantage6-hq"])
|
|
236
|
+
rv = self.app.get("/api/algorithm", headers=headers)
|
|
237
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
238
|
+
|
|
177
239
|
def test_view_algorithm_single_not_affected_by_node_bypass(self):
|
|
178
240
|
"""
|
|
179
241
|
Test that the node bypass on the algorithm list endpoint does not extend to
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests that the outside-Flask database session is released.
|
|
3
|
+
|
|
4
|
+
Covers two things:
|
|
5
|
+
|
|
6
|
+
- ``DatabaseSessionManager.session_scope`` clears the session both when its body
|
|
7
|
+
exits normally and when the body raises.
|
|
8
|
+
- Constructing an ``AlgorithmStoreApp`` leaves no session behind. Startup loads
|
|
9
|
+
the permission rules and syncs the policies outside a Flask request, so the
|
|
10
|
+
request hooks that normally clear the session never fire; a session left here
|
|
11
|
+
keeps its pooled connection in an open transaction for the lifetime of the
|
|
12
|
+
process.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from unittest import TestCase
|
|
17
|
+
from unittest.mock import patch
|
|
18
|
+
|
|
19
|
+
from vantage6.common.globals import InstanceType
|
|
20
|
+
|
|
21
|
+
from vantage6.backend.common import session as session_module, test_context
|
|
22
|
+
|
|
23
|
+
from tests_store.base.unittest_base import MOCK_PUBLIC_KEY_PEM
|
|
24
|
+
from vantage6.algorithm.store import AlgorithmStoreApp
|
|
25
|
+
from vantage6.algorithm.store.globals import PACKAGE_FOLDER
|
|
26
|
+
from vantage6.algorithm.store.model.base import Database, DatabaseSessionManager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestSessionScope(TestCase):
|
|
30
|
+
@classmethod
|
|
31
|
+
def setUpClass(cls):
|
|
32
|
+
Database().connect("sqlite://", allow_drop_all=True)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def tearDownClass(cls):
|
|
36
|
+
Database().clear_data()
|
|
37
|
+
Database().close()
|
|
38
|
+
|
|
39
|
+
def tearDown(self):
|
|
40
|
+
# make sure a failing test does not leak a session into the next one
|
|
41
|
+
DatabaseSessionManager.clear_session()
|
|
42
|
+
|
|
43
|
+
def test_session_released_on_normal_exit(self):
|
|
44
|
+
"""The outside-Flask session is cleared when the scope exits normally."""
|
|
45
|
+
with DatabaseSessionManager.session_scope() as scoped_session:
|
|
46
|
+
self.assertIsNotNone(scoped_session)
|
|
47
|
+
self.assertIsNotNone(session_module.session)
|
|
48
|
+
self.assertIsNone(session_module.session)
|
|
49
|
+
|
|
50
|
+
def test_session_released_when_body_raises(self):
|
|
51
|
+
"""The session is cleared even when the body raises - the actual bug."""
|
|
52
|
+
with self.assertRaises(RuntimeError), DatabaseSessionManager.session_scope():
|
|
53
|
+
self.assertIsNotNone(session_module.session)
|
|
54
|
+
raise RuntimeError("work blew up mid-scope")
|
|
55
|
+
self.assertIsNone(session_module.session)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestStartupReleasesSession(TestCase):
|
|
59
|
+
"""
|
|
60
|
+
Building the app must not leave a session (and connection) checked out.
|
|
61
|
+
|
|
62
|
+
Startup loads the permission rules and syncs the policies against the
|
|
63
|
+
database, all outside a Flask request.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def setUpClass(cls):
|
|
68
|
+
Database().connect("sqlite://", allow_drop_all=True)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def tearDownClass(cls):
|
|
72
|
+
Database().clear_data()
|
|
73
|
+
Database().close()
|
|
74
|
+
|
|
75
|
+
def test_no_session_left_after_app_construction(self):
|
|
76
|
+
ctx = test_context.TestContext.from_external_config_file(
|
|
77
|
+
PACKAGE_FOLDER, InstanceType.ALGORITHM_STORE
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# set required environment variables *before* creating the app
|
|
81
|
+
os.environ["KEYCLOAK_URL"] = "dummy-keycloak-url"
|
|
82
|
+
os.environ["KEYCLOAK_REALM"] = "dummy-keycloak-realm"
|
|
83
|
+
os.environ["KEYCLOAK_ADMIN_USERNAME"] = "dummy-keycloak-admin-username"
|
|
84
|
+
os.environ["KEYCLOAK_ADMIN_PASSWORD"] = "dummy-keycloak-admin-password"
|
|
85
|
+
os.environ["KEYCLOAK_ADMIN_CLIENT"] = "dummy-keycloak-admin-client"
|
|
86
|
+
os.environ["KEYCLOAK_ADMIN_CLIENT_SECRET"] = (
|
|
87
|
+
"dummy-keycloak-admin-client-secret"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
DatabaseSessionManager.clear_session()
|
|
91
|
+
self.assertIsNone(session_module.session)
|
|
92
|
+
|
|
93
|
+
with patch(
|
|
94
|
+
"vantage6.algorithm.store.AlgorithmStoreApp._get_keycloak_public_key"
|
|
95
|
+
) as mock_get_key:
|
|
96
|
+
mock_get_key.return_value = MOCK_PUBLIC_KEY_PEM.decode()
|
|
97
|
+
AlgorithmStoreApp(ctx)
|
|
98
|
+
|
|
99
|
+
self.assertIsNone(session_module.session)
|
|
@@ -27,7 +27,7 @@ from flask_principal import Identity, identity_changed
|
|
|
27
27
|
from vantage6.common import logger_name
|
|
28
28
|
from vantage6.common.enum import StorePolicies
|
|
29
29
|
from vantage6.common.exceptions import AuthenticationException
|
|
30
|
-
from vantage6.common.globals import DEFAULT_API_PATH
|
|
30
|
+
from vantage6.common.globals import DEFAULT_API_PATH, STORE_TOKEN_AUDIENCE
|
|
31
31
|
|
|
32
32
|
from vantage6.cli.context.algorithm_store import AlgorithmStoreContext
|
|
33
33
|
|
|
@@ -75,17 +75,18 @@ class AlgorithmStoreApp(Vantage6App):
|
|
|
75
75
|
static_folder=Path(__file__).parent / "static",
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
with DatabaseSessionManager.session_scope():
|
|
79
|
+
# setup the permission manager for the API endpoints
|
|
80
|
+
self.permissions = PermissionManager(RESOURCES_PATH, RESOURCES, DefaultRole)
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
# Load API resources
|
|
83
|
+
self.load_resources()
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
# sync policies with the database
|
|
86
|
+
self.setup_policies(self.ctx.config)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
if self.ctx.config.get("dev", {}).get("disable_review", False):
|
|
89
|
+
self.setup_disable_review()
|
|
89
90
|
|
|
90
91
|
log.info("Initialization done")
|
|
91
92
|
|
|
@@ -93,6 +94,10 @@ class AlgorithmStoreApp(Vantage6App):
|
|
|
93
94
|
"""Configure the Flask settings of the vantage6 algorithm store."""
|
|
94
95
|
self._configure_flask_base(DatabaseSessionManager)
|
|
95
96
|
|
|
97
|
+
# only accept Keycloak tokens issued for algorithm stores. Tokens for HQ are
|
|
98
|
+
# refused, so clients never have a reason to send them here.
|
|
99
|
+
self.app.config["JWT_DECODE_AUDIENCE"] = STORE_TOKEN_AUDIENCE
|
|
100
|
+
|
|
96
101
|
def configure_api(self) -> None:
|
|
97
102
|
"""Define global API output and its structure."""
|
|
98
103
|
self._configure_api_base(BaseHATEOASModelSchema, Base)
|
|
@@ -115,17 +120,18 @@ class AlgorithmStoreApp(Vantage6App):
|
|
|
115
120
|
Returns
|
|
116
121
|
-------
|
|
117
122
|
db.User | str:
|
|
118
|
-
The user identity, or the keycloak id if
|
|
123
|
+
The user identity, or the keycloak id if a node or HQ is
|
|
119
124
|
authenticating.
|
|
120
125
|
|
|
121
126
|
Raises
|
|
122
127
|
------
|
|
123
128
|
AuthenticationException
|
|
124
|
-
If no store `User` is registered for this (non-node)
|
|
129
|
+
If no store `User` is registered for this (non-node, non-HQ)
|
|
130
|
+
identity.
|
|
125
131
|
"""
|
|
126
132
|
identity = jwt_headers["sub"]
|
|
127
133
|
|
|
128
|
-
if jwt_headers.get("vantage6_client_type")
|
|
134
|
+
if jwt_headers.get("vantage6_client_type") in ("node", "hq"):
|
|
129
135
|
return identity
|
|
130
136
|
|
|
131
137
|
auth_identity = Identity(identity)
|
|
@@ -328,5 +334,10 @@ def run_store(config: str, system_folders: bool = True) -> AlgorithmStoreApp:
|
|
|
328
334
|
ctx = AlgorithmStoreContext.from_external_config_file(
|
|
329
335
|
config, system_folders, in_container=True
|
|
330
336
|
)
|
|
331
|
-
Database().connect(
|
|
337
|
+
Database().connect(
|
|
338
|
+
uri=ctx.get_database_uri(),
|
|
339
|
+
allow_drop_all=False,
|
|
340
|
+
pool_size=ctx.config.get("database_pool_size"),
|
|
341
|
+
max_overflow=ctx.config.get("database_max_overflow"),
|
|
342
|
+
)
|
|
332
343
|
return AlgorithmStoreApp(ctx)
|
|
@@ -52,7 +52,13 @@ class Database(BaseDatabase, metaclass=Singleton):
|
|
|
52
52
|
"""
|
|
53
53
|
self._close(Base)
|
|
54
54
|
|
|
55
|
-
def connect(
|
|
55
|
+
def connect(
|
|
56
|
+
self,
|
|
57
|
+
uri="sqlite:////tmp/test.db",
|
|
58
|
+
allow_drop_all=False,
|
|
59
|
+
pool_size: int | None = None,
|
|
60
|
+
max_overflow: int | None = None,
|
|
61
|
+
):
|
|
56
62
|
"""
|
|
57
63
|
Connect to the database.
|
|
58
64
|
|
|
@@ -62,8 +68,20 @@ class Database(BaseDatabase, metaclass=Singleton):
|
|
|
62
68
|
URI of the database. Defaults to a sqlite database in /tmp.
|
|
63
69
|
allow_drop_all : bool, optional
|
|
64
70
|
If True, the database can be dropped. Defaults to False.
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
pool_size : int, optional
|
|
72
|
+
Number of persistent connections in the SQLAlchemy connection pool.
|
|
73
|
+
When None (default), SQLAlchemy's default is used. Ignored for SQLite.
|
|
74
|
+
max_overflow : int, optional
|
|
75
|
+
Number of extra connections allowed on top of ``pool_size``. When None
|
|
76
|
+
(default), SQLAlchemy's default is used. Ignored for SQLite.
|
|
77
|
+
"""
|
|
78
|
+
self._connect(
|
|
79
|
+
Base,
|
|
80
|
+
uri,
|
|
81
|
+
allow_drop_all,
|
|
82
|
+
pool_size=pool_size,
|
|
83
|
+
max_overflow=max_overflow,
|
|
84
|
+
)
|
|
67
85
|
|
|
68
86
|
|
|
69
87
|
class DatabaseSessionManager(BaseDatabaseSessionManager):
|
|
@@ -101,6 +119,22 @@ class DatabaseSessionManager(BaseDatabaseSessionManager):
|
|
|
101
119
|
"""
|
|
102
120
|
BaseDatabaseSessionManager._new_session(DatabaseSessionManager, Database)
|
|
103
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
def session_scope():
|
|
124
|
+
"""
|
|
125
|
+
Context manager that yields a session and always releases it.
|
|
126
|
+
|
|
127
|
+
Use this for database work that runs outside the Flask request
|
|
128
|
+
lifecycle (e.g. application startup), where the request hooks that
|
|
129
|
+
normally clear the session do not fire.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
contextmanager
|
|
134
|
+
A context manager yielding a database session
|
|
135
|
+
"""
|
|
136
|
+
return BaseDatabaseSessionManager._session_scope(DatabaseSessionManager)
|
|
137
|
+
|
|
104
138
|
|
|
105
139
|
class ModelBase(BaseModelBase):
|
|
106
140
|
"""
|
|
@@ -105,24 +105,25 @@ def _authorize_user(
|
|
|
105
105
|
return None, None
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def
|
|
108
|
+
def _get_request_client_type() -> str | None:
|
|
109
109
|
"""
|
|
110
|
-
|
|
110
|
+
Get the `vantage6_client_type` of the valid JWT of the current request, e.g.
|
|
111
|
+
"node" or "hq".
|
|
111
112
|
|
|
112
|
-
Nodes are never registered as a store `User` - unlike `_authenticate`, this
|
|
113
|
-
not do a database lookup. It only relies on the `vantage6_client_type` claim
|
|
114
|
-
by the shared vantage6 Keycloak realm.
|
|
113
|
+
Nodes and HQ are never registered as a store `User` - unlike `_authenticate`, this
|
|
114
|
+
does not do a database lookup. It only relies on the `vantage6_client_type` claim
|
|
115
|
+
set by the shared vantage6 Keycloak realm.
|
|
115
116
|
|
|
116
117
|
Note that this function does not raise if no token is present at all, so it is safe
|
|
117
118
|
to call unconditionally before any policy-based access checks.
|
|
118
119
|
|
|
119
120
|
Returns
|
|
120
121
|
-------
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
str | None
|
|
123
|
+
The client type, or None if the request has no token or no client type.
|
|
123
124
|
"""
|
|
124
125
|
verify_jwt_in_request(optional=True)
|
|
125
|
-
return get_jwt().get("vantage6_client_type")
|
|
126
|
+
return get_jwt().get("vantage6_client_type")
|
|
126
127
|
|
|
127
128
|
|
|
128
129
|
def with_authentication() -> Callable:
|
|
@@ -189,20 +190,27 @@ def with_permission(resource: str, operation: Operation) -> Callable:
|
|
|
189
190
|
return protection_decorator
|
|
190
191
|
|
|
191
192
|
|
|
192
|
-
def with_permission_to_view_algorithms(
|
|
193
|
+
def with_permission_to_view_algorithms(
|
|
194
|
+
allow_node: bool = False, allow_hq: bool = False
|
|
195
|
+
) -> Callable:
|
|
193
196
|
"""
|
|
194
197
|
Decorator to verify that the user has as a permission on a resource.
|
|
195
198
|
|
|
196
199
|
Parameters
|
|
197
200
|
----------
|
|
198
201
|
allow_node : bool, optional
|
|
199
|
-
If True, also allow requests authenticated as a node (see
|
|
200
|
-
to view approved algorithms, regardless of the
|
|
201
|
-
This lets nodes independently re-verify - for
|
|
202
|
-
`allowed_algorithm_stores` policy - that an image is a registered,
|
|
203
|
-
algorithm in this store, without requiring the node to be registered
|
|
204
|
-
store `User`.
|
|
202
|
+
If True, also allow requests authenticated as a node (see
|
|
203
|
+
`_get_request_client_type`) to view approved algorithms, regardless of the
|
|
204
|
+
store's `algorithm_view` policy. This lets nodes independently re-verify - for
|
|
205
|
+
their own `allowed_algorithm_stores` policy - that an image is a registered,
|
|
206
|
+
approved algorithm in this store, without requiring the node to be registered
|
|
207
|
+
as a store `User`. Access is still restricted to approved algorithms only.
|
|
205
208
|
Default is False.
|
|
209
|
+
allow_hq : bool, optional
|
|
210
|
+
If True, also allow requests authenticated as HQ to view approved algorithms,
|
|
211
|
+
regardless of the store's `algorithm_view` policy. This lets HQ look up the
|
|
212
|
+
algorithm of a new task. Access is still restricted to approved algorithms
|
|
213
|
+
only. Default is False.
|
|
206
214
|
|
|
207
215
|
Returns
|
|
208
216
|
-------
|
|
@@ -211,6 +219,12 @@ def with_permission_to_view_algorithms(allow_node: bool = False) -> Callable:
|
|
|
211
219
|
authentication.
|
|
212
220
|
"""
|
|
213
221
|
|
|
222
|
+
allowed_client_types = {
|
|
223
|
+
client_type
|
|
224
|
+
for client_type, allowed in (("node", allow_node), ("hq", allow_hq))
|
|
225
|
+
if allowed
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
def protection_decorator(fn):
|
|
215
229
|
@wraps(fn)
|
|
216
230
|
def decorator(self, *args, **kwargs):
|
|
@@ -225,11 +239,14 @@ def with_permission_to_view_algorithms(allow_node: bool = False) -> Callable:
|
|
|
225
239
|
or request_args.get("invalidated")
|
|
226
240
|
)
|
|
227
241
|
|
|
228
|
-
# nodes re-verifying an image
|
|
229
|
-
# allowed to see approved algorithms, independent of the store's
|
|
242
|
+
# nodes re-verifying an image and HQ looking up a task's algorithm are
|
|
243
|
+
# always allowed to see approved algorithms, independent of the store's
|
|
230
244
|
# algorithm_view policy - they are never allowed to see non-approved ones
|
|
231
|
-
if
|
|
232
|
-
|
|
245
|
+
if (
|
|
246
|
+
allowed_client_types
|
|
247
|
+
and request_approved
|
|
248
|
+
and _get_request_client_type() in allowed_client_types
|
|
249
|
+
):
|
|
233
250
|
return fn(self, *args, **kwargs)
|
|
234
251
|
|
|
235
252
|
policies = Policy.get_as_dict()
|
|
@@ -148,7 +148,7 @@ class AlgorithmBaseResource(AlgorithmStoreResources):
|
|
|
148
148
|
class Algorithms(AlgorithmBaseResource):
|
|
149
149
|
"""Resource for /algorithm"""
|
|
150
150
|
|
|
151
|
-
@with_permission_to_view_algorithms(allow_node=True)
|
|
151
|
+
@with_permission_to_view_algorithms(allow_node=True, allow_hq=True)
|
|
152
152
|
def get(self):
|
|
153
153
|
"""List algorithms
|
|
154
154
|
---
|
|
@@ -159,11 +159,11 @@ class Algorithms(AlgorithmBaseResource):
|
|
|
159
159
|
algorithms, set the 'awaiting_reviewer_assignment', 'under_review' or
|
|
160
160
|
'invalidated' parameter to True.
|
|
161
161
|
|
|
162
|
-
Nodes are always allowed to list approved algorithms (e.g. filtered
|
|
163
|
-
'image'), regardless of this store's algorithm_view policy - this lets a
|
|
164
|
-
independently verify that an image is a registered, approved algorithm
|
|
165
|
-
its own allowed_algorithm_stores policy
|
|
166
|
-
filters.
|
|
162
|
+
Nodes and HQ are always allowed to list approved algorithms (e.g. filtered
|
|
163
|
+
by 'image'), regardless of this store's algorithm_view policy - this lets a
|
|
164
|
+
node independently verify that an image is a registered, approved algorithm
|
|
165
|
+
for its own allowed_algorithm_stores policy, and lets HQ look up the
|
|
166
|
+
algorithm of a new task. They cannot use the non-approved filters.
|
|
167
167
|
|
|
168
168
|
parameters:
|
|
169
169
|
- in: query
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/__init__.py
RENAMED
|
File without changes
|
{vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test___init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|