trueppm-api 0.4.0__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.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"""SSRF-guarded outbound HTTP helper for the integrations app (ADR-0049 §3).
|
|
2
|
+
|
|
3
|
+
Two integration surfaces make request-cycle HTTP calls to user/operator-supplied
|
|
4
|
+
hosts: PAT verification (#677, ``TaskLinkProvider.verify_token``) and git-link
|
|
5
|
+
status refresh (#637, ``fetch_status``). Both must defend against SSRF — a PAT's
|
|
6
|
+
``base_url`` or a pasted git URL could point at ``169.254.169.254`` (cloud
|
|
7
|
+
metadata), ``127.0.0.1`` (loopback), or an RFC1918 host on the cluster network.
|
|
8
|
+
|
|
9
|
+
This module is the single egress chokepoint. ``assert_url_allowed`` resolves the
|
|
10
|
+
target host and rejects any URL that resolves to a private / loopback /
|
|
11
|
+
link-local / reserved address; ``get`` validates first, then performs a
|
|
12
|
+
time-bounded, redirect-disabled GET with a capped response body. Both #677 and
|
|
13
|
+
#637 route through it so the guard is implemented and tested once.
|
|
14
|
+
|
|
15
|
+
The guard resolves the hostname and checks **every** returned address — a host
|
|
16
|
+
with one public and one private A record is rejected. That pre-flight check
|
|
17
|
+
alone is resolve-then-reconnect: the connection performs its own lookup, so a
|
|
18
|
+
low-TTL record could answer the check with a public address and the connect with
|
|
19
|
+
``169.254.169.254``. The connection layer therefore resolves the name once,
|
|
20
|
+
refuses if any answer is non-public, and dials only those checked addresses —
|
|
21
|
+
never connecting to a rebinding answer — then re-checks ``getpeername()`` as
|
|
22
|
+
defense in depth, all before TLS or any request byte
|
|
23
|
+
(:func:`build_guarded_opener`). This closes the
|
|
24
|
+
DNS-rebinding window without rewriting the URL (the hostname — and so SNI and
|
|
25
|
+
certificate verification — is untouched). Every urllib-based egress caller must
|
|
26
|
+
use that opener; webhook delivery shares it.
|
|
27
|
+
|
|
28
|
+
**Proxies.** The opener honors ``HTTP_PROXY`` / ``HTTPS_PROXY`` / ``NO_PROXY``
|
|
29
|
+
through urllib's normal ``ProxyHandler``. A request that goes **direct** gets the
|
|
30
|
+
connect-time peer check. A request that goes **through a proxy** gets only the
|
|
31
|
+
resolve-time pre-flight (:func:`assert_url_allowed`, advisory): the connected
|
|
32
|
+
peer is the proxy, which does the resolving and is the real egress control, so
|
|
33
|
+
checking it would be meaningless (and would reject a private proxy address). A
|
|
34
|
+
one-time WARNING is logged when a proxied request is first made.
|
|
35
|
+
|
|
36
|
+
**Operator allow-list (ADR-0590).** A self-hoster who runs an identity provider
|
|
37
|
+
*inside the same cluster* has an OIDC issuer that resolves to a private ClusterIP
|
|
38
|
+
(e.g. ``keycloak.sso.svc``); the deny-list above would block discovery / token /
|
|
39
|
+
JWKS and make in-cluster SSO impossible. ``EGRESS_ALLOWLISTED_HOSTS`` (default
|
|
40
|
+
empty) names the exact hostnames the operator trusts, which then bypass the
|
|
41
|
+
private-address check. It is **operator configuration only** — never derived from
|
|
42
|
+
request or user input — and matches on an **exact, case-insensitive hostname**
|
|
43
|
+
(no wildcard / suffix), so allow-listing ``keycloak`` cannot admit
|
|
44
|
+
``keycloak.attacker.example``. Empty by default keeps every existing install on
|
|
45
|
+
the original "every resolved address must be globally routable" posture. Note the
|
|
46
|
+
allow-list is honored by **every** caller of this chokepoint (PAT verification,
|
|
47
|
+
git-link refresh, webhooks, SMTP relay, SSO), so an allow-listed host is reachable
|
|
48
|
+
through the user-influenceable URL surfaces too — an accepted, operator-declared
|
|
49
|
+
trust, documented for operators alongside the setting.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import http.client
|
|
55
|
+
import ipaddress
|
|
56
|
+
import json
|
|
57
|
+
import logging
|
|
58
|
+
import socket
|
|
59
|
+
import urllib.error
|
|
60
|
+
import urllib.parse
|
|
61
|
+
import urllib.request
|
|
62
|
+
from dataclasses import dataclass
|
|
63
|
+
from typing import Any
|
|
64
|
+
from urllib.parse import urlparse
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
# Synchronous, request-cycle calls — the user is staring at a spinner. Short
|
|
69
|
+
# by design; 5s is the ADR-0049 §3 budget for the refresh endpoint.
|
|
70
|
+
DEFAULT_TIMEOUT = 5.0
|
|
71
|
+
|
|
72
|
+
# Cap the response body we buffer. Provider ``/user`` and PR/issue payloads are
|
|
73
|
+
# a few KB; anything larger is either hostile or not what we asked for. Reading
|
|
74
|
+
# a bounded amount prevents a malicious endpoint from exhausting memory.
|
|
75
|
+
_MAX_BODY_BYTES = 256 * 1024
|
|
76
|
+
|
|
77
|
+
_USER_AGENT = "TruePPM-Integrations/1.0"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EgressBlocked(Exception):
|
|
81
|
+
"""The URL failed the SSRF guard (bad scheme, missing host, or the host
|
|
82
|
+
resolved to a private / loopback / link-local / reserved address)."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EgressTimeout(Exception):
|
|
86
|
+
"""The request exceeded :data:`DEFAULT_TIMEOUT` (or the caller's timeout)."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EgressError(Exception):
|
|
90
|
+
"""A transport-level failure — DNS resolution failed, the connection was
|
|
91
|
+
refused, or the socket errored. Distinct from an HTTP error *response*,
|
|
92
|
+
which is returned as an :class:`EgressResponse` so callers can branch on
|
|
93
|
+
the status code (a 401 from ``/user`` means "bad token", not "unreachable").
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class EgressResponse:
|
|
99
|
+
"""A buffered HTTP response from a guarded GET.
|
|
100
|
+
|
|
101
|
+
Non-2xx responses are returned, not raised — ``verify_token`` needs to read
|
|
102
|
+
a 401/403 to distinguish an invalid token from an unreachable host.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
status: int
|
|
106
|
+
body: bytes
|
|
107
|
+
headers: dict[str, str]
|
|
108
|
+
|
|
109
|
+
def json(self) -> Any:
|
|
110
|
+
"""Parse the body as JSON, or return ``None`` if it is not valid JSON.
|
|
111
|
+
|
|
112
|
+
Providers occasionally answer a 200 with an HTML error page (e.g. a
|
|
113
|
+
login redirect landing). Returning ``None`` lets the caller treat an
|
|
114
|
+
unparseable 200 as "verified but no metadata" rather than 500ing.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
return json.loads(self.body.decode("utf-8"))
|
|
118
|
+
except (ValueError, UnicodeDecodeError):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# RFC6052 well-known NAT64 prefix. ``64:ff9b::/96`` embeds an IPv4 address in
|
|
123
|
+
# its low 32 bits; a NAT64 gateway (common on dual-stack k8s) translates it back
|
|
124
|
+
# to that IPv4 on the wire. Python reports the wrapper as ``is_global`` True, so
|
|
125
|
+
# without unwrapping it a user-supplied ``[64:ff9b::a9fe:a9fe]`` URL would reach
|
|
126
|
+
# cloud metadata (169.254.169.254) straight through the guard (#900).
|
|
127
|
+
_NAT64_WELLKNOWN = ipaddress.ip_network("64:ff9b::/96")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _embedded_ipv4(ip: ipaddress.IPv6Address) -> ipaddress.IPv4Address | None:
|
|
131
|
+
"""Extract the IPv4 address tunneled inside a transition-format IPv6 address.
|
|
132
|
+
|
|
133
|
+
Covers the forms whose ``is_global`` reflects the *wrapper* rather than the
|
|
134
|
+
embedded target, so a private/metadata IPv4 can be smuggled past the guard:
|
|
135
|
+
IPv4-mapped (``::ffff:0:0/96``), 6to4 (``2002::/16``), Teredo, the RFC6052
|
|
136
|
+
NAT64 well-known prefix, and the deprecated IPv4-compatible form
|
|
137
|
+
(``::a.b.c.d``, ``::/96``). Returns ``None`` for a native IPv6 address.
|
|
138
|
+
"""
|
|
139
|
+
if ip.ipv4_mapped is not None:
|
|
140
|
+
return ip.ipv4_mapped
|
|
141
|
+
if ip.sixtofour is not None:
|
|
142
|
+
return ip.sixtofour
|
|
143
|
+
if ip.teredo is not None:
|
|
144
|
+
return ip.teredo[1]
|
|
145
|
+
if ip in _NAT64_WELLKNOWN:
|
|
146
|
+
return ipaddress.IPv4Address(int(ip) & 0xFFFFFFFF)
|
|
147
|
+
# IPv4-compatible (deprecated, RFC4291 ``::/96``). No stdlib accessor: the
|
|
148
|
+
# high 96 bits are zero and the low 32 are the embedded IPv4. ``is_global``
|
|
149
|
+
# is True on the wrapper, so ``[::169.254.169.254]`` would otherwise reach
|
|
150
|
+
# metadata. Exclude ``::`` (unspecified) and ``::1`` (loopback) — those are
|
|
151
|
+
# native IPv6 that ``is_global`` already blocks, not tunneled IPv4.
|
|
152
|
+
as_int = int(ip)
|
|
153
|
+
if as_int >> 32 == 0 and as_int > 1:
|
|
154
|
+
return ipaddress.IPv4Address(as_int & 0xFFFFFFFF)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
159
|
+
"""Return ``True`` for any address an outbound integration call must not reach.
|
|
160
|
+
|
|
161
|
+
``is_global`` is ``False`` for the entire special-use space — private
|
|
162
|
+
(RFC1918), loopback, link-local (incl. ``169.254.169.254`` cloud metadata),
|
|
163
|
+
reserved, multicast, and unspecified — so the single negated check covers
|
|
164
|
+
every SSRF target the ADR-0049 deny-list enumerates without maintaining a
|
|
165
|
+
hand-rolled CIDR table that drifts from the IANA registry.
|
|
166
|
+
|
|
167
|
+
IPv6 transition formats are unwrapped first: ``is_global`` is computed on the
|
|
168
|
+
wrapper, so a metadata/RFC1918 IPv4 tunneled via NAT64, 6to4, Teredo, or an
|
|
169
|
+
IPv4-mapped address would otherwise pass. The embedded IPv4 is re-checked
|
|
170
|
+
through this same guard (#900).
|
|
171
|
+
"""
|
|
172
|
+
if isinstance(ip, ipaddress.IPv6Address):
|
|
173
|
+
embedded = _embedded_ipv4(ip)
|
|
174
|
+
if embedded is not None:
|
|
175
|
+
return _is_blocked_ip(embedded)
|
|
176
|
+
return not ip.is_global
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _allowlisted_hosts() -> frozenset[str]:
|
|
180
|
+
"""Operator-trusted hostnames exempt from the private-address deny-list (ADR-0590).
|
|
181
|
+
|
|
182
|
+
Read from the ``EGRESS_ALLOWLISTED_HOSTS`` setting (default empty). This is an
|
|
183
|
+
operator escape hatch for a trusted internal host the operator runs
|
|
184
|
+
themselves — chiefly an in-cluster identity provider whose OIDC issuer resolves
|
|
185
|
+
to a private ClusterIP, which the deny-list would otherwise block.
|
|
186
|
+
|
|
187
|
+
Safety: the value is **configuration only** — never request/user-derived — so
|
|
188
|
+
it cannot be widened by an attacker. Membership is an **exact, case-insensitive
|
|
189
|
+
hostname** compare, so an allow-listed ``keycloak`` never admits a lookalike
|
|
190
|
+
like ``keycloak.attacker.example``. ``django.conf`` is imported lazily to keep
|
|
191
|
+
this module importable without Django configured (it is otherwise pure stdlib).
|
|
192
|
+
"""
|
|
193
|
+
from django.conf import settings
|
|
194
|
+
|
|
195
|
+
hosts = getattr(settings, "EGRESS_ALLOWLISTED_HOSTS", ()) or ()
|
|
196
|
+
return frozenset(h.strip().lower() for h in hosts if h and h.strip())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _blocked_if_literal(host: str) -> bool | None:
|
|
200
|
+
"""Return the deny-decision for ``host`` if it is an IP literal, else ``None``.
|
|
201
|
+
|
|
202
|
+
A literal IP needs no DNS. Classifying it directly avoids a pointless
|
|
203
|
+
``getaddrinfo`` round-trip on every IP-literal target and — critically —
|
|
204
|
+
removes the only resolver dependency from the guard's rejection of a literal
|
|
205
|
+
SSRF target (``169.254.169.254`` cloud metadata, an RFC1918 host). That deny
|
|
206
|
+
becomes a pure, syscall-free computation, so a constrained network namespace
|
|
207
|
+
(e.g. a CI runner) can never turn it into a transient ``EgressError`` /
|
|
208
|
+
resolver failure and let the caller retry into a real connection (#1628).
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
ip = ipaddress.ip_address(host)
|
|
212
|
+
except ValueError:
|
|
213
|
+
return None
|
|
214
|
+
return _is_blocked_ip(ip)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
218
|
+
"""Disable redirect following so a 3xx can't bounce the request to an
|
|
219
|
+
internal host that the original (validated) URL did not resolve to.
|
|
220
|
+
|
|
221
|
+
Returning ``None`` from ``redirect_request`` tells urllib not to follow the
|
|
222
|
+
redirect; the 3xx response is surfaced to the caller as-is.
|
|
223
|
+
|
|
224
|
+
Public so sibling SSRF-guarded callers (webhook delivery, anywhere we POST
|
|
225
|
+
to a user-supplied URL) can compose their own opener without re-deriving
|
|
226
|
+
the handler. The original underscore name is kept as an alias for
|
|
227
|
+
backwards-compatibility inside this module.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _peer_ip(sock: socket.socket) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
|
235
|
+
"""Return the connected peer's IP, or ``None`` if the socket has no peer."""
|
|
236
|
+
try:
|
|
237
|
+
addr = sock.getpeername()[0]
|
|
238
|
+
except OSError:
|
|
239
|
+
return None
|
|
240
|
+
# Strip an IPv6 zone id (``fe80::1%eth0``) that ip_address() rejects.
|
|
241
|
+
try:
|
|
242
|
+
return ipaddress.ip_address(str(addr).split("%", 1)[0])
|
|
243
|
+
except ValueError:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
_DEFAULT_SOCKET_TIMEOUT = socket.getdefaulttimeout()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _assert_addrinfos_public(host: str, infos: list[Any]) -> None:
|
|
251
|
+
"""Refuse if ANY resolved address is non-public (same rule as the pre-flight)."""
|
|
252
|
+
for info in infos:
|
|
253
|
+
addr = str(info[4][0]).split("%", 1)[0]
|
|
254
|
+
try:
|
|
255
|
+
ip = ipaddress.ip_address(addr)
|
|
256
|
+
except ValueError:
|
|
257
|
+
raise EgressBlocked(f"unparseable address {addr!r}") from None
|
|
258
|
+
if _is_blocked_ip(ip):
|
|
259
|
+
raise EgressBlocked(f"host {host!r} resolves to non-public address {addr}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _guarded_create_connection(
|
|
263
|
+
address: tuple[str, int],
|
|
264
|
+
timeout: Any = _DEFAULT_SOCKET_TIMEOUT,
|
|
265
|
+
source_address: tuple[str, int] | None = None,
|
|
266
|
+
) -> socket.socket:
|
|
267
|
+
"""``socket.create_connection`` that only ever dials checked addresses (#3849).
|
|
268
|
+
|
|
269
|
+
Resolves the name ONCE, refuses if any returned address is non-public, then
|
|
270
|
+
connects to those already-checked sockaddrs with the stdlib try-each-address
|
|
271
|
+
loop. The pre-flight :func:`assert_url_allowed` and a plain connect each
|
|
272
|
+
resolve independently, so a rebinding answer must never be dialed: refusing
|
|
273
|
+
before ``connect`` also means an open and a closed internal port are
|
|
274
|
+
indistinguishable (both a permanent ``EgressBlocked``, no handshake). The
|
|
275
|
+
``getpeername()`` check after the handshake stays as defense in depth. Runs
|
|
276
|
+
before the TLS wrap and any request byte; the hostname is untouched so SNI and
|
|
277
|
+
certificate verification are unchanged. An operator-allow-listed host
|
|
278
|
+
(ADR-0590, exact match) skips the deny-list, as in :func:`assert_url_allowed`.
|
|
279
|
+
"""
|
|
280
|
+
host, port = address[0], address[1]
|
|
281
|
+
allowlisted = host.lower().strip("[]") in _allowlisted_hosts()
|
|
282
|
+
infos = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
|
|
283
|
+
if not infos:
|
|
284
|
+
raise OSError("getaddrinfo returns an empty list")
|
|
285
|
+
if not allowlisted:
|
|
286
|
+
_assert_addrinfos_public(host, infos)
|
|
287
|
+
err: OSError | None = None
|
|
288
|
+
for af, socktype, proto, _canon, sa in infos:
|
|
289
|
+
sock: socket.socket | None = None
|
|
290
|
+
try:
|
|
291
|
+
sock = socket.socket(af, socktype, proto)
|
|
292
|
+
if timeout is not _DEFAULT_SOCKET_TIMEOUT:
|
|
293
|
+
sock.settimeout(timeout)
|
|
294
|
+
if source_address:
|
|
295
|
+
sock.bind(source_address)
|
|
296
|
+
sock.connect(sa)
|
|
297
|
+
except OSError as exc:
|
|
298
|
+
err = exc
|
|
299
|
+
if sock is not None:
|
|
300
|
+
sock.close()
|
|
301
|
+
continue
|
|
302
|
+
if not allowlisted:
|
|
303
|
+
ip = _peer_ip(sock)
|
|
304
|
+
if ip is None or _is_blocked_ip(ip):
|
|
305
|
+
sock.close()
|
|
306
|
+
raise EgressBlocked(f"host {host!r} connected to a non-public address")
|
|
307
|
+
return sock
|
|
308
|
+
assert err is not None
|
|
309
|
+
raise err
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class _GuardedHTTPConnection(http.client.HTTPConnection):
|
|
313
|
+
"""HTTP connection whose connected peer is deny-list checked (#3849)."""
|
|
314
|
+
|
|
315
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
316
|
+
super().__init__(*args, **kwargs)
|
|
317
|
+
self._create_connection = _guarded_create_connection
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class _GuardedHTTPSConnection(http.client.HTTPSConnection):
|
|
321
|
+
"""HTTPS connection whose peer is checked before the TLS handshake.
|
|
322
|
+
|
|
323
|
+
``HTTPSConnection.connect`` calls the parent ``connect`` (which uses
|
|
324
|
+
``_create_connection``) and then wraps with ``server_hostname=self.host``, so
|
|
325
|
+
SNI and certificate hostname verification keep using the original name.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
329
|
+
super().__init__(*args, **kwargs)
|
|
330
|
+
self._create_connection = _guarded_create_connection
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
_proxy_warned = False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _is_proxied(req: urllib.request.Request) -> bool:
|
|
337
|
+
"""True if urllib's ProxyHandler rerouted ``req`` through a proxy."""
|
|
338
|
+
return req.has_proxy() or getattr(req, "_tunnel_host", None) is not None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _note_proxied() -> None:
|
|
342
|
+
global _proxy_warned
|
|
343
|
+
if not _proxy_warned:
|
|
344
|
+
_proxy_warned = True
|
|
345
|
+
logger.warning(
|
|
346
|
+
"Outbound request is going through a proxy (HTTP_PROXY/HTTPS_PROXY): the "
|
|
347
|
+
"connected-IP SSRF guard does not apply behind a proxy; only the resolve-time "
|
|
348
|
+
"pre-flight does. The proxy must enforce egress policy."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class _GuardedHTTPHandler(urllib.request.HTTPHandler):
|
|
353
|
+
def http_open(self, req: urllib.request.Request) -> Any:
|
|
354
|
+
if _is_proxied(req):
|
|
355
|
+
_note_proxied()
|
|
356
|
+
return self.do_open(http.client.HTTPConnection, req)
|
|
357
|
+
return self.do_open(_GuardedHTTPConnection, req)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class _GuardedHTTPSHandler(urllib.request.HTTPSHandler):
|
|
361
|
+
def https_open(self, req: urllib.request.Request) -> Any:
|
|
362
|
+
ctx = self._context # type: ignore[attr-defined]
|
|
363
|
+
if _is_proxied(req):
|
|
364
|
+
_note_proxied()
|
|
365
|
+
return self.do_open(http.client.HTTPSConnection, req, context=ctx)
|
|
366
|
+
return self.do_open(_GuardedHTTPSConnection, req, context=ctx)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def build_guarded_opener() -> urllib.request.OpenerDirector:
|
|
370
|
+
"""Build the one opener every user-controlled-URL egress call must use.
|
|
371
|
+
|
|
372
|
+
Redirects disabled; env proxies honored (see module docstring); direct
|
|
373
|
+
connections have their connected peer re-checked at connect time. A refused
|
|
374
|
+
peer raises :class:`EgressBlocked` out of ``open`` *unwrapped* — it is not an
|
|
375
|
+
``OSError``, so urllib's ``do_open`` does not convert it to ``URLError``.
|
|
376
|
+
Proxy settings are read from the environment when this is called.
|
|
377
|
+
"""
|
|
378
|
+
return urllib.request.build_opener(
|
|
379
|
+
urllib.request.ProxyHandler(),
|
|
380
|
+
NoRedirectHandler,
|
|
381
|
+
_GuardedHTTPHandler,
|
|
382
|
+
_GuardedHTTPSHandler,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def blocked_reason(exc: BaseException) -> EgressBlocked | None:
|
|
387
|
+
"""Return ``exc`` if it is an :class:`EgressBlocked`, or one wrapped as ``.reason``.
|
|
388
|
+
|
|
389
|
+
Defensive: the guarded connection's refusal normally propagates unwrapped,
|
|
390
|
+
but a caller may hold a ``URLError`` whose ``reason`` carries it.
|
|
391
|
+
"""
|
|
392
|
+
if isinstance(exc, EgressBlocked):
|
|
393
|
+
return exc
|
|
394
|
+
reason = getattr(exc, "reason", None)
|
|
395
|
+
return reason if isinstance(reason, EgressBlocked) else None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
_opener = build_guarded_opener()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def assert_url_allowed(url: str) -> None:
|
|
402
|
+
"""Raise :class:`EgressBlocked` / :class:`EgressError` if ``url`` is unsafe to fetch.
|
|
403
|
+
|
|
404
|
+
Validates the scheme is ``http``/``https``, the host is present, and that
|
|
405
|
+
**every** address the host resolves to is globally routable. Shared by
|
|
406
|
+
#677 PAT verification and #637 git-link refresh so the SSRF policy has one
|
|
407
|
+
implementation and one test surface.
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
EgressBlocked: Bad scheme, missing host, or a resolved address is
|
|
411
|
+
private / loopback / link-local / reserved.
|
|
412
|
+
EgressError: The host could not be resolved (DNS failure).
|
|
413
|
+
"""
|
|
414
|
+
parsed = urlparse(url)
|
|
415
|
+
if parsed.scheme not in ("http", "https"):
|
|
416
|
+
raise EgressBlocked(f"scheme {parsed.scheme!r} is not allowed")
|
|
417
|
+
host = parsed.hostname
|
|
418
|
+
if not host:
|
|
419
|
+
raise EgressBlocked("URL has no host")
|
|
420
|
+
# Operator escape hatch (ADR-0590): a hostname the operator explicitly trusts
|
|
421
|
+
# (e.g. an in-cluster IdP on a private ClusterIP) bypasses the private-address
|
|
422
|
+
# deny-list. Config-only, exact-match — the scheme gate above still applies.
|
|
423
|
+
if host.lower() in _allowlisted_hosts():
|
|
424
|
+
return
|
|
425
|
+
# A bracketed/parseable literal IP still has to clear the deny-list — a
|
|
426
|
+
# user can paste http://169.254.169.254 directly — but needs no resolution.
|
|
427
|
+
literal = _blocked_if_literal(host)
|
|
428
|
+
if literal is not None:
|
|
429
|
+
if literal:
|
|
430
|
+
raise EgressBlocked(f"host {host!r} is a non-public address")
|
|
431
|
+
return
|
|
432
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
433
|
+
try:
|
|
434
|
+
infos = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
|
|
435
|
+
except socket.gaierror as exc:
|
|
436
|
+
raise EgressError(f"could not resolve host {host!r}") from exc
|
|
437
|
+
for info in infos:
|
|
438
|
+
addr = info[4][0]
|
|
439
|
+
try:
|
|
440
|
+
ip = ipaddress.ip_address(addr)
|
|
441
|
+
except ValueError: # pragma: no cover — getaddrinfo returns valid IPs
|
|
442
|
+
raise EgressBlocked(f"unparseable address {addr!r}") from None
|
|
443
|
+
if _is_blocked_ip(ip):
|
|
444
|
+
raise EgressBlocked(f"host {host!r} resolves to non-public address {addr}")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def assert_host_allowed(host: str, port: int) -> None:
|
|
448
|
+
"""Raise :class:`EgressBlocked` / :class:`EgressError` if ``host`` is unsafe.
|
|
449
|
+
|
|
450
|
+
Scheme-free sibling of :func:`assert_url_allowed` for non-HTTP egress —
|
|
451
|
+
e.g. an SMTP relay host (#712). It runs the same deny-list check (every
|
|
452
|
+
resolved address must be globally routable) but without the ``http``/
|
|
453
|
+
``https`` scheme gate, which would reject an SMTP target outright. Reused by
|
|
454
|
+
the workspace-SMTP serializer (validate-at-save) *and* the connection
|
|
455
|
+
resolver (re-validate at send, which narrows but does not close the
|
|
456
|
+
DNS-rebinding window — the SMTP residual is tracked in #4647; ADR-0213 §4).
|
|
457
|
+
|
|
458
|
+
Raises:
|
|
459
|
+
EgressBlocked: Missing host or a resolved address is private /
|
|
460
|
+
loopback / link-local / reserved (SSRF to metadata / internal svc).
|
|
461
|
+
EgressError: The host could not be resolved (DNS failure).
|
|
462
|
+
"""
|
|
463
|
+
if not host:
|
|
464
|
+
raise EgressBlocked("SMTP host is empty")
|
|
465
|
+
# Operator escape hatch (ADR-0590), shared with assert_url_allowed: an
|
|
466
|
+
# explicitly trusted host (e.g. an in-cluster relay) bypasses the deny-list.
|
|
467
|
+
if host.lower() in _allowlisted_hosts():
|
|
468
|
+
return
|
|
469
|
+
# A literal IP SMTP relay needs no resolution — classify it directly (#1628).
|
|
470
|
+
literal = _blocked_if_literal(host)
|
|
471
|
+
if literal is not None:
|
|
472
|
+
if literal:
|
|
473
|
+
raise EgressBlocked(f"host {host!r} is a non-public address")
|
|
474
|
+
return
|
|
475
|
+
try:
|
|
476
|
+
infos = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
|
|
477
|
+
except socket.gaierror as exc:
|
|
478
|
+
raise EgressError(f"could not resolve host {host!r}") from exc
|
|
479
|
+
for info in infos:
|
|
480
|
+
addr = info[4][0]
|
|
481
|
+
try:
|
|
482
|
+
ip = ipaddress.ip_address(addr)
|
|
483
|
+
except ValueError: # pragma: no cover — getaddrinfo returns valid IPs
|
|
484
|
+
raise EgressBlocked(f"unparseable address {addr!r}") from None
|
|
485
|
+
if _is_blocked_ip(ip):
|
|
486
|
+
raise EgressBlocked(f"host {host!r} resolves to non-public address {addr}")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def get(
|
|
490
|
+
url: str,
|
|
491
|
+
*,
|
|
492
|
+
headers: dict[str, str] | None = None,
|
|
493
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
494
|
+
) -> EgressResponse:
|
|
495
|
+
"""Perform an SSRF-guarded, redirect-disabled, time-bounded GET.
|
|
496
|
+
|
|
497
|
+
The URL is validated by :func:`assert_url_allowed` before any socket is
|
|
498
|
+
opened. Non-2xx responses are returned (not raised) so callers can read the
|
|
499
|
+
status code; only transport failures raise.
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
EgressBlocked: The URL failed the SSRF guard.
|
|
503
|
+
EgressTimeout: The request exceeded ``timeout``.
|
|
504
|
+
EgressError: DNS / connection / socket failure.
|
|
505
|
+
"""
|
|
506
|
+
assert_url_allowed(url)
|
|
507
|
+
request_headers = {"User-Agent": _USER_AGENT, **(headers or {})}
|
|
508
|
+
req = urllib.request.Request(url, headers=request_headers, method="GET")
|
|
509
|
+
try:
|
|
510
|
+
# URL is SSRF-validated by assert_url_allowed above; redirects are
|
|
511
|
+
# disabled via the _opener (NoRedirectHandler) so a 3xx can't escape the guard.
|
|
512
|
+
with _opener.open(req, timeout=timeout) as resp: # nosec B310
|
|
513
|
+
body = resp.read(_MAX_BODY_BYTES)
|
|
514
|
+
resp_headers = {k.lower(): v for k, v in resp.headers.items()}
|
|
515
|
+
return EgressResponse(status=resp.status, body=body, headers=resp_headers)
|
|
516
|
+
except urllib.error.HTTPError as exc:
|
|
517
|
+
# An HTTP error response is still a response — return it so the caller
|
|
518
|
+
# can map 401/403 → invalid token and 5xx → unreachable.
|
|
519
|
+
try:
|
|
520
|
+
body = exc.read(_MAX_BODY_BYTES)
|
|
521
|
+
except (AttributeError, OSError, ValueError): # no body / closed fp
|
|
522
|
+
body = b""
|
|
523
|
+
resp_headers = {k.lower(): v for k, v in (exc.headers or {}).items()}
|
|
524
|
+
return EgressResponse(status=exc.code, body=body, headers=resp_headers)
|
|
525
|
+
except TimeoutError as exc:
|
|
526
|
+
raise EgressTimeout(f"request to {url!r} timed out after {timeout}s") from exc
|
|
527
|
+
except urllib.error.URLError as exc:
|
|
528
|
+
blocked = blocked_reason(exc)
|
|
529
|
+
if blocked is not None:
|
|
530
|
+
raise blocked from exc
|
|
531
|
+
# URLError wraps socket.timeout on some platforms.
|
|
532
|
+
if isinstance(exc.reason, (TimeoutError, socket.timeout)):
|
|
533
|
+
raise EgressTimeout(f"request to {url!r} timed out after {timeout}s") from exc
|
|
534
|
+
raise EgressError(f"request to {url!r} failed: {exc.reason}") from exc
|
|
535
|
+
except EgressBlocked:
|
|
536
|
+
raise
|
|
537
|
+
except OSError as exc:
|
|
538
|
+
raise EgressError(f"request to {url!r} failed: {exc}") from exc
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def post_form(
|
|
542
|
+
url: str,
|
|
543
|
+
*,
|
|
544
|
+
data: dict[str, str],
|
|
545
|
+
headers: dict[str, str] | None = None,
|
|
546
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
547
|
+
) -> EgressResponse:
|
|
548
|
+
"""Perform an SSRF-guarded, redirect-disabled, time-bounded form POST.
|
|
549
|
+
|
|
550
|
+
Same egress policy as :func:`get` — the URL is validated by
|
|
551
|
+
:func:`assert_url_allowed` before any socket is opened, redirects are
|
|
552
|
+
disabled (a 3xx can't bounce the request to an internal host), the response
|
|
553
|
+
body is capped, and non-2xx responses are returned (not raised) so callers
|
|
554
|
+
can read an OAuth error body. ``data`` is ``application/x-www-form-urlencoded``
|
|
555
|
+
encoded — the OAuth 2.0 token endpoint shape (ADR-0187 SSO token exchange).
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
EgressBlocked: The URL failed the SSRF guard.
|
|
559
|
+
EgressTimeout: The request exceeded ``timeout``.
|
|
560
|
+
EgressError: DNS / connection / socket failure.
|
|
561
|
+
"""
|
|
562
|
+
assert_url_allowed(url)
|
|
563
|
+
body = urllib.parse.urlencode(data).encode("ascii")
|
|
564
|
+
request_headers = {
|
|
565
|
+
"User-Agent": _USER_AGENT,
|
|
566
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
567
|
+
"Accept": "application/json",
|
|
568
|
+
**(headers or {}),
|
|
569
|
+
}
|
|
570
|
+
req = urllib.request.Request(url, data=body, headers=request_headers, method="POST")
|
|
571
|
+
try:
|
|
572
|
+
# URL is SSRF-validated by assert_url_allowed above; redirects are
|
|
573
|
+
# disabled via the _opener (NoRedirectHandler) so a 3xx can't escape the guard.
|
|
574
|
+
with _opener.open(req, timeout=timeout) as resp: # nosec B310
|
|
575
|
+
resp_body = resp.read(_MAX_BODY_BYTES)
|
|
576
|
+
resp_headers = {k.lower(): v for k, v in resp.headers.items()}
|
|
577
|
+
return EgressResponse(status=resp.status, body=resp_body, headers=resp_headers)
|
|
578
|
+
except urllib.error.HTTPError as exc:
|
|
579
|
+
try:
|
|
580
|
+
resp_body = exc.read(_MAX_BODY_BYTES)
|
|
581
|
+
except (AttributeError, OSError, ValueError):
|
|
582
|
+
resp_body = b""
|
|
583
|
+
resp_headers = {k.lower(): v for k, v in (exc.headers or {}).items()}
|
|
584
|
+
return EgressResponse(status=exc.code, body=resp_body, headers=resp_headers)
|
|
585
|
+
except TimeoutError as exc:
|
|
586
|
+
raise EgressTimeout(f"request to {url!r} timed out after {timeout}s") from exc
|
|
587
|
+
except urllib.error.URLError as exc:
|
|
588
|
+
blocked = blocked_reason(exc)
|
|
589
|
+
if blocked is not None:
|
|
590
|
+
raise blocked from exc
|
|
591
|
+
if isinstance(exc.reason, (TimeoutError, socket.timeout)):
|
|
592
|
+
raise EgressTimeout(f"request to {url!r} timed out after {timeout}s") from exc
|
|
593
|
+
raise EgressError(f"request to {url!r} failed: {exc.reason}") from exc
|
|
594
|
+
except EgressBlocked:
|
|
595
|
+
raise
|
|
596
|
+
except OSError as exc:
|
|
597
|
+
raise EgressError(f"request to {url!r} failed: {exc}") from exc
|