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,514 @@
|
|
|
1
|
+
"""Probe-exempt default throttles for the general API rate limit (#1080).
|
|
2
|
+
|
|
3
|
+
TruePPM installs a global ``DEFAULT_THROTTLE_CLASSES`` so every endpoint that
|
|
4
|
+
does not declare its own scoped throttle still gets a baseline anon/user rate
|
|
5
|
+
limit (bounded DoS / resource-starvation protection for self-hosters). The one
|
|
6
|
+
hazard a *global* throttle introduces is that it would also count the
|
|
7
|
+
unauthenticated Kubernetes probe endpoints — ``/api/v1/health/`` (liveness) and
|
|
8
|
+
``/api/v1/edition/`` (the shell's startup edition read) — which orchestrators
|
|
9
|
+
hit on a tight loop. If those requests consumed the shared anon bucket, a busy
|
|
10
|
+
readiness loop could 429 the liveness probe and cause Kubernetes to restart a
|
|
11
|
+
perfectly healthy pod.
|
|
12
|
+
|
|
13
|
+
This is exactly why ``settings/base.py`` historically shipped *scoped throttles
|
|
14
|
+
only* and deliberately avoided a bare ``AnonRateThrottle``. The classes below
|
|
15
|
+
resolve that tension: they are the standard DRF anon/user throttles with a
|
|
16
|
+
single override — ``get_cache_key`` returns ``None`` (which DRF treats as "do
|
|
17
|
+
not throttle this request") for the probe paths — so the global default can be
|
|
18
|
+
turned on without ever rate-limiting a k8s probe.
|
|
19
|
+
|
|
20
|
+
``/api/v1/readyz`` is deliberately **not** in that exempt set (#2820, #3346).
|
|
21
|
+
Unlike ``/health/`` and ``/edition/`` it is not free — every call does a real
|
|
22
|
+
database and cache round-trip — so a full exemption meant an unauthenticated
|
|
23
|
+
caller who reached the pod IP directly (the probe path bypasses the Ingress)
|
|
24
|
+
could drive unbounded database load with no rate limit at all. It gets its own
|
|
25
|
+
scoped throttle instead (``ReadyzRateThrottle`` below), generous enough that no
|
|
26
|
+
documented probe configuration comes close to tripping it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
import ipaddress
|
|
33
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
34
|
+
|
|
35
|
+
from django.http import HttpRequest
|
|
36
|
+
from rest_framework.request import Request
|
|
37
|
+
from rest_framework.settings import api_settings
|
|
38
|
+
from rest_framework.throttling import (
|
|
39
|
+
AnonRateThrottle,
|
|
40
|
+
BaseThrottle,
|
|
41
|
+
SimpleRateThrottle,
|
|
42
|
+
UserRateThrottle,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
# Imported for type hints only. A runtime ``from rest_framework.views import
|
|
47
|
+
# APIView`` would be circular: DRF's ``APIView`` class body eagerly resolves
|
|
48
|
+
# ``DEFAULT_THROTTLE_CLASSES`` (→ this module) while ``rest_framework.views``
|
|
49
|
+
# is still initializing, before ``APIView`` itself is bound. ``from __future__
|
|
50
|
+
# import annotations`` keeps the annotations below as strings, so this import
|
|
51
|
+
# is never needed at runtime.
|
|
52
|
+
from rest_framework.views import APIView
|
|
53
|
+
|
|
54
|
+
# Kubernetes liveness and edition-discovery probes. Free to answer (no DB/cache
|
|
55
|
+
# round-trip) and hit on a tight orchestrator loop, so the general default
|
|
56
|
+
# throttle must never count them (returning None from get_cache_key skips
|
|
57
|
+
# throttling for the request). Stored trailing-slash-normalized so the check is
|
|
58
|
+
# robust whether or not the request path carries the trailing slash.
|
|
59
|
+
#
|
|
60
|
+
# ``/api/v1/readyz`` is NOT here — see ``ReadyzRateThrottle`` below (#2820,
|
|
61
|
+
# #3346): unlike these two it does real dependency round-trips, so it gets its
|
|
62
|
+
# own bounded scope instead of a blanket exemption.
|
|
63
|
+
_PROBE_EXEMPT_PATHS = frozenset(
|
|
64
|
+
path.rstrip("/") for path in ("/api/v1/health/", "/api/v1/edition/")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_probe_path(request: Request) -> bool:
|
|
69
|
+
"""Return True when the request targets an exempt k8s probe endpoint.
|
|
70
|
+
|
|
71
|
+
Matches on ``path_info`` (the path *without* any ``SCRIPT_NAME`` mount prefix)
|
|
72
|
+
so the exemption still fires when TruePPM is served under a sub-path — otherwise
|
|
73
|
+
a probe at ``/trueppm/api/v1/health/`` would carry the prefix in ``request.path``,
|
|
74
|
+
miss the exempt set, and get throttled: the exact failure this guards against.
|
|
75
|
+
"""
|
|
76
|
+
return request.path_info.rstrip("/") in _PROBE_EXEMPT_PATHS
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProbeExemptAnonRateThrottle(AnonRateThrottle):
|
|
80
|
+
"""Anonymous default throttle that never counts the k8s probe endpoints.
|
|
81
|
+
|
|
82
|
+
Inherits the ``"anon"`` scope, so its rate comes from
|
|
83
|
+
``DEFAULT_THROTTLE_RATES["anon"]``. Only the probe exemption is added; all
|
|
84
|
+
other requests are throttled exactly like the stock ``AnonRateThrottle``.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def get_cache_key(self, request: Request, view: APIView) -> str | None:
|
|
88
|
+
"""Skip throttling for probe paths; otherwise defer to DRF's default.
|
|
89
|
+
|
|
90
|
+
Returning ``None`` tells DRF not to record or limit this request, which
|
|
91
|
+
keeps the tight-loop liveness/readiness probes off the shared anon
|
|
92
|
+
bucket (see module docstring).
|
|
93
|
+
"""
|
|
94
|
+
if _is_probe_path(request):
|
|
95
|
+
return None
|
|
96
|
+
return super().get_cache_key(request, view)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ConsumableRateThrottle(SimpleRateThrottle):
|
|
100
|
+
"""A ``SimpleRateThrottle`` that can also be charged outside DRF's pipeline.
|
|
101
|
+
|
|
102
|
+
DRF throttles only run from ``APIView.check_throttles``, so a door that is not
|
|
103
|
+
a DRF view — the Django admin login (#3557) — cannot use one at all, and a
|
|
104
|
+
door that must charge a bucket it could not know about until *inside* the view
|
|
105
|
+
body — the email-identifier login retry (#3468) — cannot use one either.
|
|
106
|
+
|
|
107
|
+
:meth:`consume_key` is the shared primitive both need: record one attempt
|
|
108
|
+
against an explicit cache key and report the verdict, using the same window
|
|
109
|
+
arithmetic ``SimpleRateThrottle.allow_request`` uses, so a bucket charged this
|
|
110
|
+
way and a bucket charged by DRF are the same bucket with the same semantics.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def consume_key(cls, key: str) -> float | None:
|
|
115
|
+
"""Record one attempt against ``key``'s bucket and report the verdict.
|
|
116
|
+
|
|
117
|
+
Returns ``None`` when the attempt was within the rate, or the number of
|
|
118
|
+
seconds to wait when the bucket was already at its limit.
|
|
119
|
+
|
|
120
|
+
Charging after the fact (rather than pre-emptively) matches how DRF
|
|
121
|
+
throttles work anyway — the Nth attempt is served and the N+1th refused.
|
|
122
|
+
"""
|
|
123
|
+
throttle = cls()
|
|
124
|
+
if not throttle.rate:
|
|
125
|
+
# No rate configured for this scope — nothing to charge, nothing to refuse.
|
|
126
|
+
return None
|
|
127
|
+
throttle.key = key
|
|
128
|
+
throttle.now = throttle.timer()
|
|
129
|
+
|
|
130
|
+
# Evict entries that have aged out of the window before recording, exactly as
|
|
131
|
+
# ``SimpleRateThrottle.allow_request`` does. ``throttle_success`` refreshes the
|
|
132
|
+
# cache TTL on every write, so without this an entry could outlive the window
|
|
133
|
+
# it belongs to and over-throttle the account. ``duration`` is set in
|
|
134
|
+
# ``SimpleRateThrottle.__init__`` from ``parse_rate`` but is not declared on the
|
|
135
|
+
# class, so it carries no type for mypy --strict.
|
|
136
|
+
duration = cast("int", getattr(throttle, "duration", 0))
|
|
137
|
+
history: list[float] = list(throttle.cache.get(throttle.key, []))
|
|
138
|
+
while history and history[-1] <= throttle.now - duration:
|
|
139
|
+
history.pop()
|
|
140
|
+
throttle.history = history
|
|
141
|
+
# ``num_requests`` and ``duration`` are set in ``SimpleRateThrottle.__init__``
|
|
142
|
+
# from ``parse_rate`` but are not declared on the class, so they carry no type.
|
|
143
|
+
if len(history) >= cast("int", getattr(throttle, "num_requests", 0)):
|
|
144
|
+
# Already at the limit before this attempt. Report the wait and record
|
|
145
|
+
# nothing — DRF's own throttle_failure does not extend the window either,
|
|
146
|
+
# so a refused attempt must not push the bucket's expiry out.
|
|
147
|
+
return throttle.wait()
|
|
148
|
+
throttle.throttle_success()
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LoginIpRateThrottle(ConsumableRateThrottle):
|
|
153
|
+
"""The IP-keyed ``login`` bucket, chargeable from a non-DRF login door (#3557).
|
|
154
|
+
|
|
155
|
+
Deliberately the **same scope and the same cache key** as the
|
|
156
|
+
``ScopedRateThrottle`` the API login runs under, not a parallel bucket of its
|
|
157
|
+
own. ``/api/v1/auth/token/`` and ``/admin/login/`` are two doors onto one
|
|
158
|
+
credential set, so an attacker who exhausts one must not find the other's
|
|
159
|
+
allowance untouched — a per-door bucket would hand them ``2 x`` the guesses
|
|
160
|
+
the ``login`` rate is written to permit. Sharing it also means an operator who
|
|
161
|
+
tunes the rate tunes both doors, with nothing to keep in sync.
|
|
162
|
+
|
|
163
|
+
The key derivation mirrors ``ScopedRateThrottle.get_cache_key`` exactly
|
|
164
|
+
(account pk when the request is already authenticated, client IP otherwise);
|
|
165
|
+
if the two ever drift the buckets silently stop being the same bucket, which
|
|
166
|
+
is why :meth:`cache_key_for_request` is the single place either spells it out.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
scope = "login"
|
|
170
|
+
|
|
171
|
+
def get_cache_key(self, request: Request, view: APIView) -> str | None:
|
|
172
|
+
"""Key on the account pk when authenticated, the client IP otherwise."""
|
|
173
|
+
return self.cache_key_for_request(request)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def cache_key_for_request(cls, request: HttpRequest | Request) -> str:
|
|
177
|
+
"""Return the ``login``-scope bucket key for ``request``'s source.
|
|
178
|
+
|
|
179
|
+
Accepts a plain Django ``HttpRequest`` as well as a DRF ``Request``: it
|
|
180
|
+
reads only ``request.user`` and ``request.META``, both of which the
|
|
181
|
+
``AuthenticationMiddleware`` has already populated on the admin login path.
|
|
182
|
+
"""
|
|
183
|
+
throttle = cls()
|
|
184
|
+
user = getattr(request, "user", None)
|
|
185
|
+
# ``get_ident`` is annotated for a DRF ``Request`` but reads only ``META``
|
|
186
|
+
# (``HTTP_X_FORWARDED_FOR`` / ``REMOTE_ADDR``) plus the ``NUM_PROXIES``
|
|
187
|
+
# setting, so a plain ``HttpRequest`` satisfies it at runtime. Narrowing
|
|
188
|
+
# here rather than duplicating DRF's proxy-depth logic is what keeps the
|
|
189
|
+
# admin door's key byte-identical to the API login's.
|
|
190
|
+
ident = (
|
|
191
|
+
user.pk
|
|
192
|
+
if user is not None and user.is_authenticated
|
|
193
|
+
else throttle.get_ident(cast("Request", request))
|
|
194
|
+
)
|
|
195
|
+
return cls.cache_format % {"scope": cls.scope, "ident": ident}
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def consume(cls, request: HttpRequest | Request) -> float | None:
|
|
199
|
+
"""Charge one login attempt from ``request``'s source against the IP bucket."""
|
|
200
|
+
return cls.consume_key(cls.cache_key_for_request(request))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class LoginAccountRateThrottle(ConsumableRateThrottle):
|
|
204
|
+
"""Per-*account* login throttle, keyed on the submitted username (#1717).
|
|
205
|
+
|
|
206
|
+
Why this exists alongside the IP-keyed ``login`` throttle:
|
|
207
|
+
The stock login throttle keys only on the client IP, so it caps guesses
|
|
208
|
+
*per source address*. A distributed credential-stuffing attack against a
|
|
209
|
+
single account rotates through a botnet / proxy pool, giving the attacker
|
|
210
|
+
the full per-IP allowance from every fresh IP — the aggregate guess rate
|
|
211
|
+
against that one account is unbounded even though no single IP trips the
|
|
212
|
+
limit. This throttle closes that gap by counting failed *and* successful
|
|
213
|
+
attempts against the same normalized username regardless of source IP, so
|
|
214
|
+
an account under distributed attack is locked out after the per-account
|
|
215
|
+
rate no matter how many IPs participate. It is *stacked* with (not a
|
|
216
|
+
replacement for) the IP throttle: both must pass, so IP-local flooding and
|
|
217
|
+
cross-IP account targeting are each bounded.
|
|
218
|
+
|
|
219
|
+
Privacy: the username is lowercased/trimmed and SHA-256 hashed before it goes
|
|
220
|
+
into the cache key, so the raw email/username is never persisted in the cache
|
|
221
|
+
backend (or leaked through a cache-key dump). Hashing is sufficient here — the
|
|
222
|
+
key only needs to be stable and collision-resistant, not reversible.
|
|
223
|
+
|
|
224
|
+
This is basic brute-force hardening (table-stakes self-hosting security), not
|
|
225
|
+
an org-wide enforced lockout *policy* with admin-configurable escalation /
|
|
226
|
+
unlock workflows — that governance layer is Enterprise.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
scope = "login_account"
|
|
230
|
+
|
|
231
|
+
def get_cache_key(self, request: Request, view: APIView) -> str | None:
|
|
232
|
+
"""Key the throttle on the hashed, normalized submitted username.
|
|
233
|
+
|
|
234
|
+
Returns ``None`` (skip this throttle) when no username is present so a
|
|
235
|
+
malformed request is not charged against an empty-string bucket — the IP
|
|
236
|
+
throttle still applies to those. Reading ``request.data`` here parses the
|
|
237
|
+
request body once (DRF caches it), which is safe inside ``check_throttles``.
|
|
238
|
+
"""
|
|
239
|
+
# ``hasattr(request, "data")`` alone is insufficient: a non-object JSON body
|
|
240
|
+
# (list/str/scalar) makes ``request.data`` a non-dict with no ``.get``, which
|
|
241
|
+
# would raise AttributeError inside check_throttles → 500 before the login
|
|
242
|
+
# serializer ever runs (#2126). A non-object body carries no username, so
|
|
243
|
+
# treat it as "no username" → skip this throttle (the IP throttle still bites).
|
|
244
|
+
username = request.data.get("username") if isinstance(request.data, dict) else None
|
|
245
|
+
if not username:
|
|
246
|
+
return None
|
|
247
|
+
return self.cache_key_for(str(username))
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def cache_key_for(cls, identifier: str) -> str:
|
|
251
|
+
"""Return the bucket key for ``identifier``.
|
|
252
|
+
|
|
253
|
+
The single owner of this throttle's key derivation. ``get_cache_key`` and
|
|
254
|
+
``charge`` must not each spell the hash out, or the two drift and the
|
|
255
|
+
canonical charge silently stops landing in the bucket ``get_cache_key``
|
|
256
|
+
reads.
|
|
257
|
+
"""
|
|
258
|
+
ident = hashlib.sha256(identifier.strip().lower().encode("utf-8")).hexdigest()
|
|
259
|
+
return cls.cache_format % {"scope": cls.scope, "ident": ident}
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def consume(cls, identifier: str) -> float | None:
|
|
263
|
+
"""Record one attempt against ``identifier``'s bucket and report the verdict.
|
|
264
|
+
|
|
265
|
+
Returns ``None`` when the attempt was within the rate, or the number of
|
|
266
|
+
seconds to wait when the bucket was already at its limit.
|
|
267
|
+
|
|
268
|
+
Why this exists (#3468): the throttle runs in ``check_throttles``, *before*
|
|
269
|
+
the view body, so it can only ever key on the identifier as submitted. Once
|
|
270
|
+
one account answers to two identifiers (its username and its email), an
|
|
271
|
+
attacker gets two independent per-account buckets and the effective guess
|
|
272
|
+
allowance against that account doubles — which defeats exactly the cross-IP
|
|
273
|
+
protection #1717 added. Resolving the email inside ``get_cache_key`` would
|
|
274
|
+
put an unauthenticated, unindexed ``email__iexact`` query in front of *every*
|
|
275
|
+
login, so instead the login view calls this once it has resolved an email to
|
|
276
|
+
its canonical username.
|
|
277
|
+
|
|
278
|
+
**Recording is not enough, and the half-measure is the trap.** The username
|
|
279
|
+
bucket already accumulates every username-form attempt (DRF charges it in
|
|
280
|
+
``allow_request``) and now every email-form attempt too, so it holds the true
|
|
281
|
+
total — but nothing *checks* it on the email path, and an attacker who spends
|
|
282
|
+
the username budget first then finds the email's own bucket empty and gets a
|
|
283
|
+
second full allowance. Only the email-first order would be capped, which is
|
|
284
|
+
worse than not fixing it at all: the protection looks complete and is
|
|
285
|
+
order-dependent. So this both records **and** reports, and the view refuses
|
|
286
|
+
on a ``None``-less return.
|
|
287
|
+
|
|
288
|
+
The residual is a narrow oracle and it is the right trade: an attacker who
|
|
289
|
+
already knows a username, and who spends that account's entire per-window
|
|
290
|
+
budget on it, can then learn whether a given address belongs to that same
|
|
291
|
+
account by whether they get a 429 rather than a 401. That leaks a mapping
|
|
292
|
+
between two identifiers of one already-known account, at one bit per window,
|
|
293
|
+
at the cost of the guesses they came for — against doubling the brute-force
|
|
294
|
+
budget on every targeted account, permanently.
|
|
295
|
+
|
|
296
|
+
Charging after the fact (rather than pre-emptively) matches how DRF throttles
|
|
297
|
+
work anyway — the Nth attempt is served and the N+1th is refused.
|
|
298
|
+
"""
|
|
299
|
+
return cls.consume_key(cls.cache_key_for(identifier))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ProbeExemptUserRateThrottle(UserRateThrottle):
|
|
303
|
+
"""Authenticated default throttle that never counts the k8s probe endpoints.
|
|
304
|
+
|
|
305
|
+
Inherits the ``"user"`` scope, so its rate comes from
|
|
306
|
+
``DEFAULT_THROTTLE_RATES["user"]``. Only the probe exemption is added; all
|
|
307
|
+
other requests are throttled exactly like the stock ``UserRateThrottle``.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def get_cache_key(self, request: Request, view: APIView) -> str | None:
|
|
311
|
+
"""Skip throttling for probe paths; otherwise defer to DRF's default.
|
|
312
|
+
|
|
313
|
+
Returning ``None`` tells DRF not to record or limit this request, which
|
|
314
|
+
keeps the tight-loop liveness/readiness probes off the shared user
|
|
315
|
+
bucket (see module docstring).
|
|
316
|
+
"""
|
|
317
|
+
if _is_probe_path(request):
|
|
318
|
+
return None
|
|
319
|
+
return super().get_cache_key(request, view)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class ReadyzRateThrottle(AnonRateThrottle):
|
|
323
|
+
"""Dedicated, generous rate limit for the unauthenticated ``/readyz`` probe.
|
|
324
|
+
|
|
325
|
+
(#2820, #3346) ``/readyz`` must stay unauthenticated — kubelet carries no
|
|
326
|
+
credential — but it is not free the way ``/health/`` and ``/edition/`` are:
|
|
327
|
+
every call does a bounded database round-trip, a cache round-trip, and a
|
|
328
|
+
migration-state check. Before this it was fully exempt from the shared
|
|
329
|
+
throttle (see ``_PROBE_EXEMPT_PATHS``), which meant an unauthenticated
|
|
330
|
+
caller who reached the pod IP directly — the probe path bypasses the
|
|
331
|
+
Ingress entirely — could drive unbounded database load with no rate limit
|
|
332
|
+
of any kind.
|
|
333
|
+
|
|
334
|
+
A dedicated ``"readyz"`` scope rather than the shared ``"anon"`` bucket, on
|
|
335
|
+
purpose: kubelet's readiness/startup probes hit the pod IP directly from
|
|
336
|
+
the node, not through the Ingress, so their source IP is the node's, not an
|
|
337
|
+
end user's. Sharing "anon" would let a legitimate probe loop compete with
|
|
338
|
+
real anonymous traffic in both directions — draining the budget a genuine
|
|
339
|
+
anonymous client needs, or (with a generous enough rate to avoid that)
|
|
340
|
+
giving an attacker room under the anon scope they would not otherwise have.
|
|
341
|
+
The rate itself (``DEFAULT_THROTTLE_RATES["readyz"]``) is picked to clear
|
|
342
|
+
every documented probe configuration with room to spare — see
|
|
343
|
+
administration/probes.md for the numbers it assumes — while still bounding
|
|
344
|
+
an outright flood.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
scope = "readyz"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def resolve_client_ident(request: HttpRequest | Request) -> str:
|
|
351
|
+
"""The raw client identity the per-IP throttles key ``request`` on (#4023).
|
|
352
|
+
|
|
353
|
+
DRF's ``BaseThrottle.get_ident``: with ``NUM_PROXIES = n`` it takes the entry
|
|
354
|
+
``n`` hops from the *right* of ``X-Forwarded-For`` (the one the outermost
|
|
355
|
+
trusted proxy appended), and ``REMOTE_ADDR`` when the header is absent or
|
|
356
|
+
``NUM_PROXIES`` is 0. Never the leftmost entry: that one is written by the
|
|
357
|
+
client and is attacker-chosen at every topology. Not validated — see
|
|
358
|
+
``resolve_client_ip``.
|
|
359
|
+
"""
|
|
360
|
+
# ``get_ident`` is annotated for a DRF ``Request`` but reads only ``META`` —
|
|
361
|
+
# see ``LoginIpRateThrottle.cache_key_for_request``.
|
|
362
|
+
return str(BaseThrottle().get_ident(cast("Request", request)) or "").strip()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def resolve_client_ip(request: HttpRequest | Request) -> str | None:
|
|
366
|
+
"""The client IP to record in an audit or security log, or ``None`` (#4023).
|
|
367
|
+
|
|
368
|
+
Every record of "where this request came from" goes through here so that the
|
|
369
|
+
forensic trail names the same address the per-IP throttles charged — an
|
|
370
|
+
attacker brute-forcing login or replaying a revoked token cannot write an
|
|
371
|
+
arbitrary address into the record, and the log and the throttle never
|
|
372
|
+
disagree about one request. The address is only as trustworthy as
|
|
373
|
+
``TRUEPPM_NUM_PROXIES`` is correct; ``describe_client_address`` is how an
|
|
374
|
+
admin checks that.
|
|
375
|
+
|
|
376
|
+
``None`` when the resolved value is empty or not an IP address, so the result
|
|
377
|
+
is always safe for an ``inet`` column and cannot inject ``key=value`` pairs
|
|
378
|
+
into a log line.
|
|
379
|
+
"""
|
|
380
|
+
ident = resolve_client_ident(request)
|
|
381
|
+
if not ident:
|
|
382
|
+
return None
|
|
383
|
+
try:
|
|
384
|
+
return str(ipaddress.ip_address(ident))
|
|
385
|
+
except ValueError:
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Bounds on the X-Forwarded-For chain echoed back by ``describe_client_address``.
|
|
390
|
+
# The header is caller-supplied, so the readback must not become a way to make an
|
|
391
|
+
# admin endpoint reflect an arbitrarily large payload.
|
|
392
|
+
_XFF_ECHO_MAX_ENTRIES = 16
|
|
393
|
+
_XFF_ECHO_MAX_ENTRY_LEN = 64
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def describe_client_address(request: HttpRequest | Request) -> dict[str, Any]:
|
|
397
|
+
"""Read back which address the per-IP throttles key *this* request on (#4020).
|
|
398
|
+
|
|
399
|
+
``REST_FRAMEWORK["NUM_PROXIES"]`` is a promise about deployment topology that
|
|
400
|
+
nothing else checks, and both ways of breaking it are silent: too low and every
|
|
401
|
+
client shares one bucket (the throttles still answer ``429``, just to the wrong
|
|
402
|
+
people); too high and a client picks its own bucket by sending
|
|
403
|
+
``X-Forwarded-For`` (the throttles still answer ``401``, just forever). The
|
|
404
|
+
only way to tell is to see the address the server resolved and compare it with
|
|
405
|
+
the address the request really came from — which only the caller knows. So this
|
|
406
|
+
returns the resolution rather than trying to judge it from the server alone.
|
|
407
|
+
|
|
408
|
+
``resolved`` comes from DRF's own ``BaseThrottle.get_ident`` rather than a
|
|
409
|
+
re-implementation, so it cannot drift from what ``anon``, ``login``,
|
|
410
|
+
``share_access``, ``readyz`` and the webhook IP throttle actually key on.
|
|
411
|
+
|
|
412
|
+
``status`` flags only the shapes the server *can* prove from one request:
|
|
413
|
+
|
|
414
|
+
- ``fewer_hops_than_configured`` — fewer ``X-Forwarded-For`` entries arrived
|
|
415
|
+
than ``NUM_PROXIES`` claims proxies. Every real proxy in the configured
|
|
416
|
+
chain would have appended one, so the setting is too high for this path (or
|
|
417
|
+
a proxy on it does not set the header). Either way the resolved address is
|
|
418
|
+
not a proxy-attested one.
|
|
419
|
+
- ``forwarded_for_ignored`` — ``NUM_PROXIES`` is 0 but the header arrived. If
|
|
420
|
+
a proxy wrote it, every client shares that proxy's bucket.
|
|
421
|
+
- ``unparsable`` — the resolved value is not an IP address.
|
|
422
|
+
- ``non_public`` — the resolved address is private, loopback, link-local or
|
|
423
|
+
otherwise not globally routable. Correct for a client inside the network;
|
|
424
|
+
a proxy's address if the caller came in over the internet.
|
|
425
|
+
- ``ok`` — none of the above. Still only proven by the caller: ``resolved``
|
|
426
|
+
must be their own address, and must not change when they send their own
|
|
427
|
+
``X-Forwarded-For``.
|
|
428
|
+
|
|
429
|
+
Workspace-operator-only by construction: it is surfaced on
|
|
430
|
+
``/api/v1/health/system/`` (``IsWorkspaceOperator``, superuser-gated, #4009)
|
|
431
|
+
and never on the public probes, because it reveals the proxies' internal
|
|
432
|
+
addresses.
|
|
433
|
+
"""
|
|
434
|
+
meta = request.META
|
|
435
|
+
raw_xff = meta.get("HTTP_X_FORWARDED_FOR")
|
|
436
|
+
remote_addr = meta.get("REMOTE_ADDR")
|
|
437
|
+
num_proxies = api_settings.NUM_PROXIES
|
|
438
|
+
# ``get_ident`` is annotated for a DRF ``Request`` but reads only ``META`` —
|
|
439
|
+
# see ``LoginIpRateThrottle.cache_key_for_request``.
|
|
440
|
+
resolved = BaseThrottle().get_ident(cast("Request", request))
|
|
441
|
+
|
|
442
|
+
chain = [entry.strip() for entry in raw_xff.split(",")] if raw_xff is not None else []
|
|
443
|
+
echoed = [entry[:_XFF_ECHO_MAX_ENTRY_LEN] for entry in chain[-_XFF_ECHO_MAX_ENTRIES:]]
|
|
444
|
+
|
|
445
|
+
status, detail = _assess_client_address(
|
|
446
|
+
num_proxies=num_proxies,
|
|
447
|
+
chain_len=len(chain),
|
|
448
|
+
has_xff=raw_xff is not None,
|
|
449
|
+
resolved=resolved,
|
|
450
|
+
)
|
|
451
|
+
return {
|
|
452
|
+
"num_proxies": num_proxies,
|
|
453
|
+
"resolved": resolved[:_XFF_ECHO_MAX_ENTRY_LEN] if resolved else resolved,
|
|
454
|
+
"remote_addr": remote_addr,
|
|
455
|
+
"forwarded_for": echoed,
|
|
456
|
+
"forwarded_for_entries": len(chain),
|
|
457
|
+
"status": status,
|
|
458
|
+
"detail": detail,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _assess_client_address(
|
|
463
|
+
*, num_proxies: int | None, chain_len: int, has_xff: bool, resolved: str | None
|
|
464
|
+
) -> tuple[str, str]:
|
|
465
|
+
"""Classify one request's client-address resolution (see ``describe_client_address``)."""
|
|
466
|
+
if num_proxies is None:
|
|
467
|
+
# DRF's legacy mode: the whole header, whitespace-stripped, is the ident.
|
|
468
|
+
# TruePPM always sets NUM_PROXIES, so this only appears if an override drops it.
|
|
469
|
+
return (
|
|
470
|
+
"fewer_hops_than_configured",
|
|
471
|
+
"NUM_PROXIES is unset, so the per-IP limits key on the whole "
|
|
472
|
+
"client-supplied X-Forwarded-For header. Set TRUEPPM_NUM_PROXIES.",
|
|
473
|
+
)
|
|
474
|
+
if num_proxies == 0:
|
|
475
|
+
if has_xff:
|
|
476
|
+
return (
|
|
477
|
+
"forwarded_for_ignored",
|
|
478
|
+
"X-Forwarded-For arrived but TRUEPPM_NUM_PROXIES is 0, so the per-IP "
|
|
479
|
+
"limits key on the immediate peer. If that peer is a proxy you run, "
|
|
480
|
+
"every client shares its bucket: set TRUEPPM_NUM_PROXIES to the number "
|
|
481
|
+
"of proxies in front of Django.",
|
|
482
|
+
)
|
|
483
|
+
elif chain_len < num_proxies:
|
|
484
|
+
return (
|
|
485
|
+
"fewer_hops_than_configured",
|
|
486
|
+
f"TRUEPPM_NUM_PROXIES is {num_proxies} but only {chain_len} "
|
|
487
|
+
"X-Forwarded-For entries arrived, so fewer proxies wrote the header than "
|
|
488
|
+
"the setting claims. A client can then choose the address the per-IP "
|
|
489
|
+
"limits key on. Lower TRUEPPM_NUM_PROXIES to the real number of proxies "
|
|
490
|
+
"on this path, or make each proxy append to X-Forwarded-For.",
|
|
491
|
+
)
|
|
492
|
+
try:
|
|
493
|
+
address = ipaddress.ip_address(resolved or "")
|
|
494
|
+
except ValueError:
|
|
495
|
+
return (
|
|
496
|
+
"unparsable",
|
|
497
|
+
"The address the per-IP limits key on is not an IP address, so the "
|
|
498
|
+
"position TRUEPPM_NUM_PROXIES selects holds something a proxy did not "
|
|
499
|
+
"write. Check the proxy chain and the setting.",
|
|
500
|
+
)
|
|
501
|
+
if not address.is_global:
|
|
502
|
+
return (
|
|
503
|
+
"non_public",
|
|
504
|
+
"The per-IP limits key this request on a non-public address. That is "
|
|
505
|
+
"correct if you are inside the network; if you came in over the "
|
|
506
|
+
"internet it is a proxy's address and every client shares one bucket "
|
|
507
|
+
"— raise TRUEPPM_NUM_PROXIES and make the outer proxy forward the chain.",
|
|
508
|
+
)
|
|
509
|
+
return (
|
|
510
|
+
"ok",
|
|
511
|
+
"Nothing wrong is provable from this request. Confirm `resolved` is your own "
|
|
512
|
+
"address, and that it does not change when you send your own "
|
|
513
|
+
"X-Forwarded-For header — if it does, TRUEPPM_NUM_PROXIES is too high.",
|
|
514
|
+
)
|