endoreg-db 0.8.6.1__py3-none-any.whl → 0.8.8.9__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.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

Files changed (503) hide show
  1. endoreg_db/authz/auth.py +74 -0
  2. endoreg_db/authz/backends.py +168 -0
  3. endoreg_db/authz/management/commands/list_routes.py +18 -0
  4. endoreg_db/authz/middleware.py +83 -0
  5. endoreg_db/authz/permissions.py +127 -0
  6. endoreg_db/authz/policy.py +218 -0
  7. endoreg_db/authz/views_auth.py +66 -0
  8. endoreg_db/config/env.py +13 -8
  9. endoreg_db/data/__init__.py +2 -11
  10. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +3 -3
  11. endoreg_db/data/event_classification/data.yaml +4 -0
  12. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  13. endoreg_db/data/examination/examinations/data.yaml +114 -14
  14. endoreg_db/data/examination/time-type/data.yaml +0 -3
  15. endoreg_db/data/examination_indication/endoscopy.yaml +108 -173
  16. endoreg_db/data/examination_indication_classification/endoscopy.yaml +0 -70
  17. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +33 -37
  18. endoreg_db/data/finding/00_generic.yaml +35 -0
  19. endoreg_db/data/finding/00_generic_complication.yaml +9 -0
  20. endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
  21. endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
  22. endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
  23. endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
  24. endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
  25. endoreg_db/data/finding_classification/00_generic.yaml +44 -0
  26. endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
  27. endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
  28. endoreg_db/data/finding_classification/02_colonoscopy_baseline.yaml +83 -0
  29. endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
  30. endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
  31. endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
  32. endoreg_db/data/finding_classification_choice/{yes_no_na.yaml → 00_generic.yaml} +5 -1
  33. endoreg_db/data/finding_classification_choice/{examination_setting_generic_types.yaml → 00_generic_baseline.yaml} +10 -2
  34. endoreg_db/data/finding_classification_choice/{complication_generic_types.yaml → 00_generic_complication.yaml} +1 -1
  35. endoreg_db/data/finding_classification_choice/{histology.yaml → 00_generic_histology.yaml} +1 -4
  36. endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
  37. endoreg_db/data/finding_classification_choice/{bowel_preparation.yaml → 02_colonoscopy_bowel_preparation.yaml} +1 -30
  38. endoreg_db/data/finding_classification_choice/{colonoscopy_not_complete_reason.yaml → 02_colonoscopy_generic.yaml} +1 -1
  39. endoreg_db/data/finding_classification_choice/{histology_polyp.yaml → 02_colonoscopy_histology.yaml} +1 -1
  40. endoreg_db/data/finding_classification_choice/{colonoscopy_location.yaml → 02_colonoscopy_location.yaml} +23 -4
  41. endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
  42. endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
  43. endoreg_db/data/finding_classification_choice/{colon_lesion_paris.yaml → 02_colonoscopy_polyp_morphology.yaml} +26 -8
  44. endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
  45. endoreg_db/data/finding_classification_type/{colonoscopy_basic.yaml → 00_generic.yaml} +18 -13
  46. endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
  47. endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
  48. endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
  49. endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
  50. endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
  51. endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
  52. endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
  53. endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
  54. endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
  55. endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
  56. endoreg_db/data/finding_type/data.yaml +8 -12
  57. endoreg_db/data/requirement/01_patient_data.yaml +93 -0
  58. endoreg_db/data/requirement/old/colon_polyp_intervention.yaml +49 -0
  59. endoreg_db/data/requirement/old/coloreg_colon_polyp.yaml +49 -0
  60. endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
  61. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +29 -12
  62. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  63. endoreg_db/data/requirement_set/{endoscopy_bleeding_risk.yaml → 02_endoscopy_bleeding_risk.yaml} +0 -6
  64. endoreg_db/data/requirement_set/90_coloreg.yaml +190 -0
  65. endoreg_db/data/requirement_set/_old_ +109 -0
  66. endoreg_db/data/requirement_set_type/data.yaml +21 -0
  67. endoreg_db/data/setup_config.yaml +4 -4
  68. endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
  69. endoreg_db/exceptions.py +4 -2
  70. endoreg_db/forms/examination_form.py +1 -1
  71. endoreg_db/helpers/data_loader.py +125 -53
  72. endoreg_db/helpers/default_objects.py +116 -81
  73. endoreg_db/import_files/__init__.py +27 -0
  74. endoreg_db/import_files/context/__init__.py +7 -0
  75. endoreg_db/import_files/context/default_sensitive_meta.py +81 -0
  76. endoreg_db/import_files/context/ensure_center.py +17 -0
  77. endoreg_db/import_files/context/file_lock.py +66 -0
  78. endoreg_db/import_files/context/import_context.py +43 -0
  79. endoreg_db/import_files/context/validate_directories.py +56 -0
  80. endoreg_db/import_files/file_storage/__init__.py +15 -0
  81. endoreg_db/import_files/file_storage/create_report_file.py +76 -0
  82. endoreg_db/import_files/file_storage/create_video_file.py +75 -0
  83. endoreg_db/import_files/file_storage/sensitive_meta_storage.py +39 -0
  84. endoreg_db/import_files/file_storage/state_management.py +400 -0
  85. endoreg_db/import_files/file_storage/storage.py +36 -0
  86. endoreg_db/import_files/import_service.md +26 -0
  87. endoreg_db/import_files/processing/__init__.py +11 -0
  88. endoreg_db/import_files/processing/report_processing/report_anonymization.py +94 -0
  89. endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
  90. endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
  91. endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +119 -0
  92. endoreg_db/import_files/pseudonymization/fake.py +52 -0
  93. endoreg_db/import_files/pseudonymization/k_anonymity.py +182 -0
  94. endoreg_db/import_files/pseudonymization/k_pseudonymity.py +128 -0
  95. endoreg_db/import_files/report_import_service.py +141 -0
  96. endoreg_db/import_files/video_import_service.py +150 -0
  97. endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
  98. endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
  99. endoreg_db/management/commands/import_report.py +130 -65
  100. endoreg_db/management/commands/import_video.py +9 -10
  101. endoreg_db/management/commands/import_video_with_classification.py +2 -2
  102. endoreg_db/management/commands/list_routes.py +18 -0
  103. endoreg_db/management/commands/load_ai_model_data.py +5 -5
  104. endoreg_db/management/commands/load_ai_model_label_data.py +9 -7
  105. endoreg_db/management/commands/load_base_db_data.py +5 -134
  106. endoreg_db/management/commands/load_center_data.py +12 -12
  107. endoreg_db/management/commands/load_contraindication_data.py +14 -16
  108. endoreg_db/management/commands/load_disease_classification_choices_data.py +15 -18
  109. endoreg_db/management/commands/load_disease_classification_data.py +15 -18
  110. endoreg_db/management/commands/load_disease_data.py +25 -28
  111. endoreg_db/management/commands/load_endoscope_data.py +20 -27
  112. endoreg_db/management/commands/load_event_data.py +14 -16
  113. endoreg_db/management/commands/load_examination_data.py +31 -44
  114. endoreg_db/management/commands/load_examination_indication_data.py +20 -21
  115. endoreg_db/management/commands/load_finding_data.py +52 -80
  116. endoreg_db/management/commands/load_information_source.py +21 -23
  117. endoreg_db/management/commands/load_lab_value_data.py +17 -26
  118. endoreg_db/management/commands/load_medication_data.py +13 -12
  119. endoreg_db/management/commands/load_organ_data.py +15 -19
  120. endoreg_db/management/commands/load_pdf_type_data.py +19 -18
  121. endoreg_db/management/commands/load_profession_data.py +14 -17
  122. endoreg_db/management/commands/load_qualification_data.py +20 -23
  123. endoreg_db/management/commands/load_report_reader_flag_data.py +17 -19
  124. endoreg_db/management/commands/load_requirement_data.py +62 -39
  125. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  126. endoreg_db/management/commands/load_risk_data.py +7 -6
  127. endoreg_db/management/commands/load_shift_data.py +20 -23
  128. endoreg_db/management/commands/load_tag_data.py +8 -11
  129. endoreg_db/management/commands/load_unit_data.py +17 -19
  130. endoreg_db/management/commands/setup_endoreg_db.py +3 -3
  131. endoreg_db/management/commands/start_filewatcher.py +46 -37
  132. endoreg_db/management/commands/storage_management.py +271 -203
  133. endoreg_db/management/commands/validate_video_files.py +1 -5
  134. endoreg_db/migrations/0001_initial.py +297 -250
  135. endoreg_db/models/__init__.py +78 -123
  136. endoreg_db/models/administration/__init__.py +21 -42
  137. endoreg_db/models/administration/ai/active_model.py +2 -2
  138. endoreg_db/models/administration/ai/ai_model.py +7 -6
  139. endoreg_db/models/administration/case/__init__.py +1 -15
  140. endoreg_db/models/administration/case/case.py +3 -3
  141. endoreg_db/models/administration/case/case_template/__init__.py +2 -14
  142. endoreg_db/models/administration/case/case_template/case_template.py +2 -124
  143. endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
  144. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
  145. endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
  146. endoreg_db/models/administration/center/center.py +33 -19
  147. endoreg_db/models/administration/center/center_product.py +12 -9
  148. endoreg_db/models/administration/center/center_resource.py +25 -19
  149. endoreg_db/models/administration/center/center_shift.py +21 -17
  150. endoreg_db/models/administration/center/center_waste.py +16 -8
  151. endoreg_db/models/administration/person/__init__.py +2 -0
  152. endoreg_db/models/administration/person/employee/employee.py +10 -5
  153. endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
  154. endoreg_db/models/administration/person/employee/employee_type.py +12 -6
  155. endoreg_db/models/administration/person/examiner/examiner.py +13 -11
  156. endoreg_db/models/administration/person/patient/__init__.py +2 -0
  157. endoreg_db/models/administration/person/patient/patient.py +129 -100
  158. endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
  159. endoreg_db/models/administration/person/person.py +4 -0
  160. endoreg_db/models/administration/person/profession/__init__.py +8 -4
  161. endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
  162. endoreg_db/models/administration/product/product.py +20 -15
  163. endoreg_db/models/administration/product/product_material.py +17 -18
  164. endoreg_db/models/administration/product/product_weight.py +12 -8
  165. endoreg_db/models/administration/product/reference_product.py +23 -55
  166. endoreg_db/models/administration/qualification/qualification.py +7 -3
  167. endoreg_db/models/administration/qualification/qualification_type.py +7 -3
  168. endoreg_db/models/administration/shift/scheduled_days.py +8 -5
  169. endoreg_db/models/administration/shift/shift.py +16 -12
  170. endoreg_db/models/administration/shift/shift_type.py +23 -31
  171. endoreg_db/models/label/__init__.py +8 -9
  172. endoreg_db/models/label/annotation/image_classification.py +10 -9
  173. endoreg_db/models/label/annotation/video_segmentation_annotation.py +23 -28
  174. endoreg_db/models/label/label.py +15 -15
  175. endoreg_db/models/label/label_set.py +19 -6
  176. endoreg_db/models/label/label_type.py +1 -1
  177. endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
  178. endoreg_db/models/label/label_video_segment/label_video_segment.py +98 -102
  179. endoreg_db/models/label/video_segmentation_label.py +4 -0
  180. endoreg_db/models/label/video_segmentation_labelset.py +4 -3
  181. endoreg_db/models/media/frame/frame.py +22 -22
  182. endoreg_db/models/media/pdf/raw_pdf.py +194 -194
  183. endoreg_db/models/media/pdf/report_file.py +25 -29
  184. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +55 -47
  185. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
  186. endoreg_db/models/media/processing_history/__init__.py +5 -0
  187. endoreg_db/models/media/processing_history/processing_history.py +96 -0
  188. endoreg_db/models/media/video/__init__.py +1 -0
  189. endoreg_db/models/media/video/create_from_file.py +139 -77
  190. endoreg_db/models/media/video/pipe_2.py +8 -9
  191. endoreg_db/models/media/video/video_file.py +174 -112
  192. endoreg_db/models/media/video/video_file_ai.py +288 -74
  193. endoreg_db/models/media/video/video_file_anonymize.py +38 -38
  194. endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
  195. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
  196. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
  197. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
  198. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
  199. endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
  200. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
  201. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
  202. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
  203. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
  204. endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
  205. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
  206. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
  207. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
  208. endoreg_db/models/media/video/video_file_io.py +113 -61
  209. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
  210. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
  211. endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
  212. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
  213. endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
  214. endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
  215. endoreg_db/models/media/video/video_file_segments.py +24 -17
  216. endoreg_db/models/media/video/video_metadata.py +19 -35
  217. endoreg_db/models/media/video/video_processing.py +96 -95
  218. endoreg_db/models/medical/contraindication/README.md +1 -0
  219. endoreg_db/models/medical/contraindication/__init__.py +13 -3
  220. endoreg_db/models/medical/disease.py +22 -16
  221. endoreg_db/models/medical/event.py +31 -18
  222. endoreg_db/models/medical/examination/__init__.py +13 -6
  223. endoreg_db/models/medical/examination/examination.py +39 -20
  224. endoreg_db/models/medical/examination/examination_indication.py +30 -95
  225. endoreg_db/models/medical/examination/examination_time.py +23 -8
  226. endoreg_db/models/medical/examination/examination_time_type.py +9 -6
  227. endoreg_db/models/medical/examination/examination_type.py +3 -4
  228. endoreg_db/models/medical/finding/finding.py +32 -40
  229. endoreg_db/models/medical/finding/finding_classification.py +42 -72
  230. endoreg_db/models/medical/finding/finding_intervention.py +25 -22
  231. endoreg_db/models/medical/finding/finding_type.py +13 -12
  232. endoreg_db/models/medical/hardware/endoscope.py +26 -26
  233. endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
  234. endoreg_db/models/medical/laboratory/lab_value.py +62 -91
  235. endoreg_db/models/medical/medication/medication.py +22 -10
  236. endoreg_db/models/medical/medication/medication_indication.py +29 -3
  237. endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
  238. endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
  239. endoreg_db/models/medical/medication/medication_schedule.py +27 -16
  240. endoreg_db/models/medical/organ/__init__.py +15 -12
  241. endoreg_db/models/medical/patient/medication_examples.py +6 -6
  242. endoreg_db/models/medical/patient/patient_disease.py +20 -23
  243. endoreg_db/models/medical/patient/patient_event.py +19 -22
  244. endoreg_db/models/medical/patient/patient_examination.py +48 -54
  245. endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
  246. endoreg_db/models/medical/patient/patient_finding.py +122 -139
  247. endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
  248. endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
  249. endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
  250. endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
  251. endoreg_db/models/medical/patient/patient_medication.py +27 -38
  252. endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
  253. endoreg_db/models/medical/risk/risk.py +7 -6
  254. endoreg_db/models/medical/risk/risk_type.py +8 -5
  255. endoreg_db/models/metadata/model_meta.py +60 -29
  256. endoreg_db/models/metadata/model_meta_logic.py +125 -18
  257. endoreg_db/models/metadata/pdf_meta.py +31 -24
  258. endoreg_db/models/metadata/sensitive_meta.py +105 -85
  259. endoreg_db/models/metadata/sensitive_meta_logic.py +198 -103
  260. endoreg_db/models/metadata/video_meta.py +51 -31
  261. endoreg_db/models/metadata/video_prediction_logic.py +16 -23
  262. endoreg_db/models/metadata/video_prediction_meta.py +29 -33
  263. endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
  264. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
  265. endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
  266. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
  267. endoreg_db/models/other/emission/emission_factor.py +18 -8
  268. endoreg_db/models/other/gender.py +10 -5
  269. endoreg_db/models/other/information_source.py +50 -29
  270. endoreg_db/models/other/material.py +9 -5
  271. endoreg_db/models/other/resource.py +6 -4
  272. endoreg_db/models/other/tag.py +10 -5
  273. endoreg_db/models/other/transport_route.py +13 -8
  274. endoreg_db/models/other/unit.py +10 -6
  275. endoreg_db/models/other/waste.py +6 -5
  276. endoreg_db/models/report/report.py +6 -0
  277. endoreg_db/models/requirement/requirement.py +329 -361
  278. endoreg_db/models/requirement/requirement_error.py +85 -0
  279. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  280. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
  281. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
  282. endoreg_db/models/requirement/requirement_operator.py +103 -112
  283. endoreg_db/models/requirement/requirement_set.py +74 -57
  284. endoreg_db/models/state/__init__.py +4 -4
  285. endoreg_db/models/state/abstract.py +2 -2
  286. endoreg_db/models/state/anonymization.py +12 -0
  287. endoreg_db/models/state/audit_ledger.py +49 -51
  288. endoreg_db/models/state/label_video_segment.py +9 -0
  289. endoreg_db/models/state/raw_pdf.py +101 -68
  290. endoreg_db/models/state/sensitive_meta.py +6 -2
  291. endoreg_db/models/state/video.py +110 -90
  292. endoreg_db/models/upload_job.py +35 -34
  293. endoreg_db/models/utils.py +28 -25
  294. endoreg_db/queries/__init__.py +3 -1
  295. endoreg_db/root_urls.py +21 -2
  296. endoreg_db/schemas/examination_evaluation.py +1 -1
  297. endoreg_db/serializers/__init__.py +2 -10
  298. endoreg_db/serializers/anonymization.py +18 -10
  299. endoreg_db/serializers/label_video_segment/label_video_segment.py +2 -29
  300. endoreg_db/serializers/meta/__init__.py +1 -6
  301. endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
  302. endoreg_db/serializers/misc/file_overview.py +11 -99
  303. endoreg_db/serializers/misc/sensitive_patient_data.py +50 -26
  304. endoreg_db/serializers/patient_examination/patient_examination.py +3 -3
  305. endoreg_db/serializers/pdf/anony_text_validation.py +39 -23
  306. endoreg_db/serializers/requirements/requirement_sets.py +92 -22
  307. endoreg_db/serializers/video/segmentation.py +2 -1
  308. endoreg_db/serializers/video/video_file_list.py +65 -34
  309. endoreg_db/serializers/video/video_processing_history.py +20 -5
  310. endoreg_db/services/__old/pdf_import.py +1487 -0
  311. endoreg_db/services/__old/video_import.py +1306 -0
  312. endoreg_db/services/anonymization.py +128 -89
  313. endoreg_db/services/lookup_service.py +65 -52
  314. endoreg_db/services/lookup_store.py +2 -2
  315. endoreg_db/services/pdf_import.py +0 -1382
  316. endoreg_db/services/report_import.py +10 -0
  317. endoreg_db/services/video_import.py +6 -1255
  318. endoreg_db/tasks/upload_tasks.py +79 -70
  319. endoreg_db/tasks/video_ingest.py +8 -4
  320. endoreg_db/urls/__init__.py +5 -32
  321. endoreg_db/urls/ai.py +32 -0
  322. endoreg_db/urls/media.py +121 -83
  323. endoreg_db/urls/root_urls.py +29 -0
  324. endoreg_db/utils/__init__.py +15 -5
  325. endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
  326. endoreg_db/utils/case_generator/__init__.py +3 -0
  327. endoreg_db/utils/dataloader.py +142 -40
  328. endoreg_db/utils/defaults/set_default_center.py +32 -0
  329. endoreg_db/utils/names.py +22 -16
  330. endoreg_db/utils/paths.py +110 -46
  331. endoreg_db/utils/permissions.py +2 -1
  332. endoreg_db/utils/pipelines/Readme.md +1 -1
  333. endoreg_db/utils/pipelines/process_video_dir.py +1 -1
  334. endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +655 -0
  335. endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +97 -0
  336. endoreg_db/utils/setup_config.py +8 -5
  337. endoreg_db/utils/storage.py +115 -0
  338. endoreg_db/utils/validate_endo_roi.py +8 -2
  339. endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
  340. endoreg_db/views/__init__.py +85 -183
  341. endoreg_db/views/ai/__init__.py +8 -0
  342. endoreg_db/views/ai/label.py +155 -0
  343. endoreg_db/views/anonymization/media_management.py +202 -166
  344. endoreg_db/views/anonymization/overview.py +99 -67
  345. endoreg_db/views/anonymization/validate.py +182 -44
  346. endoreg_db/views/media/__init__.py +7 -20
  347. endoreg_db/views/media/pdf_media.py +197 -174
  348. endoreg_db/views/media/sensitive_metadata.py +193 -138
  349. endoreg_db/views/media/video_media.py +89 -82
  350. endoreg_db/views/meta/__init__.py +0 -8
  351. endoreg_db/views/misc/__init__.py +1 -7
  352. endoreg_db/views/misc/upload_views.py +94 -93
  353. endoreg_db/views/patient/patient.py +5 -4
  354. endoreg_db/views/report/__init__.py +5 -7
  355. endoreg_db/views/{pdf → report}/reimport.py +22 -22
  356. endoreg_db/views/{pdf/pdf_stream.py → report/report_stream.py} +46 -39
  357. endoreg_db/views/requirement/evaluate.py +188 -187
  358. endoreg_db/views/requirement/lookup.py +17 -3
  359. endoreg_db/views/requirement/lookup_store.py +22 -90
  360. endoreg_db/views/requirement/requirement_utils.py +89 -0
  361. endoreg_db/views/video/__init__.py +23 -24
  362. endoreg_db/views/video/correction.py +201 -172
  363. endoreg_db/views/video/reimport.py +1 -1
  364. endoreg_db/views/{media/video_segments.py → video/segments_crud.py} +77 -40
  365. endoreg_db/views/video/{video_meta.py → video_meta_stats.py} +2 -2
  366. endoreg_db/views/video/video_stream.py +7 -8
  367. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/METADATA +7 -3
  368. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/RECORD +391 -413
  369. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/WHEEL +1 -1
  370. endoreg_db/data/finding/anatomy_colon.yaml +0 -128
  371. endoreg_db/data/finding/colonoscopy.yaml +0 -40
  372. endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +0 -56
  373. endoreg_db/data/finding/complication.yaml +0 -16
  374. endoreg_db/data/finding/data.yaml +0 -105
  375. endoreg_db/data/finding/examination_setting.yaml +0 -16
  376. endoreg_db/data/finding/medication_related.yaml +0 -18
  377. endoreg_db/data/finding/outcome.yaml +0 -12
  378. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +0 -95
  379. endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +0 -22
  380. endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +0 -25
  381. endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
  382. endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
  383. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +0 -68
  384. endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
  385. endoreg_db/data/finding_classification/colonoscopy_location.yaml +0 -80
  386. endoreg_db/data/finding_classification/colonoscopy_lst.yaml +0 -21
  387. endoreg_db/data/finding_classification/colonoscopy_nice.yaml +0 -20
  388. endoreg_db/data/finding_classification/colonoscopy_paris.yaml +0 -26
  389. endoreg_db/data/finding_classification/colonoscopy_sano.yaml +0 -22
  390. endoreg_db/data/finding_classification/colonoscopy_summary.yaml +0 -53
  391. endoreg_db/data/finding_classification/complication_generic.yaml +0 -25
  392. endoreg_db/data/finding_classification/examination_setting_generic.yaml +0 -40
  393. endoreg_db/data/finding_classification/histology_colo.yaml +0 -51
  394. endoreg_db/data/finding_classification/intervention_required.yaml +0 -26
  395. endoreg_db/data/finding_classification/medication_related.yaml +0 -23
  396. endoreg_db/data/finding_classification/visualized.yaml +0 -33
  397. endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
  398. endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
  399. endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
  400. endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +0 -15
  401. endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +0 -17
  402. endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
  403. endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +0 -14
  404. endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
  405. endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +0 -82
  406. endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
  407. endoreg_db/data/finding_classification_choice/outcome.yaml +0 -19
  408. endoreg_db/data/finding_intervention/endoscopy.yaml +0 -43
  409. endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
  410. endoreg_db/data/finding_intervention/endoscopy_egd.yaml +0 -128
  411. endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +0 -32
  412. endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +0 -9
  413. endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +0 -36
  414. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +0 -79
  415. endoreg_db/data/requirement/age.yaml +0 -26
  416. endoreg_db/data/requirement/gender.yaml +0 -25
  417. endoreg_db/management/commands/init_default_ai_model.py +0 -112
  418. endoreg_db/management/commands/reset_celery_schedule.py +0 -9
  419. endoreg_db/management/commands/validate_video.py +0 -204
  420. endoreg_db/migrations/0002_add_video_correction_models.py +0 -52
  421. endoreg_db/migrations/0003_add_center_display_name.py +0 -30
  422. endoreg_db/models/administration/permissions/__init__.py +0 -44
  423. endoreg_db/models/rule/__init__.py +0 -13
  424. endoreg_db/models/rule/rule.py +0 -27
  425. endoreg_db/models/rule/rule_applicator.py +0 -224
  426. endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
  427. endoreg_db/models/rule/rule_type.py +0 -20
  428. endoreg_db/models/rule/ruleset.py +0 -17
  429. endoreg_db/renames.yml +0 -8
  430. endoreg_db/serializers/_old/raw_pdf_meta_validation.py +0 -223
  431. endoreg_db/serializers/_old/raw_video_meta_validation.py +0 -179
  432. endoreg_db/serializers/_old/video.py +0 -71
  433. endoreg_db/serializers/meta/pdf_file_meta_extraction.py +0 -115
  434. endoreg_db/serializers/meta/report_meta.py +0 -53
  435. endoreg_db/serializers/report/__init__.py +0 -9
  436. endoreg_db/serializers/report/mixins.py +0 -45
  437. endoreg_db/serializers/report/report.py +0 -105
  438. endoreg_db/serializers/report/report_list.py +0 -22
  439. endoreg_db/serializers/report/secure_file_url.py +0 -26
  440. endoreg_db/serializers/video/video_metadata.py +0 -105
  441. endoreg_db/services/requirements_object.py +0 -147
  442. endoreg_db/services/storage_aware_video_processor.py +0 -344
  443. endoreg_db/urls/files.py +0 -6
  444. endoreg_db/urls/label_video_segment_validate.py +0 -33
  445. endoreg_db/urls/label_video_segments.py +0 -46
  446. endoreg_db/urls/report.py +0 -48
  447. endoreg_db/urls/video.py +0 -61
  448. endoreg_db/utils/case_generator/case_generator.py +0 -159
  449. endoreg_db/utils/case_generator/utils.py +0 -30
  450. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +0 -368
  451. endoreg_db/views/label/__init__.py +0 -5
  452. endoreg_db/views/label/label.py +0 -15
  453. endoreg_db/views/label_video_segment/__init__.py +0 -16
  454. endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +0 -44
  455. endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +0 -50
  456. endoreg_db/views/label_video_segment/label_video_segment.py +0 -77
  457. endoreg_db/views/label_video_segment/label_video_segment_by_label.py +0 -174
  458. endoreg_db/views/label_video_segment/label_video_segment_detail.py +0 -73
  459. endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +0 -46
  460. endoreg_db/views/label_video_segment/validate.py +0 -226
  461. endoreg_db/views/media/segments.py +0 -71
  462. endoreg_db/views/meta/available_files_list.py +0 -146
  463. endoreg_db/views/meta/report_meta.py +0 -53
  464. endoreg_db/views/meta/sensitive_meta_detail.py +0 -148
  465. endoreg_db/views/misc/secure_file_serving_view.py +0 -80
  466. endoreg_db/views/misc/secure_file_url_view.py +0 -84
  467. endoreg_db/views/misc/secure_url_validate.py +0 -79
  468. endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +0 -164
  469. endoreg_db/views/patient_finding_location/__init__.py +0 -5
  470. endoreg_db/views/patient_finding_location/pfl_create.py +0 -70
  471. endoreg_db/views/patient_finding_morphology/__init__.py +0 -5
  472. endoreg_db/views/patient_finding_morphology/pfm_create.py +0 -70
  473. endoreg_db/views/pdf/__init__.py +0 -8
  474. endoreg_db/views/report/report_list.py +0 -112
  475. endoreg_db/views/report/report_with_secure_url.py +0 -28
  476. endoreg_db/views/report/start_examination.py +0 -7
  477. endoreg_db/views/video/segmentation.py +0 -274
  478. endoreg_db/views/video/task_status.py +0 -49
  479. endoreg_db/views/video/timeline.py +0 -46
  480. endoreg_db/views/video/video_analyze.py +0 -52
  481. endoreg_db/views.py +0 -0
  482. /endoreg_db/data/requirement/{colonoscopy_baseline_austria.yaml → old/colonoscopy_baseline_austria.yaml} +0 -0
  483. /endoreg_db/data/requirement/{disease_cardiovascular.yaml → old/disease_cardiovascular.yaml} +0 -0
  484. /endoreg_db/data/requirement/{disease_classification_choice_cardiovascular.yaml → old/disease_classification_choice_cardiovascular.yaml} +0 -0
  485. /endoreg_db/data/requirement/{disease_hepatology.yaml → old/disease_hepatology.yaml} +0 -0
  486. /endoreg_db/data/requirement/{disease_misc.yaml → old/disease_misc.yaml} +0 -0
  487. /endoreg_db/data/requirement/{disease_renal.yaml → old/disease_renal.yaml} +0 -0
  488. /endoreg_db/data/requirement/{endoscopy_bleeding_risk.yaml → old/endoscopy_bleeding_risk.yaml} +0 -0
  489. /endoreg_db/data/requirement/{event_cardiology.yaml → old/event_cardiology.yaml} +0 -0
  490. /endoreg_db/data/requirement/{event_requirements.yaml → old/event_requirements.yaml} +0 -0
  491. /endoreg_db/data/requirement/{finding_colon_polyp.yaml → old/finding_colon_polyp.yaml} +0 -0
  492. /endoreg_db/{migrations/__init__.py → data/requirement/old/gender.yaml} +0 -0
  493. /endoreg_db/data/requirement/{lab_value.yaml → old/lab_value.yaml} +0 -0
  494. /endoreg_db/data/requirement/{medication.yaml → old/medication.yaml} +0 -0
  495. /endoreg_db/data/requirement_operator/{age.yaml → _old/age.yaml} +0 -0
  496. /endoreg_db/data/requirement_operator/{lab_operators.yaml → _old/lab_operators.yaml} +0 -0
  497. /endoreg_db/data/requirement_operator/{model_operators.yaml → _old/model_operators.yaml} +0 -0
  498. /endoreg_db/{models/media/video/refactor_plan.md → import_files/pseudonymization/__init__.py} +0 -0
  499. /endoreg_db/{models/media/video/video_file_frames.py → import_files/pseudonymization/pseudonymize.py} +0 -0
  500. /endoreg_db/models/{metadata/frame_ocr_result.py → report/__init__.py} +0 -0
  501. /endoreg_db/{urls/sensitive_meta.py → models/report/images.py} +0 -0
  502. /endoreg_db/utils/requirement_operator_logic/{lab_value_operators.py → _old/lab_value_operators.py} +0 -0
  503. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,74 @@
