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.
- endoreg_db/authz/auth.py +74 -0
- endoreg_db/authz/backends.py +168 -0
- endoreg_db/authz/management/commands/list_routes.py +18 -0
- endoreg_db/authz/middleware.py +83 -0
- endoreg_db/authz/permissions.py +127 -0
- endoreg_db/authz/policy.py +218 -0
- endoreg_db/authz/views_auth.py +66 -0
- endoreg_db/config/env.py +13 -8
- endoreg_db/data/__init__.py +2 -11
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +3 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/examination/examinations/data.yaml +114 -14
- endoreg_db/data/examination/time-type/data.yaml +0 -3
- endoreg_db/data/examination_indication/endoscopy.yaml +108 -173
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +0 -70
- endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +33 -37
- endoreg_db/data/finding/00_generic.yaml +35 -0
- endoreg_db/data/finding/00_generic_complication.yaml +9 -0
- endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
- endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
- endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
- endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
- endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
- endoreg_db/data/finding_classification/00_generic.yaml +44 -0
- endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
- endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
- endoreg_db/data/finding_classification/02_colonoscopy_baseline.yaml +83 -0
- endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
- endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
- endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
- endoreg_db/data/finding_classification_choice/{yes_no_na.yaml → 00_generic.yaml} +5 -1
- endoreg_db/data/finding_classification_choice/{examination_setting_generic_types.yaml → 00_generic_baseline.yaml} +10 -2
- endoreg_db/data/finding_classification_choice/{complication_generic_types.yaml → 00_generic_complication.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{histology.yaml → 00_generic_histology.yaml} +1 -4
- endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
- endoreg_db/data/finding_classification_choice/{bowel_preparation.yaml → 02_colonoscopy_bowel_preparation.yaml} +1 -30
- endoreg_db/data/finding_classification_choice/{colonoscopy_not_complete_reason.yaml → 02_colonoscopy_generic.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{histology_polyp.yaml → 02_colonoscopy_histology.yaml} +1 -1
- endoreg_db/data/finding_classification_choice/{colonoscopy_location.yaml → 02_colonoscopy_location.yaml} +23 -4
- endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
- endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
- endoreg_db/data/finding_classification_choice/{colon_lesion_paris.yaml → 02_colonoscopy_polyp_morphology.yaml} +26 -8
- endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
- endoreg_db/data/finding_classification_type/{colonoscopy_basic.yaml → 00_generic.yaml} +18 -13
- endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
- endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
- endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
- endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
- endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
- endoreg_db/data/finding_type/data.yaml +8 -12
- endoreg_db/data/requirement/01_patient_data.yaml +93 -0
- endoreg_db/data/requirement/old/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/old/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +29 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/{endoscopy_bleeding_risk.yaml → 02_endoscopy_bleeding_risk.yaml} +0 -6
- endoreg_db/data/requirement_set/90_coloreg.yaml +190 -0
- endoreg_db/data/requirement_set/_old_ +109 -0
- endoreg_db/data/requirement_set_type/data.yaml +21 -0
- endoreg_db/data/setup_config.yaml +4 -4
- endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
- endoreg_db/exceptions.py +4 -2
- endoreg_db/forms/examination_form.py +1 -1
- endoreg_db/helpers/data_loader.py +125 -53
- endoreg_db/helpers/default_objects.py +116 -81
- endoreg_db/import_files/__init__.py +27 -0
- endoreg_db/import_files/context/__init__.py +7 -0
- endoreg_db/import_files/context/default_sensitive_meta.py +81 -0
- endoreg_db/import_files/context/ensure_center.py +17 -0
- endoreg_db/import_files/context/file_lock.py +66 -0
- endoreg_db/import_files/context/import_context.py +43 -0
- endoreg_db/import_files/context/validate_directories.py +56 -0
- endoreg_db/import_files/file_storage/__init__.py +15 -0
- endoreg_db/import_files/file_storage/create_report_file.py +76 -0
- endoreg_db/import_files/file_storage/create_video_file.py +75 -0
- endoreg_db/import_files/file_storage/sensitive_meta_storage.py +39 -0
- endoreg_db/import_files/file_storage/state_management.py +400 -0
- endoreg_db/import_files/file_storage/storage.py +36 -0
- endoreg_db/import_files/import_service.md +26 -0
- endoreg_db/import_files/processing/__init__.py +11 -0
- endoreg_db/import_files/processing/report_processing/report_anonymization.py +94 -0
- endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
- endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
- endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +119 -0
- endoreg_db/import_files/pseudonymization/fake.py +52 -0
- endoreg_db/import_files/pseudonymization/k_anonymity.py +182 -0
- endoreg_db/import_files/pseudonymization/k_pseudonymity.py +128 -0
- endoreg_db/import_files/report_import_service.py +141 -0
- endoreg_db/import_files/video_import_service.py +150 -0
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
- endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
- endoreg_db/management/commands/import_report.py +130 -65
- endoreg_db/management/commands/import_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +2 -2
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +5 -5
- endoreg_db/management/commands/load_ai_model_label_data.py +9 -7
- endoreg_db/management/commands/load_base_db_data.py +5 -134
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_contraindication_data.py +14 -16
- endoreg_db/management/commands/load_disease_classification_choices_data.py +15 -18
- endoreg_db/management/commands/load_disease_classification_data.py +15 -18
- endoreg_db/management/commands/load_disease_data.py +25 -28
- endoreg_db/management/commands/load_endoscope_data.py +20 -27
- endoreg_db/management/commands/load_event_data.py +14 -16
- endoreg_db/management/commands/load_examination_data.py +31 -44
- endoreg_db/management/commands/load_examination_indication_data.py +20 -21
- endoreg_db/management/commands/load_finding_data.py +52 -80
- endoreg_db/management/commands/load_information_source.py +21 -23
- endoreg_db/management/commands/load_lab_value_data.py +17 -26
- endoreg_db/management/commands/load_medication_data.py +13 -12
- endoreg_db/management/commands/load_organ_data.py +15 -19
- endoreg_db/management/commands/load_pdf_type_data.py +19 -18
- endoreg_db/management/commands/load_profession_data.py +14 -17
- endoreg_db/management/commands/load_qualification_data.py +20 -23
- endoreg_db/management/commands/load_report_reader_flag_data.py +17 -19
- endoreg_db/management/commands/load_requirement_data.py +62 -39
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/load_risk_data.py +7 -6
- endoreg_db/management/commands/load_shift_data.py +20 -23
- endoreg_db/management/commands/load_tag_data.py +8 -11
- endoreg_db/management/commands/load_unit_data.py +17 -19
- endoreg_db/management/commands/setup_endoreg_db.py +3 -3
- endoreg_db/management/commands/start_filewatcher.py +46 -37
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/management/commands/validate_video_files.py +1 -5
- endoreg_db/migrations/0001_initial.py +297 -250
- endoreg_db/models/__init__.py +78 -123
- endoreg_db/models/administration/__init__.py +21 -42
- endoreg_db/models/administration/ai/active_model.py +2 -2
- endoreg_db/models/administration/ai/ai_model.py +7 -6
- endoreg_db/models/administration/case/__init__.py +1 -15
- endoreg_db/models/administration/case/case.py +3 -3
- endoreg_db/models/administration/case/case_template/__init__.py +2 -14
- endoreg_db/models/administration/case/case_template/case_template.py +2 -124
- endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
- endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
- endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
- endoreg_db/models/administration/center/center.py +33 -19
- endoreg_db/models/administration/center/center_product.py +12 -9
- endoreg_db/models/administration/center/center_resource.py +25 -19
- endoreg_db/models/administration/center/center_shift.py +21 -17
- endoreg_db/models/administration/center/center_waste.py +16 -8
- endoreg_db/models/administration/person/__init__.py +2 -0
- endoreg_db/models/administration/person/employee/employee.py +10 -5
- endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
- endoreg_db/models/administration/person/employee/employee_type.py +12 -6
- endoreg_db/models/administration/person/examiner/examiner.py +13 -11
- endoreg_db/models/administration/person/patient/__init__.py +2 -0
- endoreg_db/models/administration/person/patient/patient.py +129 -100
- endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
- endoreg_db/models/administration/person/person.py +4 -0
- endoreg_db/models/administration/person/profession/__init__.py +8 -4
- endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
- endoreg_db/models/administration/product/product.py +20 -15
- endoreg_db/models/administration/product/product_material.py +17 -18
- endoreg_db/models/administration/product/product_weight.py +12 -8
- endoreg_db/models/administration/product/reference_product.py +23 -55
- endoreg_db/models/administration/qualification/qualification.py +7 -3
- endoreg_db/models/administration/qualification/qualification_type.py +7 -3
- endoreg_db/models/administration/shift/scheduled_days.py +8 -5
- endoreg_db/models/administration/shift/shift.py +16 -12
- endoreg_db/models/administration/shift/shift_type.py +23 -31
- endoreg_db/models/label/__init__.py +8 -9
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +23 -28
- endoreg_db/models/label/label.py +15 -15
- endoreg_db/models/label/label_set.py +19 -6
- endoreg_db/models/label/label_type.py +1 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
- endoreg_db/models/label/label_video_segment/label_video_segment.py +98 -102
- endoreg_db/models/label/video_segmentation_label.py +4 -0
- endoreg_db/models/label/video_segmentation_labelset.py +4 -3
- endoreg_db/models/media/frame/frame.py +22 -22
- endoreg_db/models/media/pdf/raw_pdf.py +194 -194
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +55 -47
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/processing_history/__init__.py +5 -0
- endoreg_db/models/media/processing_history/processing_history.py +96 -0
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +139 -77
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +174 -112
- endoreg_db/models/media/video/video_file_ai.py +288 -74
- endoreg_db/models/media/video/video_file_anonymize.py +38 -38
- endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
- endoreg_db/models/media/video/video_file_io.py +113 -61
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
- endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
- endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
- endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
- endoreg_db/models/media/video/video_file_segments.py +24 -17
- endoreg_db/models/media/video/video_metadata.py +19 -35
- endoreg_db/models/media/video/video_processing.py +96 -95
- endoreg_db/models/medical/contraindication/README.md +1 -0
- endoreg_db/models/medical/contraindication/__init__.py +13 -3
- endoreg_db/models/medical/disease.py +22 -16
- endoreg_db/models/medical/event.py +31 -18
- endoreg_db/models/medical/examination/__init__.py +13 -6
- endoreg_db/models/medical/examination/examination.py +39 -20
- endoreg_db/models/medical/examination/examination_indication.py +30 -95
- endoreg_db/models/medical/examination/examination_time.py +23 -8
- endoreg_db/models/medical/examination/examination_time_type.py +9 -6
- endoreg_db/models/medical/examination/examination_type.py +3 -4
- endoreg_db/models/medical/finding/finding.py +32 -40
- endoreg_db/models/medical/finding/finding_classification.py +42 -72
- endoreg_db/models/medical/finding/finding_intervention.py +25 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +26 -26
- endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
- endoreg_db/models/medical/laboratory/lab_value.py +62 -91
- endoreg_db/models/medical/medication/medication.py +22 -10
- endoreg_db/models/medical/medication/medication_indication.py +29 -3
- endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
- endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
- endoreg_db/models/medical/medication/medication_schedule.py +27 -16
- endoreg_db/models/medical/organ/__init__.py +15 -12
- endoreg_db/models/medical/patient/medication_examples.py +6 -6
- endoreg_db/models/medical/patient/patient_disease.py +20 -23
- endoreg_db/models/medical/patient/patient_event.py +19 -22
- endoreg_db/models/medical/patient/patient_examination.py +48 -54
- endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
- endoreg_db/models/medical/patient/patient_finding.py +122 -139
- endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
- endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
- endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
- endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
- endoreg_db/models/medical/patient/patient_medication.py +27 -38
- endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
- endoreg_db/models/medical/risk/risk.py +7 -6
- endoreg_db/models/medical/risk/risk_type.py +8 -5
- endoreg_db/models/metadata/model_meta.py +60 -29
- endoreg_db/models/metadata/model_meta_logic.py +125 -18
- endoreg_db/models/metadata/pdf_meta.py +31 -24
- endoreg_db/models/metadata/sensitive_meta.py +105 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +198 -103
- endoreg_db/models/metadata/video_meta.py +51 -31
- endoreg_db/models/metadata/video_prediction_logic.py +16 -23
- endoreg_db/models/metadata/video_prediction_meta.py +29 -33
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
- endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
- endoreg_db/models/other/emission/emission_factor.py +18 -8
- endoreg_db/models/other/gender.py +10 -5
- endoreg_db/models/other/information_source.py +50 -29
- endoreg_db/models/other/material.py +9 -5
- endoreg_db/models/other/resource.py +6 -4
- endoreg_db/models/other/tag.py +10 -5
- endoreg_db/models/other/transport_route.py +13 -8
- endoreg_db/models/other/unit.py +10 -6
- endoreg_db/models/other/waste.py +6 -5
- endoreg_db/models/report/report.py +6 -0
- endoreg_db/models/requirement/requirement.py +329 -361
- endoreg_db/models/requirement/requirement_error.py +85 -0
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
- endoreg_db/models/requirement/requirement_operator.py +103 -112
- endoreg_db/models/requirement/requirement_set.py +74 -57
- endoreg_db/models/state/__init__.py +4 -4
- endoreg_db/models/state/abstract.py +2 -2
- endoreg_db/models/state/anonymization.py +12 -0
- endoreg_db/models/state/audit_ledger.py +49 -51
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +101 -68
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +110 -90
- endoreg_db/models/upload_job.py +35 -34
- endoreg_db/models/utils.py +28 -25
- endoreg_db/queries/__init__.py +3 -1
- endoreg_db/root_urls.py +21 -2
- endoreg_db/schemas/examination_evaluation.py +1 -1
- endoreg_db/serializers/__init__.py +2 -10
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/label_video_segment/label_video_segment.py +2 -29
- endoreg_db/serializers/meta/__init__.py +1 -6
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/file_overview.py +11 -99
- endoreg_db/serializers/misc/sensitive_patient_data.py +50 -26
- endoreg_db/serializers/patient_examination/patient_examination.py +3 -3
- endoreg_db/serializers/pdf/anony_text_validation.py +39 -23
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_file_list.py +65 -34
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/services/__old/pdf_import.py +1487 -0
- endoreg_db/services/__old/video_import.py +1306 -0
- endoreg_db/services/anonymization.py +128 -89
- endoreg_db/services/lookup_service.py +65 -52
- endoreg_db/services/lookup_store.py +2 -2
- endoreg_db/services/pdf_import.py +0 -1382
- endoreg_db/services/report_import.py +10 -0
- endoreg_db/services/video_import.py +6 -1255
- endoreg_db/tasks/upload_tasks.py +79 -70
- endoreg_db/tasks/video_ingest.py +8 -4
- endoreg_db/urls/__init__.py +5 -32
- endoreg_db/urls/ai.py +32 -0
- endoreg_db/urls/media.py +121 -83
- endoreg_db/urls/root_urls.py +29 -0
- endoreg_db/utils/__init__.py +15 -5
- endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
- endoreg_db/utils/case_generator/__init__.py +3 -0
- endoreg_db/utils/dataloader.py +142 -40
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/paths.py +110 -46
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/Readme.md +1 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +655 -0
- endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +97 -0
- endoreg_db/utils/setup_config.py +8 -5
- endoreg_db/utils/storage.py +115 -0
- endoreg_db/utils/validate_endo_roi.py +8 -2
- endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
- endoreg_db/views/__init__.py +85 -183
- endoreg_db/views/ai/__init__.py +8 -0
- endoreg_db/views/ai/label.py +155 -0
- endoreg_db/views/anonymization/media_management.py +202 -166
- endoreg_db/views/anonymization/overview.py +99 -67
- endoreg_db/views/anonymization/validate.py +182 -44
- endoreg_db/views/media/__init__.py +7 -20
- endoreg_db/views/media/pdf_media.py +197 -174
- endoreg_db/views/media/sensitive_metadata.py +193 -138
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/meta/__init__.py +0 -8
- endoreg_db/views/misc/__init__.py +1 -7
- endoreg_db/views/misc/upload_views.py +94 -93
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/report/__init__.py +5 -7
- endoreg_db/views/{pdf → report}/reimport.py +22 -22
- endoreg_db/views/{pdf/pdf_stream.py → report/report_stream.py} +46 -39
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +17 -3
- endoreg_db/views/requirement/lookup_store.py +22 -90
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +23 -24
- endoreg_db/views/video/correction.py +201 -172
- endoreg_db/views/video/reimport.py +1 -1
- endoreg_db/views/{media/video_segments.py → video/segments_crud.py} +77 -40
- endoreg_db/views/video/{video_meta.py → video_meta_stats.py} +2 -2
- endoreg_db/views/video/video_stream.py +7 -8
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/RECORD +391 -413
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/WHEEL +1 -1
- endoreg_db/data/finding/anatomy_colon.yaml +0 -128
- endoreg_db/data/finding/colonoscopy.yaml +0 -40
- endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +0 -56
- endoreg_db/data/finding/complication.yaml +0 -16
- endoreg_db/data/finding/data.yaml +0 -105
- endoreg_db/data/finding/examination_setting.yaml +0 -16
- endoreg_db/data/finding/medication_related.yaml +0 -18
- endoreg_db/data/finding/outcome.yaml +0 -12
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +0 -95
- endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +0 -22
- endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +0 -25
- endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +0 -68
- endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +0 -80
- endoreg_db/data/finding_classification/colonoscopy_lst.yaml +0 -21
- endoreg_db/data/finding_classification/colonoscopy_nice.yaml +0 -20
- endoreg_db/data/finding_classification/colonoscopy_paris.yaml +0 -26
- endoreg_db/data/finding_classification/colonoscopy_sano.yaml +0 -22
- endoreg_db/data/finding_classification/colonoscopy_summary.yaml +0 -53
- endoreg_db/data/finding_classification/complication_generic.yaml +0 -25
- endoreg_db/data/finding_classification/examination_setting_generic.yaml +0 -40
- endoreg_db/data/finding_classification/histology_colo.yaml +0 -51
- endoreg_db/data/finding_classification/intervention_required.yaml +0 -26
- endoreg_db/data/finding_classification/medication_related.yaml +0 -23
- endoreg_db/data/finding_classification/visualized.yaml +0 -33
- endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
- endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
- endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
- endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +0 -15
- endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +0 -17
- endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
- endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +0 -14
- endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
- endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +0 -82
- endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
- endoreg_db/data/finding_classification_choice/outcome.yaml +0 -19
- endoreg_db/data/finding_intervention/endoscopy.yaml +0 -43
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
- endoreg_db/data/finding_intervention/endoscopy_egd.yaml +0 -128
- endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +0 -32
- endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +0 -9
- endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +0 -36
- endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +0 -79
- endoreg_db/data/requirement/age.yaml +0 -26
- endoreg_db/data/requirement/gender.yaml +0 -25
- endoreg_db/management/commands/init_default_ai_model.py +0 -112
- endoreg_db/management/commands/reset_celery_schedule.py +0 -9
- endoreg_db/management/commands/validate_video.py +0 -204
- endoreg_db/migrations/0002_add_video_correction_models.py +0 -52
- endoreg_db/migrations/0003_add_center_display_name.py +0 -30
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/rule/__init__.py +0 -13
- endoreg_db/models/rule/rule.py +0 -27
- endoreg_db/models/rule/rule_applicator.py +0 -224
- endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
- endoreg_db/models/rule/rule_type.py +0 -20
- endoreg_db/models/rule/ruleset.py +0 -17
- endoreg_db/renames.yml +0 -8
- endoreg_db/serializers/_old/raw_pdf_meta_validation.py +0 -223
- endoreg_db/serializers/_old/raw_video_meta_validation.py +0 -179
- endoreg_db/serializers/_old/video.py +0 -71
- endoreg_db/serializers/meta/pdf_file_meta_extraction.py +0 -115
- endoreg_db/serializers/meta/report_meta.py +0 -53
- endoreg_db/serializers/report/__init__.py +0 -9
- endoreg_db/serializers/report/mixins.py +0 -45
- endoreg_db/serializers/report/report.py +0 -105
- endoreg_db/serializers/report/report_list.py +0 -22
- endoreg_db/serializers/report/secure_file_url.py +0 -26
- endoreg_db/serializers/video/video_metadata.py +0 -105
- endoreg_db/services/requirements_object.py +0 -147
- endoreg_db/services/storage_aware_video_processor.py +0 -344
- endoreg_db/urls/files.py +0 -6
- endoreg_db/urls/label_video_segment_validate.py +0 -33
- endoreg_db/urls/label_video_segments.py +0 -46
- endoreg_db/urls/report.py +0 -48
- endoreg_db/urls/video.py +0 -61
- endoreg_db/utils/case_generator/case_generator.py +0 -159
- endoreg_db/utils/case_generator/utils.py +0 -30
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +0 -368
- endoreg_db/views/label/__init__.py +0 -5
- endoreg_db/views/label/label.py +0 -15
- endoreg_db/views/label_video_segment/__init__.py +0 -16
- endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +0 -44
- endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +0 -50
- endoreg_db/views/label_video_segment/label_video_segment.py +0 -77
- endoreg_db/views/label_video_segment/label_video_segment_by_label.py +0 -174
- endoreg_db/views/label_video_segment/label_video_segment_detail.py +0 -73
- endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +0 -46
- endoreg_db/views/label_video_segment/validate.py +0 -226
- endoreg_db/views/media/segments.py +0 -71
- endoreg_db/views/meta/available_files_list.py +0 -146
- endoreg_db/views/meta/report_meta.py +0 -53
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -148
- endoreg_db/views/misc/secure_file_serving_view.py +0 -80
- endoreg_db/views/misc/secure_file_url_view.py +0 -84
- endoreg_db/views/misc/secure_url_validate.py +0 -79
- endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +0 -164
- endoreg_db/views/patient_finding_location/__init__.py +0 -5
- endoreg_db/views/patient_finding_location/pfl_create.py +0 -70
- endoreg_db/views/patient_finding_morphology/__init__.py +0 -5
- endoreg_db/views/patient_finding_morphology/pfm_create.py +0 -70
- endoreg_db/views/pdf/__init__.py +0 -8
- endoreg_db/views/report/report_list.py +0 -112
- endoreg_db/views/report/report_with_secure_url.py +0 -28
- endoreg_db/views/report/start_examination.py +0 -7
- endoreg_db/views/video/segmentation.py +0 -274
- endoreg_db/views/video/task_status.py +0 -49
- endoreg_db/views/video/timeline.py +0 -46
- endoreg_db/views/video/video_analyze.py +0 -52
- endoreg_db/views.py +0 -0
- /endoreg_db/data/requirement/{colonoscopy_baseline_austria.yaml → old/colonoscopy_baseline_austria.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_cardiovascular.yaml → old/disease_cardiovascular.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_classification_choice_cardiovascular.yaml → old/disease_classification_choice_cardiovascular.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_hepatology.yaml → old/disease_hepatology.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_misc.yaml → old/disease_misc.yaml} +0 -0
- /endoreg_db/data/requirement/{disease_renal.yaml → old/disease_renal.yaml} +0 -0
- /endoreg_db/data/requirement/{endoscopy_bleeding_risk.yaml → old/endoscopy_bleeding_risk.yaml} +0 -0
- /endoreg_db/data/requirement/{event_cardiology.yaml → old/event_cardiology.yaml} +0 -0
- /endoreg_db/data/requirement/{event_requirements.yaml → old/event_requirements.yaml} +0 -0
- /endoreg_db/data/requirement/{finding_colon_polyp.yaml → old/finding_colon_polyp.yaml} +0 -0
- /endoreg_db/{migrations/__init__.py → data/requirement/old/gender.yaml} +0 -0
- /endoreg_db/data/requirement/{lab_value.yaml → old/lab_value.yaml} +0 -0
- /endoreg_db/data/requirement/{medication.yaml → old/medication.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{age.yaml → _old/age.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{lab_operators.yaml → _old/lab_operators.yaml} +0 -0
- /endoreg_db/data/requirement_operator/{model_operators.yaml → _old/model_operators.yaml} +0 -0
- /endoreg_db/{models/media/video/refactor_plan.md → import_files/pseudonymization/__init__.py} +0 -0
- /endoreg_db/{models/media/video/video_file_frames.py → import_files/pseudonymization/pseudonymize.py} +0 -0
- /endoreg_db/models/{metadata/frame_ocr_result.py → report/__init__.py} +0 -0
- /endoreg_db/{urls/sensitive_meta.py → models/report/images.py} +0 -0
- /endoreg_db/utils/requirement_operator_logic/{lab_value_operators.py → _old/lab_value_operators.py} +0 -0
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/licenses/LICENSE +0 -0
endoreg_db/authz/auth.py
ADDED
|
@@ -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)
|