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.
Files changed (799) hide show
  1. trueppm_api/__init__.py +27 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. 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
+ )