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.
Files changed (69) hide show
  1. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/PKG-INFO +4 -8
  2. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/pyproject.toml +4 -11
  3. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/base/unittest_base.py +13 -5
  4. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_algorithm.py +62 -0
  5. vantage6_algorithm_store-5.1.0/tests_store/test_db_session_release.py +99 -0
  6. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/__init__.py +24 -13
  7. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/base.py +37 -3
  8. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/__init__.py +36 -19
  9. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/algorithm.py +6 -6
  10. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/.gitignore +0 -0
  11. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/Makefile +0 -0
  12. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/README.md +0 -0
  13. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/dev_store.sh +0 -0
  14. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/server.sh +0 -0
  15. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/__init__.py +0 -0
  16. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/base/__init__.py +0 -0
  17. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/__init__.py +0 -0
  18. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_policy.py +0 -0
  19. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_review.py +0 -0
  20. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_role.py +0 -0
  21. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_rule.py +0 -0
  22. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_user.py +0 -0
  23. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/resources/test_version.py +0 -0
  24. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test___init__.py +0 -0
  25. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test_argument_schema.py +0 -0
  26. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/tests_store/test_link_algorithms.py +0 -0
  27. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/_data/unittest_config.yaml +0 -0
  28. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/_data/unittest_fixtures.yaml +0 -0
  29. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/algorithm_create.py +0 -0
  30. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/db.py +0 -0
  31. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/default_roles.py +0 -0
  32. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/globals.py +0 -0
  33. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/init_db.py +0 -0
  34. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/link_algorithms.py +0 -0
  35. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/__init__.py +0 -0
  36. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/algorithm.py +0 -0
  37. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/allowed_argument_value.py +0 -0
  38. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/argument.py +0 -0
  39. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/common/enums.py +0 -0
  40. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/common/ui_visualization_schemas.py +0 -0
  41. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/database.py +0 -0
  42. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/function.py +0 -0
  43. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/permission.py +0 -0
  44. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/policy.py +0 -0
  45. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/review.py +0 -0
  46. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/role.py +0 -0
  47. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/role_rule_association.py +0 -0
  48. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/rule.py +0 -0
  49. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/ui_visualization.py +0 -0
  50. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/model/user.py +0 -0
  51. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/permission.py +0 -0
  52. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/policy.py +0 -0
  53. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/review.py +0 -0
  54. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/role.py +0 -0
  55. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/rule.py +0 -0
  56. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/schema/input_schema.py +0 -0
  57. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/schema/output_schema.py +0 -0
  58. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/user.py +0 -0
  59. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/resource/version.py +0 -0
  60. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/static/robots.txt +0 -0
  61. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.html +0 -0
  62. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.txt +0 -0
  63. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_algorithm.html +0 -0
  64. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_algorithm.txt +0 -0
  65. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_review.html +0 -0
  66. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/new_review.txt +0 -0
  67. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/review_no_longer_required.html +0 -0
  68. {vantage6_algorithm_store-5.0.3rc6 → vantage6_algorithm_store-5.1.0}/vantage6/algorithm/store/templates/mail/review_no_longer_required.txt +0 -0
  69. {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.3rc6
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: MIT
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.3rc6
22
- Requires-Dist: vantage6==5.0.3rc6
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.3rc6"
7
+ version = "5.1.0"
8
8
  description = "Vantage6 algorithm store"
9
9
  readme = "README.md"
10
- license = {text = "MIT"}
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.3rc6",
34
- "vantage6==5.0.3rc6",
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(self) -> dict:
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": "node",
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
- # setup the permission manager for the API endpoints
79
- self.permissions = PermissionManager(RESOURCES_PATH, RESOURCES, DefaultRole)
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
- # Load API resources
82
- self.load_resources()
82
+ # Load API resources
83
+ self.load_resources()
83
84
 
84
- # sync policies with the database
85
- self.setup_policies(self.ctx.config)
85
+ # sync policies with the database
86
+ self.setup_policies(self.ctx.config)
86
87
 
87
- if self.ctx.config.get("dev", {}).get("disable_review", False):
88
- self.setup_disable_review()
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 it is a node that is
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) identity.
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") == "node":
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(uri=ctx.get_database_uri(), allow_drop_all=False)
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(self, uri="sqlite:////tmp/test.db", allow_drop_all=False):
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
- self._connect(Base, uri, allow_drop_all)
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 _is_node_request() -> bool:
108
+ def _get_request_client_type() -> str | None:
109
109
  """
110
- Check whether the current request carries a valid node JWT.
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 does
113
- not do a database lookup. It only relies on the `vantage6_client_type` claim set
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
- bool
122
- Whether the request is authenticated as a node.
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") == "node"
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(allow_node: bool = False) -> Callable:
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 `_is_node_request`)
200
- to view approved algorithms, regardless of the store's `algorithm_view` policy.
201
- This lets nodes independently re-verify - for their own
202
- `allowed_algorithm_stores` policy - that an image is a registered, approved
203
- algorithm in this store, without requiring the node to be registered as a
204
- store `User`. Node access is still restricted to approved algorithms only.
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 against this store's catalog are always
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 allow_node and request_approved and _is_node_request():
232
- g.node = True
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 by
163
- 'image'), regardless of this store's algorithm_view policy - this lets a node
164
- independently verify that an image is a registered, approved algorithm for
165
- its own allowed_algorithm_stores policy. Nodes cannot use the non-approved
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