1
+ import jwt
2
+ import requests
3
+ from jwt import PyJWKClient
4
+ from django.conf import settings
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.auth.models import Group
7
+ from rest_framework import authentication, exceptions
8
+
9
+ User = get_user_model()
10
+
11
+ class KeycloakJWTAuthentication(authentication.BaseAuthentication):
12
+ """
13
+ Verifies Bearer JWTs against Keycloak JWKS.
14
+ Creates/updates a Django user and syncs groups if roles are present.
15
+ """
16
+
17
+ _jwks_client = None
18
+ _iss = None
19
+ _aud = None
20
+
21
+ @classmethod
22
+ def _init(cls):
23
+ if cls._jwks_client is None:
24
+ disc = requests.get(settings.OIDC_OP_DISCOVERY_ENDPOINT, timeout=5).json()
25
+ cls._jwks_client = PyJWKClient(disc["jwks_uri"])
26
+ cls._iss = disc["issuer"]
27
+ if cls._aud is None:
28
+ cls._aud = settings.OIDC_RP_CLIENT_ID
29
+
30
+ def authenticate(self, request):
31
+ auth = request.META.get("HTTP_AUTHORIZATION", "")
32
+ if not auth.startswith("Bearer "):
33
+ return None
34
+
35
+ token = auth.split(" ", 1)[1].strip()
36
+ try:
37
+ self._init()
38
+ signing_key = self._jwks_client.get_signing_key_from_jwt(token).key
39
+ claims = jwt.decode(
40
+ token,
41
+ signing_key,
42
+ algorithms=["RS256"],
43
+ audience=self._aud,
44
+ issuer=self._iss,
45
+ options={"require": ["exp", "iat", "iss"]},
46
+ )
47
+ except Exception as e:
48
+ raise exceptions.AuthenticationFailed(f"Invalid token: {e}")
49
+
50
+ username = claims.get("preferred_username") or claims.get("sub")
51
+ if not username:
52
+ raise exceptions.AuthenticationFailed("Token missing username/sub")
53
+
54
+ user, _ = User.objects.get_or_create(
55
+ username=username,
56
+ defaults={
57
+ "email": claims.get("email", ""),
58
+ "first_name": (claims.get("given_name") or "")[:150],
59
+ "last_name": (claims.get("family_name") or "")[:150],
60
+ },
61
+ )
62
+
63
+ # Optional: sync Django groups for API users too
64
+ roles = set(claims.get("roles", []) or [])
65
+ roles.update((claims.get("realm_access") or {}).get("roles", []) or [])
66
+ if roles:
67
+ groups = []
68
+ for r in roles:
69
+ grp, _ = Group.objects.get_or_create(name=r)
70
+ groups.append(grp)
71
+ user.groups.set(groups)
72
+ user.save()
73
+
74
+ return (user, None)
@@ -0,0 +1,168 @@
1
+ # endoreg_db/authz/backends.py
2
+ #
3
+ # Purpose:
4
+ # Authenticate browser users via OIDC (Keycloak), create/update a Django User on first login,
5
+ # and sync Keycloak *realm roles* into Django Groups so DRF permissions can check them.
6
+ #
7
+ # Flow:
8
+ # /oidc/authenticate/ → Keycloak login → /oidc/callback/ → this backend verifies ID token
9
+ # and returns a Django User instance. We then:
10
+ # - create the user on first login (create_user)
11
+ # - update profile fields on later logins (update_user)
12
+ # - sync roles → Django groups (so request.user.groups contains "data:read", etc.)
13
+ #
14
+ # Notes:
15
+ # - We assume you added a Keycloak mapper so roles appear either as a flat "roles" claim,
16
+ # or the standard "realm_access": {"roles": [...]}.
17
+ # - We *replace* the user's groups each login to match Keycloak (source of truth).
18
+ # - If you also need *client roles* (per-client), see the optional code in _extract_realm_roles().
19
+ #
20
+ # Settings that enable this backend (in config/settings/dev.py):
21
+ # AUTHENTICATION_BACKENDS = (
22
+ # "endoreg_db.authz.backends.KeycloakOIDCBackend",
23
+ # "django.contrib.auth.backends.ModelBackend",
24
+ # )
25
+
26
+ from mozilla_django_oidc.auth import OIDCAuthenticationBackend
27
+ from django.contrib.auth import get_user_model
28
+ from django.contrib.auth.models import Group
29
+
30
+ User = get_user_model()
31
+
32
+
33
+ def _extract_realm_roles(claims):
34
+ """
35
+ Extract Keycloak *realm* roles from ID token claims.
36
+
37
+ We support two common forms:
38
+ 1) A custom 'roles' flat claim (if you added a "roles-flat" mapper in Keycloak)
39
+ e.g., "roles": ["data:read", "data:write"]
40
+ 2) The standard 'realm_access.roles' structure from Keycloak
41
+ e.g., "realm_access": {"roles": ["data:read", "data:write"]}
42
+
43
+ Returns:
44
+ set[str]: unique, non-empty role names.
45
+ """
46
+ roles = set()
47
+
48
+ # 1) Custom/flat roles claim (if you configured such a mapper in Keycloak)
49
+ roles.update(claims.get("roles", []) or [])
50
+
51
+ # 2) Standard Keycloak realm roles location
52
+ roles.update((claims.get("realm_access") or {}).get("roles", []) or [])
53
+
54
+ # OPTIONAL — include client roles as well (uncomment if you use them)
55
+ # resource_access = claims.get("resource_access") or {}
56
+ # for client_id, entry in resource_access.items():
57
+ # for r in entry.get("roles", []) or []:
58
+ # # Prefix client roles to avoid name collisions with realm roles
59
+ # roles.add(f"{client_id}:{r}")
60
+
61
+ # Filter out any non-strings / empties, just in case
62
+ return {r for r in roles if isinstance(r, str) and r}
63
+
64
+
65
+ class KeycloakOIDCBackend(OIDCAuthenticationBackend):
66
+ """
67
+ OIDC backend used by mozilla-django-oidc during login.
68
+
69
+ Responsibilities:
70
+ - Parse OIDC claims from Keycloak
71
+ - Create a new Django user on first login (create_user)
72
+ - Update the user on subsequent logins (update_user)
73
+ - Sync Keycloak roles → Django Groups so your PolicyPermission can check them
74
+ """
75
+
76
+ # Called by the base class when no existing user matches the claims.
77
+ def create_user(self, claims):
78
+ """
79
+ Create a new Django user on first OIDC login.
80
+
81
+ Args:
82
+ claims (dict): verified ID token claims (e.g., sub, email, names, roles...)
83
+
84
+ Returns:
85
+ User: the newly created Django user
86
+ """
87
+ # Preferred username is the most human-friendly identifier in Keycloak.
88
+ # Fallback to 'sub' (the stable subject identifier) if needed.
89
+ username = claims.get("preferred_username") or claims.get("sub")
90
+
91
+ # Create a minimal user; no password is set (OIDC will handle auth).
92
+ user = User.objects.create_user(
93
+ username=username,
94
+ email=claims.get("email", ""),
95
+ first_name=(claims.get("given_name") or "")[:150],
96
+ last_name=(claims.get("family_name") or "")[:150],
97
+ )
98
+
99
+ # Ensure Django groups mirror Keycloak roles immediately.
100
+ self._sync_groups(user, claims)
101
+ return user
102
+
103
+ # Called by the base class when a matching user already exists.
104
+ def update_user(self, user, claims):
105
+ """
106
+ Update existing Django user profile fields and resync groups.
107
+
108
+ Args:
109
+ user (User): existing Django user matched from claims
110
+ claims (dict): verified ID token claims
111
+
112
+ Returns:
113
+ User: the updated user
114
+ """
115
+ # Keep user profile in sync with IdP data (safe truncation to field max length)
116
+ user.email = claims.get("email", user.email)
117
+ user.first_name = (claims.get("given_name") or user.first_name)[:150]
118
+ user.last_name = (claims.get("family_name") or user.last_name)[:150]
119
+ user.save(update_fields=["email", "first_name", "last_name"])
120
+
121
+ # Keep roles (groups) in sync on every login
122
+ self._sync_groups(user, claims)
123
+ return user
124
+
125
+ def _sync_groups(self, user, claims):
126
+ """
127
+ Make Django Groups *exactly* match the roles coming from Keycloak.
128
+
129
+ Behavior:
130
+ - For each incoming role, ensure a Django Group exists (get_or_create).
131
+ - Replace the user's groups with that exact set (source-of-truth = Keycloak).
132
+ - This means if a role is removed in Keycloak, it disappears in Django at next login.
133
+
134
+ If you prefer "additive only" behavior (never remove), change user.groups.set(...)
135
+ to a union/update pattern instead.
136
+ """
137
+ kc_roles = _extract_realm_roles(claims)
138
+
139
+ groups = []
140
+ for r in kc_roles:
141
+ grp, _ = Group.objects.get_or_create(name=r)
142
+ groups.append(grp)
143
+
144
+ # Replace membership to exactly match Keycloak roles
145
+ user.groups.set(groups)
146
+ user.save()
147
+
148
+ # OPTIONAL — map specific roles to Django staff/superuser:
149
+ # if "admin" in kc_roles or "realm-admin" in kc_roles:
150
+ # user.is_staff = True
151
+ # # user.is_superuser = True # if you truly want full Django superuser
152
+ # else:
153
+ # user.is_staff = False
154
+ # user.save(update_fields=["is_staff", "is_superuser"])
155
+
156
+ def filter_users_by_claims(self, claims):
157
+ """
158
+ Return the queryset of users matching the incoming claims.
159
+
160
+ The base class will use this to decide if create_user or update_user should run.
161
+
162
+ We match on preferred_username (case-insensitive). If not present, fall back to 'sub'.
163
+ """
164
+ username = claims.get("preferred_username") or claims.get("sub")
165
+ if not username:
166
+ # No usable identifier → no match
167
+ return self.UserModel.objects.none()
168
+ return self.UserModel.objects.filter(username__iexact=username)
@@ -0,0 +1,18 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.urls import get_resolver, URLPattern, URLResolver
3
+
4
+ def iter_patterns(prefix, patterns):
5
+ for p in patterns:
6
+ if isinstance(p, URLPattern):
7
+ yield p
8
+ elif isinstance(p, URLResolver):
9
+ yield from iter_patterns(prefix, p.url_patterns)
10
+
11
+ class Command(BaseCommand):
12
+ help = "List all URL names (useful to fill policy.py)"
13
+
14
+ def handle(self, *args, **options):
15
+ resolver = get_resolver()
16
+ for p in iter_patterns("", resolver.url_patterns):
17
+ if p.name:
18
+ self.stdout.write(p.name)
@@ -0,0 +1,83 @@
1
+ # endoreg_db/authz/middleware.py
2
+ #
3
+ # Purpose:
4
+ # - For *browser requests* that hit protected API URLs (e.g., /api/...), make sure the user
5
+ # is authenticated via Keycloak. If not, redirect them to the OIDC login view and remember
6
+ # the original URL in ?next= so they come back to the same endpoint after login.
7
+ # - For *API clients* sending a Bearer token, DO NOT redirect (that would break API usage).
8
+ # Let DRF handle authentication/authorization and return 401/403 as appropriate.
9
+ #
10
+ # How it integrates:
11
+ # - This middleware is appended in settings via KEYCLOAK.EXTRA_MIDDLEWARE (in dev.py).
12
+ # - It assumes AuthenticationMiddleware has already run (declared in base.py), so
13
+ # request.user is available and accurate.
14
+ #
15
+ # Security model:
16
+ # - We only redirect *browser* requests (no Authorization header) that target protected prefixes.
17
+ # - We attach the original URL as ?next=<relative-path>. mozilla-django-oidc will read this
18
+ # and redirect back after a successful login.
19
+ # - Optional: you can sanitize/validate the next parameter to avoid open redirects,
20
+ # though using a relative path from request.get_full_path() is already safe.
21
+
22
+ from django.shortcuts import redirect
23
+ from django.conf import settings
24
+ from urllib.parse import urlencode
25
+
26
+ # Any URL path that starts with one of these prefixes is considered "protected" for browser UX.
27
+ # You can add more prefixes if you want the same login-redirect behavior elsewhere
28
+ # (e.g., PROTECTED_PREFIXES = ("/api/", "/reports/", "/dashboard/")).
29
+ #PROTECTED_PREFIXES = ("/api/",)
30
+
31
+ # Protect the SPA shell too (everything except static/assets/oidc)
32
+ PROTECTED_PREFIXES = ("/",) # catch-all; we'll skip known public paths below
33
+
34
+ PUBLIC_PREFIXES = (
35
+ "/static/",
36
+ "/assets/",
37
+ "/media/",
38
+ "/favicon.ico",
39
+ "/oidc/", # OIDC endpoints must stay public
40
+ "/__vite", # if Vite dev assets ever used
41
+ )
42
+
43
+ class LoginRequiredForAPIsMiddleware:
44
+ """
45
+ For browser traffic:
46
+ - If a user hits a protected URL without being authenticated, redirect to OIDC login
47
+ and include ?next=<original-url> so the user returns to the same endpoint post-login.
48
+ For API clients:
49
+ - If the request has an "Authorization: Bearer <token>" header, do not redirect;
50
+ let DRF auth handle it (token flows expect 401/403, not 302).
51
+
52
+ """
53
+
54
+ def __init__(self, get_response): self.get_response = get_response
55
+
56
+ def __call__(self, request):
57
+ # request.path is the URL path without scheme/host/query (e.g., "/api/patients/").
58
+ # If for any reason it's None/empty, coerce to empty string so startswith won’t explode.
59
+ path = request.path or ""
60
+ # --- Exclusions so we don't block assets, HMR, OIDC endpoints, favicon, etc.
61
+ # Allow static, assets, vite HMR, favicon, and OIDC endpoints without redirect
62
+ # Skip public stuff
63
+ if path.startswith(PUBLIC_PREFIXES):
64
+ return self.get_response(request)
65
+
66
+ # If not protected, pass through (shouldn’t happen with PROTECTED_PREFIXES=('/' ,))
67
+ if not path.startswith(PROTECTED_PREFIXES):
68
+ return self.get_response(request)
69
+
70
+ # API/token clients never get redirected
71
+ auth = request.META.get("HTTP_AUTHORIZATION", "")
72
+ if auth.startswith("Bearer "):
73
+ return self.get_response(request)
74
+
75
+ # 3) Browser without session → redirect to OIDC
76
+ if not request.user.is_authenticated:
77
+ from django.conf import settings
78
+ from urllib.parse import urlencode
79
+ params = urlencode({"next": request.get_full_path()})
80
+ return redirect(f"{settings.LOGIN_URL}?{params}")
81
+
82
+ # 4) Authenticated → pass through
83
+ return self.get_response(request)
@@ -0,0 +1,127 @@
1
+ # endoreg_db/authz/permissions.py
2
+ #
3
+ # Purpose
4
+ # -------
5
+ # Enforce your route → role policy:
6
+ # - In DEBUG: allow everything (dev convenience).
7
+ # - In PROD: look at the user's Django Groups (synced from Keycloak roles)
8
+ # and decide per-route using REQUIRED_ROLES and DEFAULT_ROLE_BY_METHOD.
9
+ #
10
+ # How it plugs in
11
+ # ---------------
12
+ # Add this class to DRF's global permission chain in settings (you already did):
13
+ # REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = (
14
+ # "endoreg_db.utils.permissions.EnvironmentAwarePermission",
15
+ # "endoreg_db.authz.permissions.PolicyPermission",
16
+ # )
17
+ # The first class gates "auth required in prod"; this class enforces *which role*
18
+ # is needed, per route, using policy.py.
19
+ #
20
+ # Key ideas
21
+ # ---------
22
+ # - DRF route names for ViewSets are "<basename>-<action>", e.g., "patient-list".
23
+ # - REQUIRED_ROLES maps these names to a role (e.g., "data:read"/"data:write").
24
+ # - If a route isn’t listed, DEFAULT_ROLE_BY_METHOD is used ("GET"→read, writes→write).
25
+ # - Role satisfaction rule (in policy.satisfies): "write ⇒ read".
26
+ # - User roles come from Django Groups, set at OIDC login by your OIDC backend.
27
+
28
+ from rest_framework.permissions import BasePermission
29
+ from django.contrib.auth.models import AnonymousUser
30
+ from django.utils.functional import cached_property
31
+ from endoreg_db.utils.permissions import is_debug_mode
32
+ from .policy import REQUIRED_ROLES, DEFAULT_ROLE_BY_METHOD, satisfies, get_needed_role
33
+ import logging
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ def _normalized_route_name(request, view) -> str:
38
+ """
39
+ Return a stable, de-namespaced route name, e.g. 'patient-list'.
40
+ Prefer resolver_match.view_name (may be 'endoreg_db:patient-list'),
41
+ fallback to url_name, then class name.
42
+ """
43
+ rm = getattr(request, "resolver_match", None)
44
+ if rm:
45
+ # Try namespaced form first (strip namespace)
46
+ view_name = getattr(rm, "view_name", "") or ""
47
+ if view_name:
48
+ return view_name.split(":")[-1]
49
+ url_name = getattr(rm, "url_name", "") or ""
50
+ if url_name:
51
+ return url_name
52
+ return view.__class__.__name__
53
+
54
+ def _route_name(request, view):
55
+ """
56
+ Resolve a stable name for the current endpoint.
57
+
58
+ For DRF ViewSets registered via DefaultRouter:
59
+ - request.resolver_match.url_name is typically "<basename>-<action>"
60
+ e.g., "patient-list", "patient-detail", "check_pe_exist"
61
+ For plain APIViews or function views with path(name="..."):
62
+ - .url_name is that explicit name.
63
+ Fallback:
64
+ - If resolver info is missing (edge cases), use the class name as a last resort.
65
+
66
+ NOTE: Namespaces (e.g., "api:patient-list") do not affect .url_name; it's just "patient-list".
67
+ """
68
+ rm = getattr(request, "resolver_match", None)
69
+ if rm and rm.url_name:
70
+ return rm.url_name
71
+ return view.__class__.__name__ # last-resort fallback (rarely used in practice)
72
+
73
+
74
+ class PolicyPermission(BasePermission):
75
+ """
76
+ Enforce route→role mapping from policy.py.
77
+
78
+ Behavior:
79
+ - DEBUG: allow everything (keeps dev flow smooth).
80
+ - PROD: require authentication AND the right role.
81
+ Roles are read from request.user.groups (synced from Keycloak realm roles).
82
+
83
+ Why cached_property?
84
+ - REQUIRED_ROLES is a module-level dict; caching avoids re-reading it for every request.
85
+ (It remains live—if you edit the dict at runtime in tests, restart to refresh.)
86
+ """
87
+
88
+ @cached_property
89
+ def _required_roles(self):
90
+ return REQUIRED_ROLES
91
+
92
+ def has_permission(self, request, view):
93
+ route = _normalized_route_name(request, view)
94
+ method = (request.method or "").upper()
95
+
96
+ # 1) DEBUG bypass
97
+ if is_debug_mode():
98
+ logger.info("RBAC BYPASS (DEBUG): route=%s method=%s user=%s",
99
+ route, method, getattr(getattr(request, "user", None), "username", "anon"))
100
+ return True
101
+
102
+ # 2) Must be authenticated
103
+ user = getattr(request, "user", None)
104
+ if not user or isinstance(user, AnonymousUser) or not user.is_authenticated:
105
+ logger.info("RBAC DENY (UNAUTH): route=%s method=%s", route, method)
106
+ return False
107
+
108
+ # 3) Determine needed role
109
+ needed = get_needed_role(route, method)
110
+ if not needed:
111
+ logger.info(
112
+ "RBAC DENY (NO ROLE): route=%s method=%s reason=no mapping",
113
+ route, method
114
+ )
115
+ return False
116
+
117
+ # 4) Collect roles and decide
118
+ user_roles = set(user.groups.values_list("name", flat=True))
119
+ allowed = satisfies(user_roles, needed)
120
+
121
+ logger.info(
122
+ "RBAC DECISION: route=%s method=%s need=%s user=%s roles=%s => %s",
123
+ route, method, needed, getattr(user, "username", "anon"),
124
+ sorted(user_roles), "ALLOW" if allowed else "DENY"
125
+ )
126
+
127
+ return allowed
@@ -0,0 +1,218 @@
1
+ """
2
+ Authorization policy for DRF routes, backed by Keycloak realm roles.
3
+
4
+ Concepts
5
+ ========
6
+
7
+ - Keycloak holds *roles* (realm roles) like "patient:read", "patient:write",
8
+ "video:read", "video:write", "admin", etc.
9
+ - At login, those roles are synced to Django Groups with the *same* names.
10
+ - This file defines how DRF *routes* map to those roles.
11
+
12
+ Goals
13
+ =====
14
+
15
+ 1) Be DYNAMIC:
16
+ - By default, we assume:
17
+ GET/HEAD/OPTIONS → "<resource>:read"
18
+ POST/PUT/PATCH/DELETE → "<resource>:write"
19
+ - You only override special cases.
20
+
21
+ 2) Be EXPLICIT where needed:
22
+ - Some routes may require a specific role (e.g. "admin").
23
+
24
+ 3) Keep the “write ⇒ read” convention:
25
+ - If a user has "patient:write", they automatically satisfy "patient:read".
26
+ """
27
+
28
+ from typing import Dict, Union
29
+
30
+ # ------------------------------------------------------------
31
+ # Types
32
+ # ------------------------------------------------------------
33
+
34
+ # A route can map to:
35
+ # - a single role string ("patient:read")
36
+ # - or per-method roles: {"GET": "patient:read", "POST": "patient:write", ...}
37
+ RouteRoles = Dict[str, Union[str, Dict[str, str]]]
38
+
39
+ # ------------------------------------------------------------
40
+ # Resource → roles (technical roles in Keycloak)
41
+ # ------------------------------------------------------------
42
+ # These are the "technical" roles you create in Keycloak
43
+ # and assign to users/groups (directly or via composite roles).
44
+ #
45
+ # For each resource, define which role is used for read & write.
46
+ RESOURCE_ROLES = {
47
+ "patient": {
48
+ "read": "patient:read",
49
+ "write": "patient:write",
50
+ },
51
+ "video": {
52
+ "read": "video:read",
53
+ "write": "video:write",
54
+ },
55
+ #anonymization resource
56
+ "anonymization": {
57
+ "read": "anonymization:read",
58
+ "write": "anonymization:write",
59
+ },
60
+ # Add more resources as needed:
61
+ # "report": {"read": "report:read", "write": "report:write"},
62
+ }
63
+
64
+ # ------------------------------------------------------------
65
+ # Route → resource mapping
66
+ # ------------------------------------------------------------
67
+ # Map DRF route names to a resource key above.
68
+ #
69
+ # Route names:
70
+ # - ViewSet via DefaultRouter:
71
+ # basename="patient" → "patient-list", "patient-detail"
72
+ # - @action on a ViewSet:
73
+ # "<basename>-<action_name>"
74
+ # - path(..., name="..."):
75
+ # exactly that "name"
76
+ ROUTE_RESOURCE = {
77
+ # Patients
78
+ "patient-list": "patient", # /api/patients/
79
+ "patient-detail": "patient", # /api/patients/{id}/
80
+
81
+ # Custom patient helper
82
+ "check_pe_exist": "patient",
83
+
84
+ # Example for videos (if you have these ViewSets registered)
85
+ "videos-list": "video",
86
+ "videos-detail": "video",
87
+
88
+ "anonymization_items_overview": "anonymization",
89
+ # Add more mappings as your API grows
90
+
91
+
92
+ }
93
+
94
+ # ------------------------------------------------------------
95
+ # Explicit overrides by route
96
+ # ------------------------------------------------------------
97
+ # ONLY put something here if it deviates from the normal pattern.
98
+ #
99
+ # Example use cases:
100
+ # - admin-only DELETE
101
+ # - public GET that doesn't require login
102
+ #
103
+ # If a route is NOT in REQUIRED_ROLES, the policy falls back to:
104
+ # 1) ROUTE_RESOURCE + RESOURCE_ROLES
105
+ # 2) DEFAULT_ROLE_BY_METHOD
106
+ REQUIRED_ROLES: RouteRoles = {
107
+ # Example: make patient DELETE admin-only (optional)
108
+ # "patient-detail": {
109
+ # "DELETE": "admin", # admin role in Keycloak
110
+ # },
111
+
112
+ # Example: a special helper route that you always want read-only patients role:
113
+ # "check_pe_exist": "patient:read",
114
+ }
115
+
116
+ # ------------------------------------------------------------
117
+ # Fallback by HTTP method (used when no per-route override)
118
+ # ------------------------------------------------------------
119
+ # This is the last fallback if a route is not in REQUIRED_ROLES
120
+ # *and* not in ROUTE_RESOURCE.
121
+ #
122
+ # If you want global "data:read"/"data:write" roles to stay valid,
123
+ # you can leave this as "data:read"/"data:write".
124
+ #
125
+ # If you move fully to resource-based roles, you can leave this as None
126
+ # or a generic "data:read"/"data:write" depending on your preference.
127
+ DEFAULT_ROLE_BY_METHOD = {
128
+ "GET": "data:read",
129
+ "HEAD": "data:read",
130
+ "OPTIONS": "data:read",
131
+ "POST": "data:write",
132
+ "PUT": "data:write",
133
+ "PATCH": "data:write",
134
+ "DELETE": "data:write",
135
+ }
136
+
137
+
138
+
139
+ # ------------------------------------------------------------
140
+ # Role satisfaction rule
141
+ # ------------------------------------------------------------
142
+
143
+ def satisfies(user_roles: set[str], needed: str) -> bool:
144
+ """
145
+ Return True if user_roles satisfy the needed role.
146
+
147
+ Rules:
148
+ - Exact match → allow
149
+ - "write ⇒ read":
150
+ If needed ends with ':read', having '<base>:write' also counts.
151
+
152
+ Examples:
153
+ user_roles = {"patient:read"}:
154
+ satisfies(..., "patient:read") → True
155
+ satisfies(..., "patient:write") → False
156
+
157
+ user_roles = {"patient:write"}:
158
+ satisfies(..., "patient:write") → True
159
+ satisfies(..., "patient:read") → True (write ⇒ read)
160
+ """
161
+ if not needed:
162
+ return False
163
+
164
+ # Global override: any user with role "endoregdb_user" passes all checks
165
+ if "endoregdb_user" in user_roles:
166
+ return True
167
+
168
+ # 1) exact role match
169
+ if needed in user_roles:
170
+ return True
171
+
172
+ # 2) write⇒read shortcut
173
+ if needed.endswith(":read"):
174
+ base = needed.rsplit(":", 1)[0]
175
+ if f"{base}:write" in user_roles:
176
+ return True
177
+
178
+ return False
179
+
180
+
181
+ # ------------------------------------------------------------
182
+ # Helper: compute which role is needed for a given route + method
183
+ # ------------------------------------------------------------
184
+
185
+ def get_needed_role(route_name: str, method: str) -> str | None:
186
+ """
187
+ Compute the required role for a given route + HTTP method.
188
+
189
+ Priority:
190
+ 1) REQUIRED_ROLES[route_name] if present
191
+ - if dict: use per-method role if defined
192
+ - if str : use that role for all methods
193
+ 2) ROUTE_RESOURCE + RESOURCE_ROLES (resource-based policy)
194
+ - e.g. route "patient-list" with GET → "patient:read"
195
+ 3) DEFAULT_ROLE_BY_METHOD[method] as final fallback
196
+ - e.g. "data:read"/"data:write" if you keep those as global roles.
197
+ """
198
+ method = (method or "").upper()
199
+
200
+ # 1) explicit per-route overrides
201
+ per_route = REQUIRED_ROLES.get(route_name)
202
+ if isinstance(per_route, dict):
203
+ role = per_route.get(method)
204
+ if role:
205
+ return role
206
+ elif isinstance(per_route, str):
207
+ return per_route # one role for all methods of that route
208
+
209
+ # 2) resource-based default
210
+ resource = ROUTE_RESOURCE.get(route_name)
211
+ if resource in RESOURCE_ROLES:
212
+ op = "read" if method in ("GET", "HEAD", "OPTIONS") else "write"
213
+ role = RESOURCE_ROLES[resource].get(op)
214
+ if role:
215
+ return role
216
+
217
+ # 3) global fallback by method
218
+ return DEFAULT_ROLE_BY_METHOD.get(method)