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,1034 @@
|
|
|
1
|
+
"""SECRET_KEY hardening — refuse to boot in prod with a weak key (#566).
|
|
2
|
+
|
|
3
|
+
PYSEC-2025-183 against pyjwt notes that key length is the caller's
|
|
4
|
+
responsibility. ``djangorestframework-simplejwt`` inherits Django's
|
|
5
|
+
``SECRET_KEY`` for ``SIGNING_KEY`` unless explicitly overridden, so any
|
|
6
|
+
weakness in ``SECRET_KEY`` flows directly into JWT signing. Django's own
|
|
7
|
+
``check_secret_key`` only warns; we want a hard refusal in prod.
|
|
8
|
+
|
|
9
|
+
Two enforcement paths share the same validator:
|
|
10
|
+
|
|
11
|
+
* ``manage.py check --deploy`` — exercised via the Django system-check
|
|
12
|
+
registry below (tagged ``Tags.security``, ``deploy=True``).
|
|
13
|
+
* App boot — ``settings/prod.py`` calls :func:`validate_secret_key` at
|
|
14
|
+
import time and raises ``RuntimeError`` on any error message. Without
|
|
15
|
+
this, ``gunicorn``/``asgi`` workers never run system checks and a weak
|
|
16
|
+
key would only surface at the first JWT verify failure.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import uuid
|
|
24
|
+
from collections.abc import Mapping, Sequence
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from django.core.checks import Error, register
|
|
28
|
+
from django.core.checks import Warning as CheckWarning
|
|
29
|
+
from django.core.checks.messages import CheckMessage
|
|
30
|
+
from django.core.checks.registry import Tags
|
|
31
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
32
|
+
from django.utils.module_loading import import_string
|
|
33
|
+
|
|
34
|
+
from trueppm_api.core.storage_config import S3_STORAGE_BACKENDS
|
|
35
|
+
|
|
36
|
+
MIN_SECRET_KEY_LENGTH = 32
|
|
37
|
+
|
|
38
|
+
INSECURE_PREFIX = "django-insecure-"
|
|
39
|
+
|
|
40
|
+
# Deploy-check ids are an operator-facing contract — they are what goes into
|
|
41
|
+
# ``SILENCED_SYSTEM_CHECKS`` — so they are bound to constants rather than
|
|
42
|
+
# repeated, and must not be renumbered once shipped.
|
|
43
|
+
#
|
|
44
|
+
# E004 is deliberately shared by two unrelated checks: an untrustworthy
|
|
45
|
+
# JWT signing key and an unusable attachment-storage backend. That sharing
|
|
46
|
+
# predates this constant and is asserted by tests in both
|
|
47
|
+
# ``test_signing_key_check.py`` and ``test_secret_key_check.py``; naming it once
|
|
48
|
+
# here does not change it. The cost is that an operator silencing E004 for one
|
|
49
|
+
# also silences the other, which is worth splitting separately rather than as
|
|
50
|
+
# part of a Sonar cleanup.
|
|
51
|
+
_ID_UNTRUSTED_CONFIG = "trueppm.E004"
|
|
52
|
+
|
|
53
|
+
#: Prefixes of documented placeholder keys, matched case-insensitively (#3187).
|
|
54
|
+
#:
|
|
55
|
+
#: Length and the ``django-insecure-`` prefix were the only two things
|
|
56
|
+
#: ``validate_secret_key`` looked at, and the placeholder ``.env.example`` shipped
|
|
57
|
+
#: — ``REPLACE-WITH-A-LONG-RANDOM-STRING-AT-LEAST-32-CHARS-LONG`` — satisfied
|
|
58
|
+
#: both: 56 characters, no Django prefix. It therefore cleared this validator AND
|
|
59
|
+
#: ``validate_signing_key``, and because ``JWT_SIGNING_KEY`` defaults to
|
|
60
|
+
#: ``SECRET_KEY``, a verbatim ``cp .env.example .env`` (which the README and the
|
|
61
|
+
#: deployment guide both instruct) yielded a production install whose
|
|
62
|
+
#: token-signing key was published in this repository.
|
|
63
|
+
#:
|
|
64
|
+
#: The real fix is that the key now ships empty and ``init-prod.sh`` generates it;
|
|
65
|
+
#: this list is the backstop for an operator who reaches a deploy some other way,
|
|
66
|
+
#: and for the docs' own strings if they are ever copied verbatim again. Kept
|
|
67
|
+
#: deliberately short and prefix-matched, for the same reason
|
|
68
|
+
#: ``PLACEHOLDER_SERVICE_PASSWORDS`` is: a denylist that tries to be a
|
|
69
|
+
#: password-strength oracle produces false failures on legitimate secrets.
|
|
70
|
+
PLACEHOLDER_SECRET_KEY_PREFIXES = ("replace-with", "change-me", "changeme", "your-secret")
|
|
71
|
+
|
|
72
|
+
# Remediation commands, extracted so the same generator isn't duplicated across every
|
|
73
|
+
# ``Error`` hint (SonarCloud S1192). The secrets variant seeds ``SECRET_KEY`` and the
|
|
74
|
+
# JWT signing key; the Fernet variant seeds the integration-credential encryption key.
|
|
75
|
+
_TOKEN_URLSAFE_CMD = 'python3 -c "import secrets; print(secrets.token_urlsafe(50))"'
|
|
76
|
+
_TOKEN_URLSAFE_HINT = f"Generate one with: {_TOKEN_URLSAFE_CMD}"
|
|
77
|
+
_FERNET_KEY_CMD = (
|
|
78
|
+
'python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"'
|
|
79
|
+
)
|
|
80
|
+
_FERNET_KEY_HINT = f"Generate one with: {_FERNET_KEY_CMD}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_placeholder_key(key: str) -> bool:
|
|
84
|
+
"""True when ``key`` starts with a documented placeholder prefix (#3187)."""
|
|
85
|
+
lowered = key.lower()
|
|
86
|
+
return any(lowered.startswith(prefix) for prefix in PLACEHOLDER_SECRET_KEY_PREFIXES)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_secret_key(secret_key: str | None, *, debug: bool) -> list[CheckMessage]:
|
|
90
|
+
"""Return Django check errors for a weak ``SECRET_KEY`` in prod.
|
|
91
|
+
|
|
92
|
+
Returns an empty list when ``debug`` is True so developer workstations
|
|
93
|
+
keep booting with the placeholder key from ``settings/base.py``.
|
|
94
|
+
"""
|
|
95
|
+
if debug:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
errors: list[CheckMessage] = []
|
|
99
|
+
|
|
100
|
+
if not secret_key:
|
|
101
|
+
errors.append(
|
|
102
|
+
Error(
|
|
103
|
+
"SECRET_KEY is empty in a non-DEBUG environment.",
|
|
104
|
+
hint=_TOKEN_URLSAFE_HINT,
|
|
105
|
+
id="trueppm.E001",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return errors
|
|
109
|
+
|
|
110
|
+
if secret_key.startswith(INSECURE_PREFIX):
|
|
111
|
+
errors.append(
|
|
112
|
+
Error(
|
|
113
|
+
f"SECRET_KEY starts with {INSECURE_PREFIX!r} — this is the "
|
|
114
|
+
"Django placeholder and must not be used outside DEBUG.",
|
|
115
|
+
hint=_TOKEN_URLSAFE_HINT,
|
|
116
|
+
id="trueppm.E002",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if _is_placeholder_key(secret_key):
|
|
121
|
+
errors.append(
|
|
122
|
+
Error(
|
|
123
|
+
f"SECRET_KEY is a documented placeholder ({secret_key!r}) — it is "
|
|
124
|
+
"published in this repository, so JWTs signed with it can be "
|
|
125
|
+
"forged by anyone.",
|
|
126
|
+
hint=_TOKEN_URLSAFE_HINT,
|
|
127
|
+
id="trueppm.E002",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if len(secret_key) < MIN_SECRET_KEY_LENGTH:
|
|
132
|
+
errors.append(
|
|
133
|
+
Error(
|
|
134
|
+
f"SECRET_KEY is {len(secret_key)} characters; minimum is {MIN_SECRET_KEY_LENGTH}.",
|
|
135
|
+
hint=(
|
|
136
|
+
"JWT signing inherits SECRET_KEY when SIMPLE_JWT.SIGNING_KEY "
|
|
137
|
+
"is unset (PYSEC-2025-183). Generate a strong key with: " + _TOKEN_URLSAFE_CMD
|
|
138
|
+
),
|
|
139
|
+
id="trueppm.E003",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return errors
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Placeholder credentials that ship in .env.example / docs and must never survive
|
|
147
|
+
# into a real deploy. Compared case-insensitively against the password component of
|
|
148
|
+
# DATABASE_URL / REDIS_URL. Kept deliberately short: this is a guard against the
|
|
149
|
+
# documented placeholder being left in place, not a password-strength oracle — a
|
|
150
|
+
# denylist that tries to be clever produces false failures on legitimate secrets.
|
|
151
|
+
PLACEHOLDER_SERVICE_PASSWORDS = frozenset(
|
|
152
|
+
{
|
|
153
|
+
"change-me",
|
|
154
|
+
"changeme",
|
|
155
|
+
"password",
|
|
156
|
+
"postgres",
|
|
157
|
+
"trueppm",
|
|
158
|
+
"secret",
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
MIN_SERVICE_PASSWORD_LENGTH = 12
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def validate_service_credentials(
|
|
166
|
+
db_password: str | None,
|
|
167
|
+
redis_password: str | None,
|
|
168
|
+
*,
|
|
169
|
+
debug: bool,
|
|
170
|
+
) -> list[CheckMessage]:
|
|
171
|
+
"""Return errors for placeholder or absent DB / Valkey passwords in prod (#3176).
|
|
172
|
+
|
|
173
|
+
The gap this closes: ``SECRET_KEY``, the attachment backend, the integration
|
|
174
|
+
encryption key, and ``sslmode`` all refuse to boot when misconfigured, but the
|
|
175
|
+
database and cache credentials were checked only for *presence* — by compose's
|
|
176
|
+
``${DB_PASSWORD:?...}``, which ``change-me`` satisfies. ``.env.example`` shipped
|
|
177
|
+
exactly that string, so the documented copy-paste path produced a deploy whose
|
|
178
|
+
every other credential was validated and whose datastore ones were not.
|
|
179
|
+
|
|
180
|
+
Bounded on the bundled compose stack (neither service publishes a host port), so
|
|
181
|
+
this is an ``Error`` about the credential itself rather than a claim of
|
|
182
|
+
exploitability — and it stops being bounded the moment an operator repoints at an
|
|
183
|
+
external database, which ``.env.example`` documents how to do.
|
|
184
|
+
|
|
185
|
+
A URL with no password component at all is left alone: trust authentication
|
|
186
|
+
(Unix socket, IAM, cert) is a legitimate posture and has no password to weaken.
|
|
187
|
+
Returns an empty list when ``debug`` is True, like every sibling validator.
|
|
188
|
+
"""
|
|
189
|
+
if debug:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
errors: list[CheckMessage] = []
|
|
193
|
+
for label, password, err_id in (
|
|
194
|
+
("DATABASE_URL", db_password, "trueppm.E010"),
|
|
195
|
+
("REDIS_URL", redis_password, "trueppm.E011"),
|
|
196
|
+
):
|
|
197
|
+
if not password:
|
|
198
|
+
# No password component — trust auth (Unix socket, IAM, cert), or an
|
|
199
|
+
# unauthenticated local service. Neither is this check's business.
|
|
200
|
+
continue
|
|
201
|
+
if password.lower() in PLACEHOLDER_SERVICE_PASSWORDS:
|
|
202
|
+
errors.append(
|
|
203
|
+
Error(
|
|
204
|
+
f"{label} uses the placeholder password {password!r} in a "
|
|
205
|
+
"non-DEBUG environment.",
|
|
206
|
+
hint=(
|
|
207
|
+
"This is the value shipped in .env.example as a fill-me-in. "
|
|
208
|
+
"Replace it with: " + _TOKEN_URLSAFE_CMD
|
|
209
|
+
),
|
|
210
|
+
id=err_id,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
elif len(password) < MIN_SERVICE_PASSWORD_LENGTH:
|
|
214
|
+
errors.append(
|
|
215
|
+
Error(
|
|
216
|
+
f"{label} password is {len(password)} characters; minimum is "
|
|
217
|
+
f"{MIN_SERVICE_PASSWORD_LENGTH}.",
|
|
218
|
+
hint="Generate a strong one with: " + _TOKEN_URLSAFE_CMD,
|
|
219
|
+
id=err_id,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return errors
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@register(Tags.security, deploy=True)
|
|
226
|
+
def check_service_credentials(
|
|
227
|
+
app_configs: Sequence[object] | None = None,
|
|
228
|
+
**kwargs: object,
|
|
229
|
+
) -> list[CheckMessage]:
|
|
230
|
+
"""Django system check entry point — reads live settings."""
|
|
231
|
+
from django.conf import settings
|
|
232
|
+
|
|
233
|
+
databases = getattr(settings, "DATABASES", {}) or {}
|
|
234
|
+
db_password = (databases.get("default") or {}).get("PASSWORD")
|
|
235
|
+
redis_password = urllib.parse.urlparse(getattr(settings, "REDIS_URL", "") or "").password
|
|
236
|
+
|
|
237
|
+
return validate_service_credentials(
|
|
238
|
+
db_password,
|
|
239
|
+
redis_password,
|
|
240
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@register(Tags.security, deploy=True)
|
|
245
|
+
def check_secret_key(
|
|
246
|
+
app_configs: Sequence[object] | None = None,
|
|
247
|
+
**kwargs: object,
|
|
248
|
+
) -> list[CheckMessage]:
|
|
249
|
+
"""Django system check entry point — reads live settings."""
|
|
250
|
+
from django.conf import settings
|
|
251
|
+
|
|
252
|
+
return validate_secret_key(
|
|
253
|
+
getattr(settings, "SECRET_KEY", None),
|
|
254
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def validate_signing_key(
|
|
259
|
+
signing_key: str | None,
|
|
260
|
+
secret_key: str | None,
|
|
261
|
+
*,
|
|
262
|
+
debug: bool,
|
|
263
|
+
) -> list[CheckMessage]:
|
|
264
|
+
"""Return deploy errors for a weak, explicitly-set JWT ``SIGNING_KEY`` (#2247).
|
|
265
|
+
|
|
266
|
+
When ``JWT_SIGNING_KEY`` is left unset it defaults to ``SECRET_KEY`` (see
|
|
267
|
+
``settings/base.py``), and :func:`validate_secret_key` already covers that
|
|
268
|
+
value — re-validating here would double-report the same error. So this only
|
|
269
|
+
fires when the operator has set a *distinct* signing key, and it applies the
|
|
270
|
+
same strength bar (non-placeholder, ``>= MIN_SECRET_KEY_LENGTH``) because a
|
|
271
|
+
separate-but-weak signing key would defeat the whole point of separating it.
|
|
272
|
+
Returns an empty list under DEBUG so developer workstations keep booting.
|
|
273
|
+
"""
|
|
274
|
+
if debug:
|
|
275
|
+
return []
|
|
276
|
+
if not signing_key or signing_key == secret_key:
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
errors: list[CheckMessage] = []
|
|
280
|
+
|
|
281
|
+
if signing_key.startswith(INSECURE_PREFIX):
|
|
282
|
+
errors.append(
|
|
283
|
+
Error(
|
|
284
|
+
f"JWT_SIGNING_KEY starts with {INSECURE_PREFIX!r} — this is the "
|
|
285
|
+
"Django placeholder and must not be used to sign tokens.",
|
|
286
|
+
hint=_TOKEN_URLSAFE_HINT,
|
|
287
|
+
id=_ID_UNTRUSTED_CONFIG,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if _is_placeholder_key(signing_key):
|
|
292
|
+
errors.append(
|
|
293
|
+
Error(
|
|
294
|
+
f"JWT_SIGNING_KEY is a documented placeholder ({signing_key!r}) — "
|
|
295
|
+
"it is published in this repository, so tokens signed with it can "
|
|
296
|
+
"be forged by anyone.",
|
|
297
|
+
hint=_TOKEN_URLSAFE_HINT,
|
|
298
|
+
id=_ID_UNTRUSTED_CONFIG,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if len(signing_key) < MIN_SECRET_KEY_LENGTH:
|
|
303
|
+
errors.append(
|
|
304
|
+
Error(
|
|
305
|
+
f"JWT_SIGNING_KEY is {len(signing_key)} characters; "
|
|
306
|
+
f"minimum is {MIN_SECRET_KEY_LENGTH}.",
|
|
307
|
+
hint=(
|
|
308
|
+
"A separate JWT signing key must be at least as strong as "
|
|
309
|
+
"SECRET_KEY, or a leak of it forges tokens for any user. " + _TOKEN_URLSAFE_HINT
|
|
310
|
+
),
|
|
311
|
+
id="trueppm.E005",
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return errors
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@register(Tags.security, deploy=True)
|
|
319
|
+
def check_signing_key(
|
|
320
|
+
app_configs: Sequence[object] | None = None,
|
|
321
|
+
**kwargs: object,
|
|
322
|
+
) -> list[CheckMessage]:
|
|
323
|
+
"""Django system check entry point — reads live settings."""
|
|
324
|
+
from django.conf import settings
|
|
325
|
+
|
|
326
|
+
return validate_signing_key(
|
|
327
|
+
getattr(settings, "JWT_SIGNING_KEY", None),
|
|
328
|
+
getattr(settings, "SECRET_KEY", None),
|
|
329
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Attachment storage hardening — refuse to boot in prod when uploads would land
|
|
335
|
+
# on ephemeral local disk (#775).
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
_LOCAL_STORAGE_BACKENDS = frozenset(
|
|
339
|
+
{
|
|
340
|
+
"django.core.files.storage.FileSystemStorage",
|
|
341
|
+
"django.core.files.storage.filesystem.FileSystemStorage",
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _media_root_write_failure(media_root: str | os.PathLike[str] | None) -> str | None:
|
|
347
|
+
"""Return why ``media_root`` cannot receive uploads, or None if it can.
|
|
348
|
+
|
|
349
|
+
Probes with a real create-and-delete rather than ``os.access``: the container
|
|
350
|
+
runs as a non-root UID against a volume whose ownership the operator controls,
|
|
351
|
+
and ``os.access`` answers from the permission bits without accounting for a
|
|
352
|
+
read-only mount, so it returns True on exactly the path that then raises
|
|
353
|
+
EROFS. Missing parents are created — the chart mounts an empty PVC and Django
|
|
354
|
+
would otherwise only create the tree on the first upload.
|
|
355
|
+
|
|
356
|
+
The probe file is per-process and its removal tolerates being already gone.
|
|
357
|
+
Six containers import these settings against ONE ReadWriteMany volume, and
|
|
358
|
+
they boot together: with a shared probe name, two of them interleaving
|
|
359
|
+
write/unlink leaves the loser's ``unlink`` raising ``FileNotFoundError`` —
|
|
360
|
+
an ``OSError``, which this function would report as "not writable" and the
|
|
361
|
+
boot guard would turn into a crash-loop on a perfectly good volume.
|
|
362
|
+
"""
|
|
363
|
+
if media_root is None or str(media_root) == "":
|
|
364
|
+
return "MEDIA_ROOT is empty, so uploads resolve against the process working directory"
|
|
365
|
+
path = Path(media_root)
|
|
366
|
+
if not path.is_absolute():
|
|
367
|
+
return (
|
|
368
|
+
f"MEDIA_ROOT {str(media_root)!r} is relative, so uploads follow the "
|
|
369
|
+
"process working directory"
|
|
370
|
+
)
|
|
371
|
+
try:
|
|
372
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
probe = path / f".trueppm-write-probe-{os.getpid()}-{uuid.uuid4().hex}"
|
|
374
|
+
probe.write_bytes(b"")
|
|
375
|
+
probe.unlink(missing_ok=True)
|
|
376
|
+
except OSError as exc:
|
|
377
|
+
return f"MEDIA_ROOT {str(path)!r} is not writable: {exc.strerror or exc}"
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def validate_attachment_storage(
|
|
382
|
+
default_storage_backend: str | None,
|
|
383
|
+
*,
|
|
384
|
+
debug: bool,
|
|
385
|
+
allow_local: bool,
|
|
386
|
+
media_root: str | os.PathLike[str] | None = None,
|
|
387
|
+
writes_disabled: bool = False,
|
|
388
|
+
) -> list[CheckMessage]:
|
|
389
|
+
"""Return a deploy error when attachments cannot durably land on disk.
|
|
390
|
+
|
|
391
|
+
Two distinct failures, both of which used to surface only on the first upload:
|
|
392
|
+
|
|
393
|
+
1. ``TaskAttachment.file`` uses the default file-storage backend. In a
|
|
394
|
+
containerized prod deploy a ``FileSystemStorage`` backend loses every
|
|
395
|
+
upload on pod restart and the signed-url action returns a non-signed
|
|
396
|
+
static path. Operators that back local storage with a persistent volume
|
|
397
|
+
can opt in via ``TRUEPPM_ALLOW_LOCAL_ATTACHMENT_STORAGE=true``.
|
|
398
|
+
2. Having opted in, the operator may have no writable ``MEDIA_ROOT`` behind
|
|
399
|
+
the opt-in — which was every deployment before #3184, because the setting
|
|
400
|
+
did not exist and Django's default resolved against ``/app`` on a
|
|
401
|
+
read-only root filesystem. The opt-in is a claim about the deployment
|
|
402
|
+
("local disk is durable here"); this check is what makes the claim testable
|
|
403
|
+
at boot instead of at the first EROFS on upload. ``media_root`` is optional
|
|
404
|
+
so a caller that only wants the backend question keeps the old signature.
|
|
405
|
+
|
|
406
|
+
Returns an empty list under DEBUG so developer workstations keep using local
|
|
407
|
+
storage.
|
|
408
|
+
|
|
409
|
+
``writes_disabled`` (ADR-1197) is the third escape, distinct from both: the
|
|
410
|
+
interactive/share-link demo modes give the api pod no writable media path
|
|
411
|
+
and no storage credentials on purpose, but they are not durability holes —
|
|
412
|
+
``DemoReadOnlyMiddleware`` (armed by the same ``TRUEPPM_DEMO_READ_ONLY`` this
|
|
413
|
+
flag is sourced from) refuses every attachment upload before a request can
|
|
414
|
+
reach storage at all. A backend that will never be written to cannot lose an
|
|
415
|
+
upload, so the durability question this check exists to enforce does not
|
|
416
|
+
apply. This is deliberately keyed off the SAME flag that arms the write
|
|
417
|
+
fence rather than a second, independently-settable one: a standalone
|
|
418
|
+
"attachments disabled" flag could drift out of sync with whether writes are
|
|
419
|
+
actually blocked and reopen the exact hole #775 closed.
|
|
420
|
+
|
|
421
|
+
This parameter is intentionally generic — any future caller whose writes are
|
|
422
|
+
provably blocked before reaching storage may pass ``writes_disabled=True`` —
|
|
423
|
+
but "provably" is load-bearing: a bare boolean here is only as trustworthy
|
|
424
|
+
as its wiring. Every value passed in today is DEMO_READ_ONLY, verified at
|
|
425
|
+
boot by :func:`validate_demo_read_only_enforcement` (settings/prod.py calls
|
|
426
|
+
it first and raises if DemoReadOnlyMiddleware is not actually installed). A
|
|
427
|
+
new source must add its own equivalent trip-wire rather than relying on
|
|
428
|
+
this docstring's promise.
|
|
429
|
+
"""
|
|
430
|
+
if debug or writes_disabled:
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
is_local = default_storage_backend in _LOCAL_STORAGE_BACKENDS
|
|
434
|
+
|
|
435
|
+
if allow_local:
|
|
436
|
+
if not is_local or media_root is None:
|
|
437
|
+
return []
|
|
438
|
+
failure = _media_root_write_failure(media_root)
|
|
439
|
+
if failure is None:
|
|
440
|
+
return []
|
|
441
|
+
return [
|
|
442
|
+
Error(
|
|
443
|
+
"TRUEPPM_ALLOW_LOCAL_ATTACHMENT_STORAGE is set, but local "
|
|
444
|
+
f"attachment storage cannot be used: {failure}.",
|
|
445
|
+
hint=(
|
|
446
|
+
"Set TRUEPPM_MEDIA_ROOT to a writable persistent path and mount a "
|
|
447
|
+
"volume there. On Helm set persistence.media.enabled=true (the "
|
|
448
|
+
"chart mounts the claim on the api and celery-worker pods); on "
|
|
449
|
+
"docker compose the `media` volume is mounted at "
|
|
450
|
+
"/var/lib/trueppm/media. Or drop the opt-in and point "
|
|
451
|
+
"TRUEPPM_DEFAULT_FILE_STORAGE at object storage."
|
|
452
|
+
),
|
|
453
|
+
id=_ID_UNTRUSTED_CONFIG,
|
|
454
|
+
)
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
if is_local:
|
|
458
|
+
return [
|
|
459
|
+
Error(
|
|
460
|
+
"Task attachments use local filesystem storage "
|
|
461
|
+
f"({default_storage_backend}) in a non-DEBUG environment; uploads "
|
|
462
|
+
"are lost on container/pod restart.",
|
|
463
|
+
hint=(
|
|
464
|
+
"Point STORAGES['default']['BACKEND'] at a remote object-storage "
|
|
465
|
+
"backend (e.g. S3/MinIO via django-storages) using the "
|
|
466
|
+
"TRUEPPM_DEFAULT_FILE_STORAGE env var, or set "
|
|
467
|
+
"TRUEPPM_ALLOW_LOCAL_ATTACHMENT_STORAGE=true if local storage is "
|
|
468
|
+
"backed by a persistent volume (and TRUEPPM_MEDIA_ROOT points at "
|
|
469
|
+
"that volume)."
|
|
470
|
+
),
|
|
471
|
+
id=_ID_UNTRUSTED_CONFIG,
|
|
472
|
+
)
|
|
473
|
+
]
|
|
474
|
+
return []
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@register(Tags.security, deploy=True)
|
|
478
|
+
def check_attachment_storage(
|
|
479
|
+
app_configs: Sequence[object] | None = None,
|
|
480
|
+
**kwargs: object,
|
|
481
|
+
) -> list[CheckMessage]:
|
|
482
|
+
"""Django system check entry point — reads live STORAGES + override flag."""
|
|
483
|
+
from django.conf import settings
|
|
484
|
+
|
|
485
|
+
storages = getattr(settings, "STORAGES", {}) or {}
|
|
486
|
+
backend = storages.get("default", {}).get("BACKEND")
|
|
487
|
+
return validate_attachment_storage(
|
|
488
|
+
backend,
|
|
489
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
490
|
+
allow_local=bool(getattr(settings, "ALLOW_LOCAL_ATTACHMENT_STORAGE", False)),
|
|
491
|
+
media_root=getattr(settings, "MEDIA_ROOT", None),
|
|
492
|
+
writes_disabled=bool(getattr(settings, "DEMO_READ_ONLY", False)),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
# writes_disabled trip-wire (#3925, ADR-1197)
|
|
498
|
+
#
|
|
499
|
+
# validate_attachment_storage's writes_disabled parameter is deliberately
|
|
500
|
+
# generic — "no request can reach storage" is a legitimate boot-guard escape
|
|
501
|
+
# for any future read-only deployment mode, not something to special-case by
|
|
502
|
+
# name. But a bare boolean is only as trustworthy as its wiring: EVERY
|
|
503
|
+
# deployment shape we ship today (there is exactly one — DEMO_READ_ONLY /
|
|
504
|
+
# DemoReadOnlyMiddleware) must prove the enforcement it claims is actually
|
|
505
|
+
# installed, not just assert it. Without this, a future refactor that trims or
|
|
506
|
+
# reorders MIDDLEWARE and drops the demo-read-only entry — while
|
|
507
|
+
# TRUEPPM_DEMO_READ_ONLY=true is still set somewhere — would silently leave
|
|
508
|
+
# the attachment-storage guard disabled for a chart that no longer blocks any
|
|
509
|
+
# write, reopening #775 with no failing check anywhere to catch it.
|
|
510
|
+
#
|
|
511
|
+
# A NEW caller that wants its own writes_disabled=True source (a maintenance
|
|
512
|
+
# read-only lock, a DR read replica, ...) must add an equivalent trip-wire for
|
|
513
|
+
# ITS enforcement mechanism — this one only proves DemoReadOnlyMiddleware.
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
_DEMO_READ_ONLY_MIDDLEWARE = "trueppm_api.core.demo_read_only.DemoReadOnlyMiddleware"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def validate_demo_read_only_enforcement(
|
|
520
|
+
*, demo_read_only: bool, middleware: Sequence[str]
|
|
521
|
+
) -> list[CheckMessage]:
|
|
522
|
+
"""Refuse to boot if DEMO_READ_ONLY is set but its middleware is not installed.
|
|
523
|
+
|
|
524
|
+
Returns an empty list when ``demo_read_only`` is False — a normal, non-demo
|
|
525
|
+
chart never sets ``TRUEPPM_DEMO_READ_ONLY``, so this is a no-op for every
|
|
526
|
+
production deployment and only ever fires on a demo deployment whose
|
|
527
|
+
MIDDLEWARE has drifted out of sync with its own claim.
|
|
528
|
+
"""
|
|
529
|
+
if not demo_read_only or _DEMO_READ_ONLY_MIDDLEWARE in middleware:
|
|
530
|
+
return []
|
|
531
|
+
return [
|
|
532
|
+
Error(
|
|
533
|
+
"TRUEPPM_DEMO_READ_ONLY is set, but "
|
|
534
|
+
f"{_DEMO_READ_ONLY_MIDDLEWARE} is not in MIDDLEWARE — unsafe "
|
|
535
|
+
"requests are not actually being refused, and the attachment-"
|
|
536
|
+
"storage boot guard (#775) has been told they are.",
|
|
537
|
+
hint=(
|
|
538
|
+
f"Restore '{_DEMO_READ_ONLY_MIDDLEWARE}' to MIDDLEWARE, or unset "
|
|
539
|
+
"TRUEPPM_DEMO_READ_ONLY if this deployment is no longer meant "
|
|
540
|
+
"to be read-only."
|
|
541
|
+
),
|
|
542
|
+
id="trueppm.E013",
|
|
543
|
+
)
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@register(Tags.security, deploy=True)
|
|
548
|
+
def check_demo_read_only_enforcement(
|
|
549
|
+
app_configs: Sequence[object] | None = None,
|
|
550
|
+
**kwargs: object,
|
|
551
|
+
) -> list[CheckMessage]:
|
|
552
|
+
"""Django system check entry point — reads live DEMO_READ_ONLY + MIDDLEWARE."""
|
|
553
|
+
from django.conf import settings
|
|
554
|
+
|
|
555
|
+
return validate_demo_read_only_enforcement(
|
|
556
|
+
demo_read_only=bool(getattr(settings, "DEMO_READ_ONLY", False)),
|
|
557
|
+
middleware=getattr(settings, "MIDDLEWARE", []) or [],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
# Storage-backend importability (#2559)
|
|
563
|
+
#
|
|
564
|
+
# TRUEPPM_DEFAULT_FILE_STORAGE is a dotted path resolved lazily by Django, so a
|
|
565
|
+
# typo — or a backend whose package the image does not carry — surfaces as a bare
|
|
566
|
+
# ``ModuleNotFoundError`` on the first attachment upload rather than at deploy
|
|
567
|
+
# time. The image bundles django-storages' S3 extra only; GCS and Azure Blob are
|
|
568
|
+
# on the signed-url allow-list but need extras we do not ship, so the hint has to
|
|
569
|
+
# name the specific package to install rather than say "install django-storages".
|
|
570
|
+
# ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
#: Dotted-path prefix -> the pip requirement that provides it. Prefix-matched so a
|
|
573
|
+
#: subclass or a future backend module in the same package still resolves to the
|
|
574
|
+
#: right remediation.
|
|
575
|
+
_STORAGE_BACKEND_PACKAGES: tuple[tuple[str, str], ...] = (
|
|
576
|
+
("storages.backends.gcloud", "django-storages[google]"),
|
|
577
|
+
("storages.backends.azure_storage", "django-storages[azure]"),
|
|
578
|
+
("storages.backends.sftpstorage", "django-storages[sftp]"),
|
|
579
|
+
("storages.backends.dropbox", "django-storages[dropbox]"),
|
|
580
|
+
("storages.backends.s3", "django-storages[s3]"),
|
|
581
|
+
("storages.", "django-storages"),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _remediation_package(backend_path: str) -> str | None:
|
|
586
|
+
"""Return the pip requirement providing ``backend_path``, if we know one."""
|
|
587
|
+
for prefix, requirement in _STORAGE_BACKEND_PACKAGES:
|
|
588
|
+
if backend_path.startswith(prefix):
|
|
589
|
+
return requirement
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def validate_storage_backend(
|
|
594
|
+
default_storage_backend: str | None,
|
|
595
|
+
*,
|
|
596
|
+
backend_options: Mapping[str, object] | None = None,
|
|
597
|
+
) -> list[CheckMessage]:
|
|
598
|
+
"""Return deploy errors when the configured storage backend cannot work.
|
|
599
|
+
|
|
600
|
+
Two failure modes, both of which used to surface only on the first upload:
|
|
601
|
+
|
|
602
|
+
1. The dotted path does not import — the operator followed documentation for a
|
|
603
|
+
backend whose package is absent from this image. The hint names the exact
|
|
604
|
+
requirement to install, because the S3 extra we bundle does not cover the
|
|
605
|
+
GCS/Azure backends the signed-url allow-list also recognizes.
|
|
606
|
+
2. The path imports but an S3-family backend has no ``bucket_name`` — boto3
|
|
607
|
+
raises an opaque ``ValueError: Required parameter name not set`` on save,
|
|
608
|
+
which names neither the setting nor the backend.
|
|
609
|
+
|
|
610
|
+
Runs in every environment including DEBUG: an unimportable backend is a
|
|
611
|
+
misconfiguration on a developer workstation too, and catching it at
|
|
612
|
+
``manage.py check`` is strictly better than at upload time.
|
|
613
|
+
"""
|
|
614
|
+
if not default_storage_backend:
|
|
615
|
+
return []
|
|
616
|
+
|
|
617
|
+
# ImproperlyConfigured as well as ImportError: django-storages' gcloud, azure,
|
|
618
|
+
# and dropbox modules catch their own missing bindings at import and re-raise as
|
|
619
|
+
# ImproperlyConfigured ("Could not load Google Cloud Storage bindings"), so an
|
|
620
|
+
# ImportError-only except would miss exactly the unbundled extras this check is
|
|
621
|
+
# for. Everything here is a "the configured backend cannot be loaded" failure
|
|
622
|
+
# regardless of which exception type carries it.
|
|
623
|
+
try:
|
|
624
|
+
import_string(default_storage_backend)
|
|
625
|
+
except (ImportError, ImproperlyConfigured) as exc:
|
|
626
|
+
requirement = _remediation_package(default_storage_backend)
|
|
627
|
+
hint = (
|
|
628
|
+
f"Install it with: pip install '{requirement}' (this image bundles "
|
|
629
|
+
"django-storages[s3] only), or point TRUEPPM_DEFAULT_FILE_STORAGE at a "
|
|
630
|
+
"backend the image carries."
|
|
631
|
+
if requirement
|
|
632
|
+
else (
|
|
633
|
+
"Check TRUEPPM_DEFAULT_FILE_STORAGE for a typo, or install the "
|
|
634
|
+
"package that provides this backend."
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
return [
|
|
638
|
+
Error(
|
|
639
|
+
f"STORAGES['default']['BACKEND'] is set to "
|
|
640
|
+
f"{default_storage_backend!r}, which cannot be imported: {exc}.",
|
|
641
|
+
hint=hint,
|
|
642
|
+
id="trueppm.E007",
|
|
643
|
+
)
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
if default_storage_backend in S3_STORAGE_BACKENDS and not (backend_options or {}).get(
|
|
647
|
+
"bucket_name"
|
|
648
|
+
):
|
|
649
|
+
return [
|
|
650
|
+
Error(
|
|
651
|
+
f"{default_storage_backend} is configured without a bucket name; "
|
|
652
|
+
"attachment uploads would fail with 'Required parameter name not set'.",
|
|
653
|
+
hint=(
|
|
654
|
+
"Set TRUEPPM_S3_BUCKET_NAME to the bucket that holds task "
|
|
655
|
+
"attachments. For a non-AWS endpoint (MinIO, Ceph, R2) also set "
|
|
656
|
+
"TRUEPPM_S3_ENDPOINT_URL and TRUEPPM_S3_ADDRESSING_STYLE=path."
|
|
657
|
+
),
|
|
658
|
+
id="trueppm.E008",
|
|
659
|
+
)
|
|
660
|
+
]
|
|
661
|
+
return []
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@register(Tags.security, deploy=True)
|
|
665
|
+
def check_storage_backend(
|
|
666
|
+
app_configs: Sequence[object] | None = None,
|
|
667
|
+
**kwargs: object,
|
|
668
|
+
) -> list[CheckMessage]:
|
|
669
|
+
"""Django system check entry point — resolves the live STORAGES default."""
|
|
670
|
+
from django.conf import settings
|
|
671
|
+
|
|
672
|
+
storages = getattr(settings, "STORAGES", {}) or {}
|
|
673
|
+
default = storages.get("default", {}) or {}
|
|
674
|
+
return validate_storage_backend(
|
|
675
|
+
default.get("BACKEND"),
|
|
676
|
+
backend_options=default.get("OPTIONS"),
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# ---------------------------------------------------------------------------
|
|
681
|
+
# Signed-URL backend detection — the attachment ``signed-url`` action must refuse
|
|
682
|
+
# rather than lie when the storage backend can't actually produce a time-limited,
|
|
683
|
+
# user-scoped URL (#573, MED-2 follow-up to !306's security review).
|
|
684
|
+
# ---------------------------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
#: Storage backends known to sign ``.url()`` with a query-string expiry (the
|
|
687
|
+
#: django-storages object-storage backends). Explicit allow-list, not a deny-list
|
|
688
|
+
#: of known-bad backends: an unrecognized backend (including a self-hoster's
|
|
689
|
+
#: custom one) might silently behave just like ``FileSystemStorage`` and return a
|
|
690
|
+
#: stable indefinite-lifetime path, which is exactly the misleading ``expires_at``
|
|
691
|
+
#: this check exists to prevent. Self-hosters running a signing-capable backend
|
|
692
|
+
#: not yet in this list can opt in via ``TRUEPPM_ATTACHMENT_STORAGE_SIGNS_URLS``.
|
|
693
|
+
_SIGNING_CAPABLE_STORAGE_BACKENDS = frozenset(
|
|
694
|
+
{
|
|
695
|
+
"storages.backends.s3boto3.S3Boto3Storage", # django-storages, legacy class name
|
|
696
|
+
"storages.backends.s3.S3Storage", # django-storages >=1.14
|
|
697
|
+
"storages.backends.gcloud.GoogleCloudStorage", # django-storages GCS
|
|
698
|
+
"storages.backends.azure_storage.AzureStorage", # django-storages Azure Blob
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def storage_backend_supports_signed_urls(
|
|
704
|
+
backend_path: str | None,
|
|
705
|
+
*,
|
|
706
|
+
force_signing_capable: bool = False,
|
|
707
|
+
) -> bool:
|
|
708
|
+
"""Whether ``backend_path``'s ``.url()`` produces a real time-limited signed URL.
|
|
709
|
+
|
|
710
|
+
``TaskAttachment.file.url`` is a genuine signed URL only for object-storage
|
|
711
|
+
backends that sign the query string with an expiry (S3/MinIO, GCS, Azure Blob
|
|
712
|
+
via django-storages). ``FileSystemStorage`` — and any backend this function
|
|
713
|
+
doesn't recognize — returns the same indefinite-lifetime path on every call, so
|
|
714
|
+
the ``expires_at`` the ``signed-url`` action promises would be fiction. Fails
|
|
715
|
+
closed: only an explicitly allow-listed backend is trusted; the operator opt-in
|
|
716
|
+
(``force_signing_capable``, wired to ``TRUEPPM_ATTACHMENT_STORAGE_SIGNS_URLS``)
|
|
717
|
+
exists for a signing-capable backend not yet on the list rather than silently
|
|
718
|
+
trusting an unrecognized one.
|
|
719
|
+
"""
|
|
720
|
+
if force_signing_capable:
|
|
721
|
+
return True
|
|
722
|
+
return backend_path in _SIGNING_CAPABLE_STORAGE_BACKENDS
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# Integration credential encryption key — refuse to boot in prod without a valid
|
|
727
|
+
# Fernet key (#1002). Mirrors the fail-closed posture chosen for SECRET_KEY
|
|
728
|
+
# (#566) and attachment storage (#775).
|
|
729
|
+
# ---------------------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def validate_integration_encryption_key(
|
|
733
|
+
key: str | None,
|
|
734
|
+
*,
|
|
735
|
+
debug: bool,
|
|
736
|
+
) -> list[CheckMessage]:
|
|
737
|
+
"""Return a deploy error when ``INTEGRATION_ENCRYPTION_KEY`` is missing/malformed.
|
|
738
|
+
|
|
739
|
+
The key encrypts integration PATs at rest (ADR-0049). Its only existing guard
|
|
740
|
+
is ``integrations.encryption._load_fernet``, which raises on the *first*
|
|
741
|
+
encrypt/decrypt — so a prod deploy that forgets the key boots successfully and
|
|
742
|
+
only 500s the first time a user connects a PAT, potentially long after deploy.
|
|
743
|
+
This validator moves that failure to boot time. Returns an empty list under
|
|
744
|
+
``debug`` so dev / CI keep booting with the deterministic test key.
|
|
745
|
+
"""
|
|
746
|
+
if debug:
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
if not key:
|
|
750
|
+
return [
|
|
751
|
+
Error(
|
|
752
|
+
"INTEGRATION_ENCRYPTION_KEY is empty in a non-DEBUG environment.",
|
|
753
|
+
hint=(
|
|
754
|
+
"Integration credentials cannot be encrypted without it. " + _FERNET_KEY_HINT
|
|
755
|
+
),
|
|
756
|
+
id="trueppm.E005",
|
|
757
|
+
)
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
# A truncated or garbled key must fail at boot, not at first encrypt. Fernet
|
|
761
|
+
# requires a 32-byte urlsafe-base64 key and raises ValueError otherwise.
|
|
762
|
+
from cryptography.fernet import Fernet
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
Fernet(key.encode() if isinstance(key, str) else key)
|
|
766
|
+
except (ValueError, TypeError):
|
|
767
|
+
return [
|
|
768
|
+
Error(
|
|
769
|
+
"INTEGRATION_ENCRYPTION_KEY is not a valid Fernet key "
|
|
770
|
+
"(expected 32-byte urlsafe-base64).",
|
|
771
|
+
hint=_FERNET_KEY_HINT,
|
|
772
|
+
id="trueppm.E006",
|
|
773
|
+
)
|
|
774
|
+
]
|
|
775
|
+
|
|
776
|
+
return []
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@register(Tags.security, deploy=True)
|
|
780
|
+
def check_integration_encryption_key(
|
|
781
|
+
app_configs: Sequence[object] | None = None,
|
|
782
|
+
**kwargs: object,
|
|
783
|
+
) -> list[CheckMessage]:
|
|
784
|
+
"""Django system check entry point — reads live settings."""
|
|
785
|
+
from django.conf import settings
|
|
786
|
+
|
|
787
|
+
return validate_integration_encryption_key(
|
|
788
|
+
getattr(settings, "INTEGRATION_ENCRYPTION_KEY", None),
|
|
789
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
# ---------------------------------------------------------------------------
|
|
794
|
+
# MCP program-export policy (#3014)
|
|
795
|
+
#
|
|
796
|
+
# TRUEPPM_MCP_PROGRAM_EXPORT_POLICY takes one of two words. A typo cannot be
|
|
797
|
+
# allowed to silently pick a behavior: `program_export_policy()` falls back to the
|
|
798
|
+
# restrictive `withhold` on an unrecognized value, which is the right runtime
|
|
799
|
+
# default but also means a misspelled `allwo` would look like it worked and quietly
|
|
800
|
+
# do the opposite of what the operator wrote. This check makes the typo loud at
|
|
801
|
+
# deploy time so the fallback stays a safety net rather than a place mistakes hide.
|
|
802
|
+
#
|
|
803
|
+
# Registered as an Error rather than a Warning: the setting governs a consent
|
|
804
|
+
# control, and "your security policy is not the one you wrote" is not advisory.
|
|
805
|
+
# ---------------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def validate_mcp_program_export_policy(value: object) -> list[CheckMessage]:
|
|
809
|
+
"""Validate the configured program-export policy word."""
|
|
810
|
+
from trueppm_api.apps.projects.mcp_settings import (
|
|
811
|
+
PROGRAM_EXPORT_POLICIES,
|
|
812
|
+
PROGRAM_EXPORT_WITHHOLD,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
if isinstance(value, str) and value.strip().lower() in PROGRAM_EXPORT_POLICIES:
|
|
816
|
+
return []
|
|
817
|
+
allowed = ", ".join(sorted(PROGRAM_EXPORT_POLICIES))
|
|
818
|
+
return [
|
|
819
|
+
Error(
|
|
820
|
+
f"TRUEPPM_MCP_PROGRAM_EXPORT_POLICY is {value!r}, which is not a "
|
|
821
|
+
f"recognized policy ({allowed}).",
|
|
822
|
+
hint=(
|
|
823
|
+
"The running instance falls back to "
|
|
824
|
+
f"{PROGRAM_EXPORT_WITHHOLD!r} (the safe value), so agent tokens are "
|
|
825
|
+
"being refused a program bulk export whenever a member project has "
|
|
826
|
+
"opted out of agent reads — which may not be what you intended. Fix "
|
|
827
|
+
"the value or unset it to accept the default."
|
|
828
|
+
),
|
|
829
|
+
id="trueppm.E009",
|
|
830
|
+
)
|
|
831
|
+
]
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@register(Tags.security, deploy=True)
|
|
835
|
+
def check_mcp_program_export_policy(
|
|
836
|
+
app_configs: Sequence[object] | None = None,
|
|
837
|
+
**kwargs: object,
|
|
838
|
+
) -> list[CheckMessage]:
|
|
839
|
+
"""Django system check entry point — reads the live setting."""
|
|
840
|
+
from django.conf import settings
|
|
841
|
+
|
|
842
|
+
return validate_mcp_program_export_policy(
|
|
843
|
+
getattr(settings, "TRUEPPM_MCP_PROGRAM_EXPORT_POLICY", "withhold")
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def validate_project_soft_delete_retention(value: object) -> list[CheckMessage]:
|
|
848
|
+
"""Reject a soft-delete retention window of 0 days.
|
|
849
|
+
|
|
850
|
+
``TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS`` is the one retention window
|
|
851
|
+
whose purge is irreversible and reaches user data: ``retention.run_purge``
|
|
852
|
+
computes ``cutoff = now() - timedelta(days=value)``, so 0 makes the cutoff
|
|
853
|
+
*now* and HARD-deletes every trashed project — with its tasks, dependencies,
|
|
854
|
+
sprints and baselines, via DB CASCADE — on the next tick. There is no
|
|
855
|
+
tombstone and no undo.
|
|
856
|
+
|
|
857
|
+
The API already refuses it: ``RetentionPolicyWriteSerializer.value`` is
|
|
858
|
+
``IntegerField(min_value=1)``, so the Settings → System Health save-bar
|
|
859
|
+
cannot store 0. The environment variable had no such floor, and the Helm
|
|
860
|
+
chart's own comment told operators to set exactly this value "to keep the
|
|
861
|
+
default" (#3186) — ``env.int`` parses ``"0"`` as 0, not as the default.
|
|
862
|
+
|
|
863
|
+
Deliberately scoped to this one key. The other five windows cover log-shaped
|
|
864
|
+
tables (webhooks, exports, imports, task runs, sync batches) where "keep
|
|
865
|
+
nothing" is a defensible operator choice, and rejecting 0 there would
|
|
866
|
+
crash-loop a working deployment on upgrade.
|
|
867
|
+
|
|
868
|
+
Disabling the purge entirely is a different lever and still available: turn
|
|
869
|
+
the policy off in Settings → System Health, which stores a
|
|
870
|
+
``RetentionPolicy`` row with ``enabled=False`` and resolves to ``None``.
|
|
871
|
+
"""
|
|
872
|
+
if value != 0:
|
|
873
|
+
return []
|
|
874
|
+
return [
|
|
875
|
+
Error(
|
|
876
|
+
"TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS is 0, which hard-deletes "
|
|
877
|
+
"every trashed project on the next retention purge.",
|
|
878
|
+
hint=(
|
|
879
|
+
"0 sets the purge cutoff to the present moment, so every project "
|
|
880
|
+
"in the trash — and all of its tasks, dependencies, sprints and "
|
|
881
|
+
"baselines, via CASCADE — is deleted irreversibly. Set the number "
|
|
882
|
+
"of days to retain trashed projects (the default is 30), or unset "
|
|
883
|
+
"the variable to accept that default. To stop auto-purging "
|
|
884
|
+
"entirely, leave the variable alone and disable the policy in "
|
|
885
|
+
"Settings → System Health instead."
|
|
886
|
+
),
|
|
887
|
+
id="trueppm.E010",
|
|
888
|
+
)
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@register(Tags.security, deploy=True)
|
|
893
|
+
def check_project_soft_delete_retention(
|
|
894
|
+
app_configs: Sequence[object] | None = None,
|
|
895
|
+
**kwargs: object,
|
|
896
|
+
) -> list[CheckMessage]:
|
|
897
|
+
"""Django system check entry point — reads the live setting."""
|
|
898
|
+
from django.conf import settings
|
|
899
|
+
|
|
900
|
+
return validate_project_soft_delete_retention(
|
|
901
|
+
getattr(settings, "TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS", 30)
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def validate_allowed_hosts(
|
|
906
|
+
hosts: Sequence[str] | None,
|
|
907
|
+
*,
|
|
908
|
+
debug: bool,
|
|
909
|
+
allow_wildcard: bool,
|
|
910
|
+
) -> list[CheckMessage]:
|
|
911
|
+
"""Reject a wildcard ``ALLOWED_HOSTS`` in a non-DEBUG deployment (#3515).
|
|
912
|
+
|
|
913
|
+
``ALLOWED_HOSTS`` is the *only* enforcement point Django applies to the host
|
|
914
|
+
a request claims: ``HttpRequest.get_host()`` validates its result against
|
|
915
|
+
this list before any view runs, whichever header the value came from. A bare
|
|
916
|
+
``*`` disables that check entirely, so ``request.build_absolute_uri()`` — the
|
|
917
|
+
OIDC ``redirect_uri`` fallback and the inbound Git-webhook URL an admin
|
|
918
|
+
pastes into GitHub/GitLab — will render whatever host the caller asked for.
|
|
919
|
+
|
|
920
|
+
Why this guard exists next to the ``USE_X_FORWARDED_HOST`` decision rather
|
|
921
|
+
than on its own: that setting only changes *which* header supplies the host,
|
|
922
|
+
and both candidates are bounded by this list. ``*`` removes the bound for
|
|
923
|
+
every posture at once, which makes it the load-bearing control of the two.
|
|
924
|
+
|
|
925
|
+
Wildcard *subdomains* (``.example.com``) are deliberately allowed: they still
|
|
926
|
+
constrain the host to a suffix the operator chose, and self-hosters running
|
|
927
|
+
per-tenant subdomains have a legitimate need for them. Only the bare ``*``,
|
|
928
|
+
which constrains nothing, is refused.
|
|
929
|
+
|
|
930
|
+
Refusal rather than a warning, with a named opt-out, follows
|
|
931
|
+
``TRUEPPM_ALLOW_UNENCRYPTED_DB``: prod forces single-line JSON logs, so a
|
|
932
|
+
warning becomes one indexed record nobody reads, while a hard refusal with no
|
|
933
|
+
escape would break an existing install on upgrade with no remedy shorter than
|
|
934
|
+
a redeploy.
|
|
935
|
+
"""
|
|
936
|
+
if debug or allow_wildcard:
|
|
937
|
+
return []
|
|
938
|
+
if not any(host == "*" for host in hosts or ()):
|
|
939
|
+
return []
|
|
940
|
+
return [
|
|
941
|
+
Error(
|
|
942
|
+
"ALLOWED_HOSTS contains the wildcard '*', which disables host "
|
|
943
|
+
"validation entirely in a non-DEBUG deployment.",
|
|
944
|
+
hint=(
|
|
945
|
+
"'*' is the only value that removes the bound on the host Django "
|
|
946
|
+
"reports, so absolute URLs the API builds — the OIDC redirect_uri "
|
|
947
|
+
"and the inbound Git-webhook URL — follow whatever host the caller "
|
|
948
|
+
"sends. List the names your deployment actually answers on "
|
|
949
|
+
"instead: your public hostname, plus the in-cluster Service name "
|
|
950
|
+
"and 'localhost' if you use `helm test` or `kubectl port-forward`. "
|
|
951
|
+
"A wildcard subdomain ('.example.com') is accepted and is the "
|
|
952
|
+
"supported way to serve many hosts. If you must keep '*' because "
|
|
953
|
+
"the host set is genuinely unknowable, set "
|
|
954
|
+
"TRUEPPM_ALLOW_WILDCARD_HOSTS=true to acknowledge it."
|
|
955
|
+
),
|
|
956
|
+
id="trueppm.E012",
|
|
957
|
+
)
|
|
958
|
+
]
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@register(Tags.security, deploy=True)
|
|
962
|
+
def check_allowed_hosts(
|
|
963
|
+
app_configs: Sequence[object] | None = None,
|
|
964
|
+
**kwargs: object,
|
|
965
|
+
) -> list[CheckMessage]:
|
|
966
|
+
"""Django system check entry point — reads the live setting."""
|
|
967
|
+
from django.conf import settings
|
|
968
|
+
|
|
969
|
+
return validate_allowed_hosts(
|
|
970
|
+
getattr(settings, "ALLOWED_HOSTS", None),
|
|
971
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
972
|
+
allow_wildcard=bool(getattr(settings, "ALLOW_WILDCARD_ALLOWED_HOSTS", False)),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
# ---------------------------------------------------------------------------
|
|
977
|
+
# FRONTEND_BASE_URL — warn (not refuse) when unset in prod (#4188).
|
|
978
|
+
#
|
|
979
|
+
# Every outbound email that used to build an absolute link from this setting
|
|
980
|
+
# already degrades gracefully when it is empty: blocker/task deep-links, the
|
|
981
|
+
# password-reset link, and the mention-notification footer all omit the link
|
|
982
|
+
# rather than emit a relative path that resolves nowhere. Workspace-invite and
|
|
983
|
+
# export-ready emails did not — they emitted a bare relative path
|
|
984
|
+
# (``/invite/accept?token=...``, ``/settings/workspace/danger``) that no mail
|
|
985
|
+
# client can follow, which this issue's ``_render_invite_email`` /
|
|
986
|
+
# ``_render_export_email`` fix closes. This check is the boot-time half: an
|
|
987
|
+
# empty value is a legitimate zero-config dev default, so it is a Warning, not
|
|
988
|
+
# an Error — refusing to boot over it would break every existing
|
|
989
|
+
# self-hosted single-origin install that never configured it and has managed
|
|
990
|
+
# fine on the in-app/SSO fallbacks. What it cannot do is warn an operator who
|
|
991
|
+
# never runs `manage.py check --deploy`, which is why the invite email's own
|
|
992
|
+
# copy (above) repeats the fix inline for the one recipient who will actually
|
|
993
|
+
# read it.
|
|
994
|
+
# ---------------------------------------------------------------------------
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def validate_frontend_base_url(base_url: str | None, *, debug: bool) -> list[CheckMessage]:
|
|
998
|
+
"""Warn when ``FRONTEND_BASE_URL`` is empty outside DEBUG.
|
|
999
|
+
|
|
1000
|
+
Returns an empty list under ``debug`` so developer workstations — which
|
|
1001
|
+
routinely run without a configured public origin — keep booting silently.
|
|
1002
|
+
"""
|
|
1003
|
+
if debug or base_url:
|
|
1004
|
+
return []
|
|
1005
|
+
return [
|
|
1006
|
+
CheckWarning(
|
|
1007
|
+
"TRUEPPM_FRONTEND_BASE_URL is empty in a non-DEBUG environment.",
|
|
1008
|
+
hint=(
|
|
1009
|
+
"Workspace-invite emails cannot include an accept link without it "
|
|
1010
|
+
"— an invited user has no account yet and no other way to reach "
|
|
1011
|
+
"the link, so any invite sent while this is unset must be resent "
|
|
1012
|
+
"once it is fixed. Password-reset, export-ready, and "
|
|
1013
|
+
"mention-notification emails also degrade to plain prose instead "
|
|
1014
|
+
"of a clickable link. Set TRUEPPM_FRONTEND_BASE_URL to the public "
|
|
1015
|
+
"origin the web app is served from, e.g. "
|
|
1016
|
+
"https://trueppm.example.com."
|
|
1017
|
+
),
|
|
1018
|
+
id="trueppm.W001",
|
|
1019
|
+
)
|
|
1020
|
+
]
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
@register(Tags.security, deploy=True)
|
|
1024
|
+
def check_frontend_base_url(
|
|
1025
|
+
app_configs: Sequence[object] | None = None,
|
|
1026
|
+
**kwargs: object,
|
|
1027
|
+
) -> list[CheckMessage]:
|
|
1028
|
+
"""Django system check entry point — reads live settings."""
|
|
1029
|
+
from django.conf import settings
|
|
1030
|
+
|
|
1031
|
+
return validate_frontend_base_url(
|
|
1032
|
+
getattr(settings, "FRONTEND_BASE_URL", None),
|
|
1033
|
+
debug=bool(getattr(settings, "DEBUG", False)),
|
|
1034
|
+
)
|