cornflow 1.2.3a2__py3-none-any.whl → 1.2.3a4__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.
@@ -1,9 +1,11 @@
1
-
2
1
  # Imports from external libraries
3
- from flask import current_app
2
+ import sys
4
3
  from importlib import import_module
4
+
5
+ from flask import current_app
5
6
  from sqlalchemy.exc import DBAPIError, IntegrityError
6
- import sys
7
+
8
+ from cornflow.endpoints import resources, alarms_resources
7
9
 
8
10
  # Imports from internal libraries
9
11
  from cornflow.models import ViewModel
@@ -11,45 +13,21 @@ from cornflow.shared import db
11
13
 
12
14
 
13
15
  def register_views_command(external_app: str = None, verbose: bool = False):
16
+ """
17
+ Register views for the application.
18
+ external_app: If provided, it will register the views for the external app.
19
+ verbose: If True, it will print the views that are being registered.
20
+ """
21
+ resources_to_register = get_resources_to_register(external_app)
22
+ views_registered_urls_all_attributes = get_database_view()
23
+ views_to_register, views_registered_urls_all_attributes = get_views_to_register(
24
+ resources_to_register, views_registered_urls_all_attributes
25
+ )
26
+ views_to_delete, views_to_update = get_views_to_update_and_delete(
27
+ resources_to_register, views_registered_urls_all_attributes
28
+ )
14
29
 
15
- if external_app is None:
16
- from cornflow.endpoints import resources, alarms_resources
17
- resources_to_register = resources
18
- if current_app.config["ALARMS_ENDPOINTS"]:
19
- resources_to_register = resources + alarms_resources
20
- elif external_app is not None:
21
- sys.path.append("./")
22
- external_module = import_module(external_app)
23
- resources_to_register = external_module.endpoints.resources
24
- else:
25
- resources_to_register = []
26
- exit()
27
-
28
- views_registered = [view.name for view in ViewModel.get_all_objects()]
29
-
30
- views_to_register = [
31
- ViewModel(
32
- {
33
- "name": view["endpoint"],
34
- "url_rule": view["urls"],
35
- "description": view["resource"].DESCRIPTION,
36
- }
37
- )
38
- for view in resources_to_register
39
- if view["endpoint"] not in views_registered
40
- ]
41
-
42
- if len(views_to_register) > 0:
43
- db.session.bulk_save_objects(views_to_register)
44
-
45
- try:
46
- db.session.commit()
47
- except IntegrityError as e:
48
- db.session.rollback()
49
- current_app.logger.error(f"Integrity error on views register: {e}")
50
- except DBAPIError as e:
51
- db.session.rollback()
52
- current_app.logger.error(f"Unknow error on views register: {e}")
30
+ load_changes_to_db(views_to_delete, views_to_register, views_to_update)
53
31
 
54
32
  if "postgres" in str(db.session.get_bind()):
55
33
  db.engine.execute(
@@ -68,3 +46,155 @@ def register_views_command(external_app: str = None, verbose: bool = False):
68
46
  current_app.logger.info("No new endpoints to be registered")
69
47
 
70
48
  return True
49
+
50
+
51
+ def load_changes_to_db(views_to_delete, views_to_register, views_to_update):
52
+ """
53
+ Load changes to the database.
54
+ views_to_delete: List of views to delete.
55
+ views_to_register: List of views to register.
56
+ views_to_update: List of views to update.
57
+ """
58
+ if len(views_to_register) > 0:
59
+ db.session.bulk_save_objects(views_to_register)
60
+ if len(views_to_update) > 0:
61
+ db.session.bulk_update_mappings(ViewModel, views_to_update)
62
+ # If the list views_to_delete is not empty, we will iterate over it and delete the views
63
+ # If it is empty, we will not delete any view since we are iterating over an empty list
64
+ for view_id in views_to_delete:
65
+ view_to_delete = ViewModel.get_one_object(idx=view_id)
66
+ if view_to_delete:
67
+ view_to_delete.delete()
68
+ try:
69
+ db.session.commit()
70
+ except IntegrityError as e:
71
+ db.session.rollback()
72
+ current_app.logger.error(f"Integrity error on views register: {e}")
73
+ except DBAPIError as e:
74
+ db.session.rollback()
75
+ current_app.logger.error(f"Unknow error on views register: {e}")
76
+
77
+
78
+ def get_views_to_delete(
79
+ all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
80
+ ):
81
+ """
82
+ Get the views to delete.
83
+ Views to delete: exist in registered views but not in resources to register.
84
+ """
85
+ return [
86
+ view_attrs["id"]
87
+ for view_name, view_attrs in views_registered_urls_all_attributes.items()
88
+ if view_name not in all_resources_to_register_views_endpoints
89
+ ]
90
+
91
+
92
+ def get_views_to_update(
93
+ all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
94
+ ):
95
+ """
96
+ Get the views to update.
97
+ Views to update: exist in both but with different url_rule or description.
98
+ """
99
+ return [
100
+ {
101
+ "id": view_attrs["id"],
102
+ "name": view_name,
103
+ "url_rule": all_resources_to_register_views_endpoints[view_name][
104
+ "url_rule"
105
+ ],
106
+ "description": all_resources_to_register_views_endpoints[view_name][
107
+ "description"
108
+ ],
109
+ }
110
+ for view_name, view_attrs in views_registered_urls_all_attributes.items()
111
+ if view_name in all_resources_to_register_views_endpoints
112
+ and (
113
+ view_attrs["url_rule"]
114
+ != all_resources_to_register_views_endpoints[view_name]["url_rule"]
115
+ or view_attrs["description"]
116
+ != all_resources_to_register_views_endpoints[view_name]["description"]
117
+ )
118
+ ]
119
+
120
+
121
+ def get_views_to_update_and_delete(
122
+ resources_to_register, views_registered_urls_all_attributes
123
+ ):
124
+ """
125
+ Get the views to update and delete.
126
+ all_resources_to_register_views_endpoints: Dictionary of all resources to register views endpoints.
127
+ views_registered_urls_all_attributes: Dictionary of views registered urls all attributes.
128
+ """
129
+ all_resources_to_register_views_endpoints = {
130
+ view["endpoint"]: {
131
+ "url_rule": view["urls"],
132
+ "description": view["resource"].DESCRIPTION,
133
+ }
134
+ for view in resources_to_register
135
+ }
136
+
137
+ views_to_delete = get_views_to_delete(
138
+ all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
139
+ )
140
+
141
+ views_to_update = get_views_to_update(
142
+ all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
143
+ )
144
+
145
+ return views_to_delete, views_to_update
146
+
147
+
148
+ def get_views_to_register(resources_to_register, views_registered_urls_all_attributes):
149
+ """
150
+ Get the views to register.
151
+ resources_to_register: List of resources to register.
152
+ """
153
+
154
+ views_to_register = [
155
+ ViewModel(
156
+ {
157
+ "name": view["endpoint"],
158
+ "url_rule": view["urls"],
159
+ "description": view["resource"].DESCRIPTION,
160
+ }
161
+ )
162
+ for view in resources_to_register
163
+ if view["endpoint"] not in views_registered_urls_all_attributes.keys()
164
+ ]
165
+
166
+ return views_to_register, views_registered_urls_all_attributes
167
+
168
+
169
+ def get_database_view():
170
+ """
171
+ Get the database views.
172
+ """
173
+ views_registered_urls_all_attributes = {
174
+ view.name: {
175
+ "url_rule": view.url_rule,
176
+ "description": view.description,
177
+ "id": view.id,
178
+ }
179
+ for view in ViewModel.get_all_objects()
180
+ }
181
+ return views_registered_urls_all_attributes
182
+
183
+
184
+ def get_resources_to_register(external_app):
185
+ if external_app is None:
186
+ resources_to_register = resources
187
+ if current_app.config["ALARMS_ENDPOINTS"]:
188
+ resources_to_register = resources + alarms_resources
189
+ current_app.logger.info(" ALARMS ENDPOINTS ENABLED ")
190
+ else:
191
+ current_app.logger.info(f" USING EXTERNAL APP: {external_app} ")
192
+ sys.path.append("./")
193
+ external_module = import_module(external_app)
194
+ if current_app.config["ALARMS_ENDPOINTS"]:
195
+ resources_to_register = (
196
+ external_module.endpoints.resources + resources + alarms_resources
197
+ )
198
+ else:
199
+ resources_to_register = external_module.endpoints.resources + resources
200
+ return resources_to_register
@@ -396,10 +396,10 @@ class Auth:
396
396
  "The permission for this endpoint is not in the database."
397
397
  )
398
398
  raise NoPermission(
399
- error="You do not have permission to access this endpoint",
399
+ error="The permission for this endpoint is not in the database.",
400
400
  status_code=403,
401
401
  log_txt=f"Error while user {user_id} tries to access endpoint. "
402
- f"The user does not permission to access. ",
402
+ f"The permission for this endpoint is not in the database.",
403
403
  )
404
404
 
405
405
  for role in user_roles:
@@ -414,7 +414,7 @@ class Auth:
414
414
  error="You do not have permission to access this endpoint",
415
415
  status_code=403,
416
416
  log_txt=f"Error while user {user_id} tries to access endpoint {view_id} with action {action_id}. "
417
- f"The user does not permission to access. ",
417
+ f"The user does not have permission to access. ",
418
418
  )
419
419
 
420
420
  @staticmethod
cornflow/shared/const.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  In this file we import the values for different constants on cornflow server
3
3
  """
4
- CORNFLOW_VERSION = "1.2.3a2"
4
+ CORNFLOW_VERSION = "1.2.3.a4"
5
5
  INTERNAL_TOKEN_ISSUER = "cornflow"
6
6
 
7
7
  # endpoints responses for health check
@@ -29,7 +29,6 @@ from cornflow.app import (
29
29
  register_dag_permissions,
30
30
  register_roles,
31
31
  register_views,
32
- register_base_assignations,
33
32
  )
34
33
  from cornflow.commands.dag import register_deployed_dags_command_test
35
34
  from cornflow.endpoints import resources, alarms_resources
@@ -495,149 +494,3 @@ class TestCommands(TestCase):
495
494
  },
496
495
  )
497
496
  self.assertEqual(403, response.status_code)
498
-
499
- def test_permissions_not_deleted_when_roles_removed_from_code(self):
500
- """
501
- Test that permissions are NOT deleted when roles are removed from ROLES_WITH_ACCESS.
502
-
503
- This test demonstrates the current bug: when a role is removed from
504
- ROLES_WITH_ACCESS in an endpoint's code, the corresponding permissions
505
- in the database are not automatically deleted. This happens because
506
- the deletion logic in register_base_permissions_command is commented out.
507
-
508
- The test should currently FAIL to demonstrate the bug exists.
509
- """
510
- # First, initialize the access system normally
511
- self.runner.invoke(access_init)
512
-
513
- # Get the original ROLES_WITH_ACCESS for ExampleDataListEndpoint
514
- from cornflow.endpoints.example_data import ExampleDataListEndpoint
515
-
516
- original_roles = ExampleDataListEndpoint.ROLES_WITH_ACCESS.copy()
517
-
518
- # Verify initial permissions are created for all three roles
519
- # Get the view ID for the endpoint
520
- from cornflow.models import ViewModel
521
-
522
- view = ViewModel.query.filter_by(name="example-data").first()
523
- self.assertIsNotNone(view, "example-data view should exist")
524
-
525
- # Check permissions exist for all original roles
526
- from cornflow.shared.const import ACTIONS_MAP
527
- from cornflow.models import PermissionViewRoleModel
528
-
529
- # Check GET action permissions (action_id=1 is typically GET)
530
- get_action_id = 1
531
- initial_permissions = PermissionViewRoleModel.query.filter_by(
532
- api_view_id=view.id, action_id=get_action_id
533
- ).all()
534
-
535
- initial_role_ids = [perm.role_id for perm in initial_permissions]
536
- self.assertEqual(
537
- len(original_roles),
538
- len(initial_permissions),
539
- f"Should have permissions for all {len(original_roles)} original roles",
540
- )
541
-
542
- # Now simulate removing PLANNER_ROLE from ROLES_WITH_ACCESS
543
- from cornflow.shared.const import PLANNER_ROLE, VIEWER_ROLE, ADMIN_ROLE
544
-
545
- modified_roles = [VIEWER_ROLE, ADMIN_ROLE] # Remove PLANNER_ROLE
546
-
547
- # Temporarily modify the ROLES_WITH_ACCESS
548
- ExampleDataListEndpoint.ROLES_WITH_ACCESS = modified_roles
549
-
550
- try:
551
-
552
- # Run the permission registration again
553
- # (this simulates redeploying the app with modified roles)
554
- self.runner.invoke(register_base_assignations, ["-v"])
555
-
556
- # Check permissions after the "code change"
557
- updated_permissions = PermissionViewRoleModel.query.filter_by(
558
- api_view_id=view.id, action_id=get_action_id
559
- ).all()
560
-
561
- updated_role_ids = [perm.role_id for perm in updated_permissions]
562
-
563
- # THIS IS THE BUG: The permission for PLANNER_ROLE should be deleted
564
- # but it's not because the deletion logic is commented out
565
- # So we expect this assertion to FAIL, demonstrating the bug
566
- self.assertEqual(
567
- len(modified_roles),
568
- len(updated_permissions),
569
- f"After removing PLANNER_ROLE from code, should only have {len(modified_roles)} permissions, "
570
- f"but still has {len(updated_permissions)} permissions. "
571
- f"This demonstrates the bug: permissions are not deleted when roles are removed from ROLES_WITH_ACCESS.",
572
- )
573
-
574
- # Also check that PLANNER_ROLE permission was actually removed
575
- planner_permissions = [
576
- perm for perm in updated_permissions if perm.role_id == PLANNER_ROLE
577
- ]
578
- self.assertEqual(
579
- 0,
580
- len(planner_permissions),
581
- "PLANNER_ROLE permission should have been deleted but still exists",
582
- )
583
-
584
- finally:
585
- # Restore original ROLES_WITH_ACCESS to avoid affecting other tests
586
- ExampleDataListEndpoint.ROLES_WITH_ACCESS = original_roles
587
-
588
- def test_custom_role_permissions_are_preserved(self):
589
- """
590
- Test that permissions for custom roles (not in ROLES_MAP) are preserved
591
- during permission synchronization.
592
-
593
- This test verifies that when permissions are synchronized, only permissions
594
- for roles defined in ROLES_MAP are subject to deletion, while custom roles
595
- added manually to the database are preserved.
596
- """
597
- # First, initialize the access system normally
598
- self.runner.invoke(access_init)
599
-
600
- # Get a view to work with
601
- from cornflow.models import ViewModel, PermissionViewRoleModel, RoleModel
602
-
603
- view = ViewModel.query.filter_by(name="example-data").first()
604
- self.assertIsNotNone(view, "example-data view should exist")
605
-
606
- # Create a custom role that is NOT in ROLES_MAP
607
- custom_role_id = 999 # Use an ID that's not in ROLES_MAP
608
- custom_role = RoleModel({"id": custom_role_id, "name": "custom_role"})
609
- custom_role.save()
610
-
611
- # Create a permission for this custom role
612
- from cornflow.shared.const import GET_ACTION
613
-
614
- custom_permission = PermissionViewRoleModel(
615
- {"role_id": custom_role_id, "action_id": GET_ACTION, "api_view_id": view.id}
616
- )
617
- custom_permission.save()
618
-
619
- # Verify the custom permission exists
620
- custom_perms_before = PermissionViewRoleModel.query.filter_by(
621
- role_id=custom_role_id, action_id=GET_ACTION, api_view_id=view.id
622
- ).all()
623
- self.assertEqual(
624
- 1, len(custom_perms_before), "Custom permission should exist before sync"
625
- )
626
-
627
- # Run permission synchronization (this would previously delete custom role permissions)
628
- self.runner.invoke(register_base_assignations, ["-v"])
629
-
630
- # Verify the custom permission still exists after synchronization
631
- custom_perms_after = PermissionViewRoleModel.query.filter_by(
632
- role_id=custom_role_id, action_id=GET_ACTION, api_view_id=view.id
633
- ).all()
634
- self.assertEqual(
635
- 1,
636
- len(custom_perms_after),
637
- "Custom role permission should be preserved after synchronization. "
638
- "Permissions should only be deleted for roles defined in ROLES_MAP.",
639
- )
640
-
641
- # Clean up
642
- custom_permission.delete()
643
- custom_role.delete()