trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
"""Cookie-based JWT auth views (#897).
|
|
2
|
+
|
|
3
|
+
These views replace the stock simplejwt token endpoints so the *refresh* token
|
|
4
|
+
is delivered to the browser in an ``httpOnly`` cookie scoped to the refresh path
|
|
5
|
+
instead of in the JSON response body. The access token is still returned in the
|
|
6
|
+
JSON body and held in memory by the SPA.
|
|
7
|
+
|
|
8
|
+
Why httpOnly for the refresh token:
|
|
9
|
+
A refresh token is long-lived and exchangeable for access tokens, so it is
|
|
10
|
+
the high-value credential. Persisting it where JavaScript can read it
|
|
11
|
+
(localStorage / a JS-visible cookie) means any XSS that runs in the SPA can
|
|
12
|
+
exfiltrate it and impersonate the user past the lifetime of any single
|
|
13
|
+
access token. An ``httpOnly`` cookie is unreadable from JavaScript, so an
|
|
14
|
+
XSS payload can ride the current session but cannot steal the long-lived
|
|
15
|
+
credential. The short-lived access token stays in memory (not localStorage)
|
|
16
|
+
so it dies with the tab.
|
|
17
|
+
|
|
18
|
+
CSRF posture:
|
|
19
|
+
The refresh cookie uses ``SameSite=Strict`` (by default — see
|
|
20
|
+
``core/refresh_cookie_policy.py``) and is ``Path``-scoped to the refresh
|
|
21
|
+
endpoint only. ``SameSite=Strict`` means the browser never attaches the
|
|
22
|
+
cookie to a cross-site request, so a forged request from an attacker's
|
|
23
|
+
origin cannot trigger a refresh on the victim's behalf. The refresh endpoint
|
|
24
|
+
is otherwise unauthenticated-by-cookie (it reads the refresh token solely
|
|
25
|
+
from this cookie), and a successful refresh only mints a new short-lived
|
|
26
|
+
access token returned in the response body — which a cross-site attacker
|
|
27
|
+
cannot read (CORS). There is therefore no additional CSRF *token* required
|
|
28
|
+
on the refresh path. Login and logout are likewise safe against a *missing*
|
|
29
|
+
CSRF token: login carries no ambient credential, and logout is idempotent
|
|
30
|
+
(clearing a cookie + best-effort blacklist) with no cross-site state-change
|
|
31
|
+
value beyond revoking the caller's own session.
|
|
32
|
+
|
|
33
|
+
SameSite is nonetheless a **single, operator-configurable** control (#3556):
|
|
34
|
+
``TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITE`` can be relaxed to ``Lax`` or
|
|
35
|
+
``None`` for a split-origin/framed deploy, and at ``None`` the cookie rides
|
|
36
|
+
on every cross-site request. :func:`_is_cross_site_auth_request` is defense
|
|
37
|
+
in depth for exactly that case: it independently refuses a refresh/logout
|
|
38
|
+
request whose ``Sec-Fetch-Site`` is ``cross-site``, or whose ``Origin`` is
|
|
39
|
+
present and neither same-origin nor in ``CSRF_TRUSTED_ORIGINS`` — so a
|
|
40
|
+
relaxed SameSite policy is not the *only* thing standing between a forged
|
|
41
|
+
cross-site page and these endpoints. A request that sends neither header
|
|
42
|
+
(the mobile client, a script, curl) is not treated as cross-site; see that
|
|
43
|
+
function's docstring for why refusing them would be the wrong trade.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import contextlib
|
|
49
|
+
import hashlib
|
|
50
|
+
import ipaddress
|
|
51
|
+
import logging
|
|
52
|
+
from collections.abc import Sequence
|
|
53
|
+
from datetime import timedelta
|
|
54
|
+
from typing import Any, TypeGuard, cast
|
|
55
|
+
from urllib.parse import urlsplit
|
|
56
|
+
|
|
57
|
+
from django.conf import settings
|
|
58
|
+
from django.contrib.auth import get_user_model
|
|
59
|
+
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
60
|
+
from django.http import HttpRequest
|
|
61
|
+
from django.utils import timezone
|
|
62
|
+
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
|
63
|
+
from rest_framework import serializers, status
|
|
64
|
+
from rest_framework.exceptions import AuthenticationFailed, Throttled
|
|
65
|
+
from rest_framework.request import Request
|
|
66
|
+
from rest_framework.response import Response
|
|
67
|
+
from rest_framework.throttling import ScopedRateThrottle
|
|
68
|
+
from rest_framework.views import APIView
|
|
69
|
+
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
|
70
|
+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
|
71
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
72
|
+
from rest_framework_simplejwt.views import TokenObtainPairView
|
|
73
|
+
|
|
74
|
+
from trueppm_api.core.throttling import LoginAccountRateThrottle, resolve_client_ident
|
|
75
|
+
|
|
76
|
+
# Auth-domain logger. Failed-login attempts are emitted here as a structured
|
|
77
|
+
# WARNING so self-hosting operators can alarm on brute-force / credential-stuffing
|
|
78
|
+
# spikes (e.g. ship trueppm.auth to a SIEM and alert on a burst). Reuses the same
|
|
79
|
+
# named logger as the password-reset flow so all auth events land on one channel.
|
|
80
|
+
logger = logging.getLogger("trueppm.auth")
|
|
81
|
+
|
|
82
|
+
#: The active user model. Bound once at module scope so the login path's
|
|
83
|
+
#: email resolution and its timing-equalizing dummy hash both go through the
|
|
84
|
+
#: same class the authentication backend uses.
|
|
85
|
+
User = get_user_model()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _client_ip(request: HttpRequest | Request) -> str:
|
|
89
|
+
"""Best-effort client IP for the audit event.
|
|
90
|
+
|
|
91
|
+
The address the per-IP throttles key on (``core.throttling.resolve_client_ident``),
|
|
92
|
+
not the left-most ``X-Forwarded-For`` entry, which the client writes (#4023). So
|
|
93
|
+
``auth.login_failed`` names the address the login throttle actually charged, and
|
|
94
|
+
a brute-forcer cannot pick what the forensic record says.
|
|
95
|
+
|
|
96
|
+
**The result is parsed as an IP address and replaced with ``"invalid"`` if it is
|
|
97
|
+
not one** (#3552). At ``NUM_PROXIES = 1`` with a single-entry header the resolved
|
|
98
|
+
value is still whatever that one entry says, and this field sits before another
|
|
99
|
+
in a space-delimited ``key=value`` line, so an unvalidated value containing spaces and ``=
|
|
100
|
+
lets a caller inject extra pairs into the record:
|
|
101
|
+
|
|
102
|
+
X-Forwarded-For: 1.2.3.4 user_id=1 method=password
|
|
103
|
+
|
|
104
|
+
A last-wins logfmt/Splunk-kv extractor then attributes the session to a different
|
|
105
|
+
account. Raw CR/LF cannot reach here (the HTTP parser rejects them) so a wholly
|
|
106
|
+
forged line was never possible, but intra-line field forgery was. Validating here
|
|
107
|
+
fixes ``auth.login_failed`` — reachable *without* credentials, and so the worse of
|
|
108
|
+
the two — at the same time.
|
|
109
|
+
"""
|
|
110
|
+
candidate = resolve_client_ident(request)
|
|
111
|
+
if not candidate:
|
|
112
|
+
return "unknown"
|
|
113
|
+
try:
|
|
114
|
+
return str(ipaddress.ip_address(candidate))
|
|
115
|
+
except ValueError:
|
|
116
|
+
return "invalid"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _origin_matches_trusted(origin: str, trusted_origins: Sequence[str]) -> bool:
|
|
120
|
+
"""Whether ``origin`` matches an entry in ``CSRF_TRUSTED_ORIGINS`` (#3556).
|
|
121
|
+
|
|
122
|
+
Mirrors Django's own matching rules for that setting: an exact
|
|
123
|
+
``scheme://host[:port]`` match, or a ``https://*.example.com``-style entry
|
|
124
|
+
(a leading ``*.`` wildcard segment) matching any subdomain of
|
|
125
|
+
``example.com`` on that scheme. Case-sensitive on the host is intentional —
|
|
126
|
+
hostnames arrive lower-cased from both the browser and ``CSRF_TRUSTED_ORIGINS``
|
|
127
|
+
in practice, and DNS names are case-insensitive but this list is operator-typed
|
|
128
|
+
configuration, not attacker input, so normalizing case here would hide a typo
|
|
129
|
+
rather than tolerate one.
|
|
130
|
+
"""
|
|
131
|
+
for allowed in trusted_origins:
|
|
132
|
+
if origin == allowed:
|
|
133
|
+
return True
|
|
134
|
+
scheme, _, host_pattern = allowed.partition("://")
|
|
135
|
+
if not host_pattern.startswith("*."):
|
|
136
|
+
continue
|
|
137
|
+
suffix = host_pattern[1:] # "*.example.com" -> ".example.com"
|
|
138
|
+
origin_scheme, _, origin_host = origin.partition("://")
|
|
139
|
+
if origin_scheme == scheme and origin_host.endswith(suffix) and origin_host != suffix:
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _is_same_origin(request: Request, origin: str) -> bool:
|
|
145
|
+
"""Whether an ``Origin`` header value names this request's own origin (#3556)."""
|
|
146
|
+
parsed = urlsplit(origin)
|
|
147
|
+
if not parsed.scheme or not parsed.netloc:
|
|
148
|
+
# Not a well-formed "scheme://host" origin — never treat it as a match.
|
|
149
|
+
return False
|
|
150
|
+
return parsed.scheme == request.scheme and parsed.netloc == request.get_host()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_cross_site_auth_request(request: Request) -> bool:
|
|
154
|
+
"""Whether a refresh/logout POST carries a browser-supplied cross-site signal.
|
|
155
|
+
|
|
156
|
+
Defense in depth for #3556: ``SameSite`` on the refresh cookie is the
|
|
157
|
+
unconditional CSRF control, but it is operator-configurable
|
|
158
|
+
(``TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITE``) and can be relaxed to ``Lax`` or
|
|
159
|
+
``None`` for a split-origin/framed deploy. This check is independent of that
|
|
160
|
+
setting — it refuses on either of two signals, checked in order:
|
|
161
|
+
|
|
162
|
+
1. ``Sec-Fetch-Site: cross-site`` — the Fetch Metadata header every current
|
|
163
|
+
mainstream browser sends, and unambiguous: the browser itself is
|
|
164
|
+
reporting that the request's initiator is a different site. Values other
|
|
165
|
+
than exactly ``"cross-site"`` (``same-origin``, ``same-site``, ``none``,
|
|
166
|
+
or the header absent) are not refused here.
|
|
167
|
+
2. An ``Origin`` header that is present and names neither this request's own
|
|
168
|
+
origin nor an entry in ``CSRF_TRUSTED_ORIGINS`` (for a reverse proxy that
|
|
169
|
+
rewrites ``Origin``/``Referer``, same as the CSRF middleware's own use of
|
|
170
|
+
that setting).
|
|
171
|
+
|
|
172
|
+
**Absence of both headers is deliberately NOT treated as cross-site.** A
|
|
173
|
+
non-browser caller — the mobile app, a server-to-server integration, a
|
|
174
|
+
script using curl/httpx — sends neither Fetch Metadata nor (usually) an
|
|
175
|
+
``Origin`` header, and these two endpoints are unauthenticated-by-anything-
|
|
176
|
+
but-the-cookie by design (the access token may already have expired). Failing
|
|
177
|
+
such a request closed would lock out every legitimate non-browser client
|
|
178
|
+
while adding no protection — an attacker forging a cross-site *browser*
|
|
179
|
+
request cannot suppress the headers a real cross-site fetch/form-post
|
|
180
|
+
triggers, but a same-origin, header-free tool is simply not a CSRF actor.
|
|
181
|
+
This function is therefore a browser-specific backstop layered on top of
|
|
182
|
+
SameSite, never the sole gate.
|
|
183
|
+
"""
|
|
184
|
+
if request.META.get("HTTP_SEC_FETCH_SITE") == "cross-site":
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
origin = request.META.get("HTTP_ORIGIN")
|
|
188
|
+
if not origin:
|
|
189
|
+
return False
|
|
190
|
+
if _is_same_origin(request, origin):
|
|
191
|
+
return False
|
|
192
|
+
trusted_origins = getattr(settings, "CSRF_TRUSTED_ORIGINS", None) or []
|
|
193
|
+
return not _origin_matches_trusted(origin, trusted_origins)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _login_body(request_data: Any) -> dict[str, Any]:
|
|
197
|
+
"""Return the login body as a dict, or an empty one.
|
|
198
|
+
|
|
199
|
+
A non-object JSON body (list/str/scalar) leaves ``request.data`` without a
|
|
200
|
+
``.get``; every helper here goes through this guard so no path can turn a
|
|
201
|
+
malformed login into a 500 (#2126). Returning ``{}`` rather than ``None`` keeps
|
|
202
|
+
each caller's own logic single-branch.
|
|
203
|
+
"""
|
|
204
|
+
return request_data if isinstance(request_data, dict) else {}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _submitted_identifier(request_data: Any) -> str | None:
|
|
208
|
+
"""Return the trimmed ``username`` field from a login body, or None.
|
|
209
|
+
|
|
210
|
+
A non-string value is refused rather than coerced — ``str(...)`` on a dict or
|
|
211
|
+
list would mint an identifier nobody typed.
|
|
212
|
+
"""
|
|
213
|
+
raw = _login_body(request_data).get("username")
|
|
214
|
+
return raw.strip() if isinstance(raw, str) else None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _looks_like_email(identifier: str | None) -> TypeGuard[str]:
|
|
218
|
+
"""True when ``identifier`` is worth retrying as an email address.
|
|
219
|
+
|
|
220
|
+
Deliberately just an ``@`` test rather than validation. Two things it buys, and
|
|
221
|
+
the second is the load-bearing one:
|
|
222
|
+
|
|
223
|
+
* the ordinary username path never pays for the extra query, because a
|
|
224
|
+
username without an ``@`` never reaches the fallback at all; and
|
|
225
|
+
* the empty string can never reach ``email__iexact``. ``User.email`` is
|
|
226
|
+
``blank=True`` on Django's default model — SSO- and admin-created accounts
|
|
227
|
+
routinely have ``""`` — so an ``email__iexact=""`` lookup would match every
|
|
228
|
+
account that has no email at all, and on an install with exactly one such
|
|
229
|
+
account it would hand an attacker a working identifier for it.
|
|
230
|
+
"""
|
|
231
|
+
return identifier is not None and "@" in identifier
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _resolve_email_identifier(identifier: str) -> str | None:
|
|
235
|
+
"""Resolve an email address to the username of the one account holding it.
|
|
236
|
+
|
|
237
|
+
Returns ``None`` whenever the caller must NOT be retried under a different
|
|
238
|
+
identifier. Django's ``User.email`` carries no uniqueness constraint, so the
|
|
239
|
+
ambiguous case is the dangerous one and it fails closed — mirroring the
|
|
240
|
+
``email__iexact`` branch in ``apps.sso.services.resolve_user``, which refuses
|
|
241
|
+
an ambiguous match for the same reason: silently picking one of two accounts
|
|
242
|
+
signs the caller in as an account they did not name.
|
|
243
|
+
"""
|
|
244
|
+
matches = list(
|
|
245
|
+
User.objects.filter(email__iexact=identifier)
|
|
246
|
+
.order_by("pk")
|
|
247
|
+
.values_list("username", flat=True)[:2]
|
|
248
|
+
)
|
|
249
|
+
if len(matches) != 1:
|
|
250
|
+
# 0 → nothing to retry. 2+ → ambiguous, fail closed (see docstring).
|
|
251
|
+
return None
|
|
252
|
+
username = str(matches[0])
|
|
253
|
+
if username.lower() == identifier.lower():
|
|
254
|
+
# The account's username IS this string, so the first attempt already tried
|
|
255
|
+
# it against this exact account and it failed. Retrying would spend a second
|
|
256
|
+
# password comparison to reach the identical answer.
|
|
257
|
+
return None
|
|
258
|
+
return username
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _burn_equivalent_password_hash(request_data: Any) -> None:
|
|
262
|
+
"""Spend one password-hash's worth of work on the email-fallback miss path.
|
|
263
|
+
|
|
264
|
+
Without this the endpoint is an email-existence oracle. ``ModelBackend`` already
|
|
265
|
+
equalizes its own username miss (it runs ``UserModel().set_password(password)``
|
|
266
|
+
so a nonexistent user costs the same as a wrong password), but the email
|
|
267
|
+
fallback adds a *second* real comparison on the hit path only — so "this email
|
|
268
|
+
has an account" would measure as roughly twice the hash time of "it does not",
|
|
269
|
+
and the dominant term in a login response is the hasher, not the query.
|
|
270
|
+
|
|
271
|
+
Called on every ``@``-shaped identifier whose fallback did not run, so all
|
|
272
|
+
``@``-shaped failures cost two hashes and are indistinguishable from each other.
|
|
273
|
+
This equalizes the dominant term, not every nanosecond — the extra ``email__iexact``
|
|
274
|
+
query on some paths is ~1ms against ~100ms of PBKDF2, well under the noise floor
|
|
275
|
+
of a network round trip. The cost is that a failed ``@``-shaped login burns two
|
|
276
|
+
hashes instead of one; both login throttles bound that, and it is the same trade
|
|
277
|
+
Django itself already makes for the username miss.
|
|
278
|
+
"""
|
|
279
|
+
password = _login_body(request_data).get("password")
|
|
280
|
+
# The User() here is disposable — never saved, discarded on return — so there is
|
|
281
|
+
# no unvalidated credential being persisted; this mirrors ModelBackend's own
|
|
282
|
+
# UserModel().set_password(password) timing-equalization decoy.
|
|
283
|
+
# nosemgrep: unvalidated-password
|
|
284
|
+
User().set_password(password if isinstance(password, str) else "")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _emit_login_failure_event(request: Request, canonical_identifier: str | None = None) -> None:
|
|
288
|
+
"""Emit an auth-failure audit event for a rejected login (#1717).
|
|
289
|
+
|
|
290
|
+
The attempted username is hashed (never logged in the clear) so the event is
|
|
291
|
+
correlatable across attempts — an operator can see that one account is being
|
|
292
|
+
hammered from many IPs — without writing raw credentials/emails into logs.
|
|
293
|
+
|
|
294
|
+
``canonical_identifier`` is the account's own username, passed when the attempt
|
|
295
|
+
arrived as an email and the view resolved it (#3468). Hashing the *resolved*
|
|
296
|
+
identifier is what keeps that correlation working: without it one account
|
|
297
|
+
produces two distinct hashes depending on which of its identifiers was typed,
|
|
298
|
+
and an operator alarming on "one account, many IPs" undercounts by exactly the
|
|
299
|
+
split.
|
|
300
|
+
"""
|
|
301
|
+
# A non-object JSON body (list/str/scalar) leaves ``request.data`` without a
|
|
302
|
+
# ``.get``; guard on the dict shape so audit emission can never turn a rejected
|
|
303
|
+
# login into a 500 (#2126). A non-object body simply has no username → "unknown".
|
|
304
|
+
emit_login_failure(
|
|
305
|
+
request, identifier=canonical_identifier or _submitted_identifier(request.data)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def emit_login_failure(request: HttpRequest | Request, *, identifier: str | None) -> None:
|
|
310
|
+
"""Emit ``auth.login_failed`` for a rejected login on any door.
|
|
311
|
+
|
|
312
|
+
The counterpart to :func:`emit_login_success`, and public for the same reason
|
|
313
|
+
(#3557): the Django admin is a second password door, and an operator alarming
|
|
314
|
+
on credential stuffing must see its refusals on the same channel, in the same
|
|
315
|
+
shape, as the API's. A door that emitted its own differently-spelled line would
|
|
316
|
+
be invisible to the rule ``auth.login_failed`` exists to feed.
|
|
317
|
+
|
|
318
|
+
``identifier`` is the submitted (or view-resolved) username; it is hashed here
|
|
319
|
+
and never logged in the clear. ``request`` may be a plain Django ``HttpRequest``
|
|
320
|
+
— only ``META`` is read, for the best-effort client IP.
|
|
321
|
+
"""
|
|
322
|
+
username_hash = (
|
|
323
|
+
hashlib.sha256(str(identifier).strip().lower().encode("utf-8")).hexdigest()
|
|
324
|
+
if identifier
|
|
325
|
+
else "unknown"
|
|
326
|
+
)
|
|
327
|
+
logger.warning(
|
|
328
|
+
"auth.login_failed username_hash=%s client_ip=%s",
|
|
329
|
+
username_hash,
|
|
330
|
+
_client_ip(request),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def emit_login_success(
|
|
335
|
+
request: HttpRequest | Request, *, user: Any, method: str, remember: bool
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Emit the structured login-success line for a session that was actually minted.
|
|
338
|
+
|
|
339
|
+
The counterpart to :func:`_emit_login_failure_event` (#3552, ADR-1120). Both land on
|
|
340
|
+
``trueppm.auth`` — including the SSO callback's success, which imports this rather
|
|
341
|
+
than logging to ``trueppm.sso`` beside the SSO *refusals*. Correlating "who got in"
|
|
342
|
+
must not require knowing which door they used.
|
|
343
|
+
|
|
344
|
+
**Call this last, once the session exists — not when credentials validate.** The two
|
|
345
|
+
are different moments: :class:`CookieTokenObtainPairView` runs the enterprise
|
|
346
|
+
``local_login_allowed`` seam *after* validation, and that seam can still return a 403
|
|
347
|
+
with no cookie. A line emitted at the validation point therefore reports a success
|
|
348
|
+
for a request that was refused, which inverts exactly the signal an operator alarms
|
|
349
|
+
on. The same applies to the SSO callback: after the refresh cookie is set, not after
|
|
350
|
+
``resolve_user`` returns.
|
|
351
|
+
|
|
352
|
+
``INFO``, not ``WARNING`` — a successful login is not an anomaly, and putting it at
|
|
353
|
+
``WARNING`` beside ``auth.login_failed`` would poison the alerting rule that line
|
|
354
|
+
exists to feed. ``DJANGO_LOG_LEVEL`` defaults to ``INFO``, so the line is visible in a
|
|
355
|
+
default production deploy; ``settings/dev.py`` replaces ``LOGGING`` with a root
|
|
356
|
+
handler at ``WARNING``, so it is not visible under dev settings (which is why tests
|
|
357
|
+
raise the level explicitly).
|
|
358
|
+
|
|
359
|
+
Fields go in the message rather than in ``extra=`` because the dev console formatter
|
|
360
|
+
renders only ``%(message)s``: an ``extra``-only field would exist in the production
|
|
361
|
+
JSON handler and nowhere else, and a record whose content depends on the deployment is
|
|
362
|
+
worse than one that is uniformly greppable.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
request: The request that established the session (read only for the client IP).
|
|
366
|
+
user: The authenticated user. Only ``pk`` is logged — never the email or
|
|
367
|
+
username, matching ``_emit_login_failure_event``, which hashes the submitted
|
|
368
|
+
identifier rather than writing it in the clear.
|
|
369
|
+
method: ``"password"``, ``"sso:<provider-slug>"``, or ``"admin"`` (the
|
|
370
|
+
hardened Django admin login, #3557).
|
|
371
|
+
remember: Whether the session opted into browser-persistent "remember me". SSO
|
|
372
|
+
logins are always ``False`` (an IdP redirect carries no such choice).
|
|
373
|
+
"""
|
|
374
|
+
logger.info(
|
|
375
|
+
"auth.login_succeeded user_id=%s method=%s client_ip=%s remember=%s",
|
|
376
|
+
getattr(user, "pk", None),
|
|
377
|
+
method,
|
|
378
|
+
_client_ip(request),
|
|
379
|
+
remember,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _set_refresh_cookie(
|
|
384
|
+
response: Response, refresh_token: str, *, persistent_seconds: int | None
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Attach the refresh token to ``response`` as a hardened httpOnly cookie.
|
|
387
|
+
|
|
388
|
+
Cookie attributes are driven by settings so a non-HTTPS local dev server can
|
|
389
|
+
still complete the flow (``AUTH_REFRESH_COOKIE_SECURE=False``) while
|
|
390
|
+
production defaults to ``Secure``. The cookie is ``Path``-scoped to
|
|
391
|
+
``/api/v1/auth/`` so it is never sent on ordinary API calls — only the auth
|
|
392
|
+
endpoints that must read it (refresh and logout) receive it. Scoping it to the
|
|
393
|
+
refresh endpoint alone silently broke logout revocation (#2999).
|
|
394
|
+
|
|
395
|
+
``persistent_seconds`` controls browser *persistence* (#2246): an int sets a
|
|
396
|
+
``Max-Age`` so the cookie survives browser close ("remember me"); ``None``
|
|
397
|
+
emits no ``Max-Age``/``Expires`` → a **session cookie** that the browser drops
|
|
398
|
+
on close (the not-remembered / shared-machine default). This is independent of
|
|
399
|
+
the token's own ``exp`` (the real credential bound), which the caller sets via
|
|
400
|
+
:func:`_apply_remember`.
|
|
401
|
+
"""
|
|
402
|
+
response.set_cookie(
|
|
403
|
+
key=settings.AUTH_REFRESH_COOKIE_NAME,
|
|
404
|
+
value=refresh_token,
|
|
405
|
+
max_age=persistent_seconds,
|
|
406
|
+
httponly=True,
|
|
407
|
+
secure=settings.AUTH_REFRESH_COOKIE_SECURE,
|
|
408
|
+
samesite=settings.AUTH_REFRESH_COOKIE_SAMESITE,
|
|
409
|
+
path=settings.AUTH_REFRESH_COOKIE_PATH,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _cookie_seconds(remember: bool) -> int | None:
|
|
414
|
+
"""Cookie ``Max-Age`` for a remember choice: persistent 30d, or session (None)."""
|
|
415
|
+
if remember:
|
|
416
|
+
return int(settings.REFRESH_TOKEN_REMEMBER_LIFETIME.total_seconds())
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _record_outstanding(refresh: RefreshToken) -> None:
|
|
421
|
+
"""Upsert the ``OutstandingToken`` bookkeeping row for ``refresh``.
|
|
422
|
+
|
|
423
|
+
This row is what makes a refresh token *revocable*. ``revoke_all_refresh_tokens``
|
|
424
|
+
(the "sign out every device" primitive behind password reset and off-boarding)
|
|
425
|
+
iterates ``OutstandingToken`` and blacklists each row, so a live token with no
|
|
426
|
+
row cannot be revoked by any server-side action.
|
|
427
|
+
|
|
428
|
+
Two distinct cases, which is why this is an upsert rather than an update (#2999):
|
|
429
|
+
|
|
430
|
+
* **Login** — ``RefreshToken.for_user`` already wrote a row, but its
|
|
431
|
+
``expires_at`` was copied from the token's ``exp`` *at creation*: the 7-day
|
|
432
|
+
class default, before :func:`_apply_remember` overrides it. ``expires_at``
|
|
433
|
+
drives the nightly ``flushexpiredtokens`` cleanup and ``BlacklistedToken``
|
|
434
|
+
cascades on the ``OutstandingToken``, so a row left at 7 days for a 30-day
|
|
435
|
+
token would have its revocation record flushed on day 7 while the JWT stayed
|
|
436
|
+
valid to day 30 → **replayable between day 7 and day 30**.
|
|
437
|
+
* **Rotation** — ``refresh.set_jti()`` mints a new jti and simplejwt records
|
|
438
|
+
nothing for it (``outstand()`` is called nowhere in simplejwt's rotation
|
|
439
|
+
path). Before #2999 the row was therefore missing for precisely the token
|
|
440
|
+
that was live, so ``revoke_all_refresh_tokens`` walked a set of
|
|
441
|
+
already-blacklisted rows, returned a plausible non-zero count, and left the
|
|
442
|
+
real session usable. A password reset did not evict an attacker.
|
|
443
|
+
|
|
444
|
+
``created_at`` is only written on insert, so a re-issued row keeps its original
|
|
445
|
+
mint time. No-op when the blacklist app is absent (lean deploy degrades to
|
|
446
|
+
TTL-only expiry) or when the token carries no jti/user claim.
|
|
447
|
+
"""
|
|
448
|
+
if "rest_framework_simplejwt.token_blacklist" not in settings.INSTALLED_APPS:
|
|
449
|
+
return
|
|
450
|
+
from rest_framework_simplejwt.settings import api_settings
|
|
451
|
+
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
|
|
452
|
+
from rest_framework_simplejwt.utils import datetime_from_epoch
|
|
453
|
+
|
|
454
|
+
jti = refresh.payload.get(api_settings.JTI_CLAIM)
|
|
455
|
+
user_id = refresh.payload.get(api_settings.USER_ID_CLAIM)
|
|
456
|
+
if jti is None or user_id is None:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
OutstandingToken.objects.update_or_create(
|
|
460
|
+
jti=jti,
|
|
461
|
+
create_defaults={
|
|
462
|
+
"user_id": user_id,
|
|
463
|
+
"token": str(refresh),
|
|
464
|
+
"created_at": timezone.now(),
|
|
465
|
+
"expires_at": datetime_from_epoch(refresh["exp"]),
|
|
466
|
+
},
|
|
467
|
+
defaults={
|
|
468
|
+
"user_id": user_id,
|
|
469
|
+
"token": str(refresh),
|
|
470
|
+
"expires_at": datetime_from_epoch(refresh["exp"]),
|
|
471
|
+
},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _token_subject_is_active(refresh: RefreshToken) -> bool:
|
|
476
|
+
"""Whether the account a refresh token was minted for is still usable.
|
|
477
|
+
|
|
478
|
+
Returns ``False`` for a missing claim, an account that no longer exists, and a
|
|
479
|
+
deactivated one. The wording of the caller's rejection is deliberately the same
|
|
480
|
+
"invalid or expired" string simplejwt uses for a bad token: a distinct message
|
|
481
|
+
would tell an attacker holding a stolen token that the account exists and was
|
|
482
|
+
disabled, which is an enumeration signal on the one endpoint that is reachable
|
|
483
|
+
without authentication.
|
|
484
|
+
"""
|
|
485
|
+
from rest_framework_simplejwt.settings import api_settings
|
|
486
|
+
|
|
487
|
+
user_id = refresh.payload.get(api_settings.USER_ID_CLAIM)
|
|
488
|
+
if user_id is None:
|
|
489
|
+
return False
|
|
490
|
+
|
|
491
|
+
user_model = get_user_model()
|
|
492
|
+
try:
|
|
493
|
+
return user_model.objects.filter(
|
|
494
|
+
**{api_settings.USER_ID_FIELD: user_id, "is_active": True}
|
|
495
|
+
).exists()
|
|
496
|
+
except (DjangoValidationError, ValueError, TypeError):
|
|
497
|
+
# A claim of the wrong type for the pk column (a string where an int is
|
|
498
|
+
# expected) makes Django raise at query build. DRF does not convert
|
|
499
|
+
# Django's ValidationError, so letting it escape turns a bad token into a
|
|
500
|
+
# 500 on an unauthenticated endpoint. Treat an unusable claim as "not a
|
|
501
|
+
# valid subject" — the same answer as a claim that matches nothing.
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _apply_remember(refresh: RefreshToken, *, remember: bool) -> None:
|
|
506
|
+
"""Stamp the ``remember`` claim + matching ``exp`` on a refresh token (#2246).
|
|
507
|
+
|
|
508
|
+
The choice is carried as a boolean claim *on the token itself* so it survives
|
|
509
|
+
encode/decode and is inherited automatically across rotation (the stateless
|
|
510
|
+
refresh endpoint has no ``remember_me`` in its request). ``set_exp(lifetime=)``
|
|
511
|
+
overrides simplejwt's class default, baking the real credential lifetime — 30
|
|
512
|
+
days for remember, 12 hours (sliding via rotation) for a session login.
|
|
513
|
+
"""
|
|
514
|
+
lifetime = (
|
|
515
|
+
settings.REFRESH_TOKEN_REMEMBER_LIFETIME
|
|
516
|
+
if remember
|
|
517
|
+
else settings.REFRESH_TOKEN_SESSION_LIFETIME
|
|
518
|
+
)
|
|
519
|
+
refresh["remember"] = remember
|
|
520
|
+
refresh.set_exp(lifetime=lifetime)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _clear_refresh_cookie(response: Response) -> None:
|
|
524
|
+
"""Delete the refresh cookie, matching the path/samesite it was set with.
|
|
525
|
+
|
|
526
|
+
``delete_cookie`` must be called with the same ``path`` (and ``samesite``)
|
|
527
|
+
the cookie was set with, or the browser keeps the original cookie.
|
|
528
|
+
"""
|
|
529
|
+
response.delete_cookie(
|
|
530
|
+
key=settings.AUTH_REFRESH_COOKIE_NAME,
|
|
531
|
+
path=settings.AUTH_REFRESH_COOKIE_PATH,
|
|
532
|
+
samesite=settings.AUTH_REFRESH_COOKIE_SAMESITE,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class _LoginRequestSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
537
|
+
"""Login request body: credentials + the optional ``remember_me`` flag (#2246).
|
|
538
|
+
|
|
539
|
+
``username`` accepts either identifier (#3468) — the field keeps its name for
|
|
540
|
+
wire compatibility with every existing client and with simplejwt's serializer.
|
|
541
|
+
|
|
542
|
+
Declared for the OpenAPI schema only — the view validates credentials via
|
|
543
|
+
simplejwt's ``TokenObtainPairSerializer`` and reads ``remember_me`` from the
|
|
544
|
+
body separately, so this serializer documents the request shape (so API-first
|
|
545
|
+
consumers see ``remember_me``) without being used to validate.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
username = serializers.CharField(
|
|
549
|
+
help_text=(
|
|
550
|
+
"The account's username, or the email address on the account (#3468). "
|
|
551
|
+
"The username is matched first; the email is tried only if that fails, "
|
|
552
|
+
"and only when exactly one account carries it — an email shared by two "
|
|
553
|
+
"accounts is refused rather than resolved to either."
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
password = serializers.CharField(write_only=True, style={"input_type": "password"})
|
|
557
|
+
remember_me = serializers.BooleanField(
|
|
558
|
+
required=False,
|
|
559
|
+
default=False,
|
|
560
|
+
help_text=(
|
|
561
|
+
"Keep the session across browser close (~30 days). Omitted or false → a "
|
|
562
|
+
"session-only login: the refresh cookie is dropped on browser close, with "
|
|
563
|
+
"a ~12h idle lifetime. Must be a JSON boolean; non-boolean values are "
|
|
564
|
+
"treated as false."
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class _LoginResponseSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
570
|
+
"""Login response shape: the access token only.
|
|
571
|
+
|
|
572
|
+
The refresh token is intentionally absent from the body — it is delivered as
|
|
573
|
+
an httpOnly cookie (#897). Declared explicitly so drf-spectacular does not
|
|
574
|
+
emit simplejwt's default ``TokenObtainPair`` schema, which still claims a
|
|
575
|
+
required ``refresh`` field this view never returns (#997).
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
access = serializers.CharField()
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _resolve_login_email_fallback(body: Any) -> str | None:
|
|
582
|
+
"""Resolve a failed username login's identifier as an email, or ``None``.
|
|
583
|
+
|
|
584
|
+
When the identifier looks like an email but matches no account, burns an
|
|
585
|
+
equivalent password hash so the refusal costs the same as a wrong password and
|
|
586
|
+
does not reveal whether an account carries that email.
|
|
587
|
+
"""
|
|
588
|
+
identifier = _submitted_identifier(body)
|
|
589
|
+
if not _looks_like_email(identifier):
|
|
590
|
+
return None
|
|
591
|
+
resolved = _resolve_email_identifier(identifier)
|
|
592
|
+
if resolved is None:
|
|
593
|
+
_burn_equivalent_password_hash(body)
|
|
594
|
+
return resolved
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class AuthErrorDetailSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
598
|
+
"""The DRF detail error envelope returned with a 401 or 403 from login."""
|
|
599
|
+
|
|
600
|
+
detail = serializers.CharField()
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class CookieTokenObtainPairView(TokenObtainPairView):
|
|
604
|
+
"""JWT login: return the access token in the body, refresh in an httpOnly cookie.
|
|
605
|
+
|
|
606
|
+
Throttled with the scoped ``login`` rate (#770) to bound password guessing.
|
|
607
|
+
The refresh token is *removed* from the JSON response body and set as a
|
|
608
|
+
hardened cookie instead, so a successful login response no longer carries the
|
|
609
|
+
long-lived credential anywhere JavaScript can read it (#897).
|
|
610
|
+
|
|
611
|
+
Two throttles are STACKED (#1717): the IP-keyed ``login`` scope bounds guesses
|
|
612
|
+
per source address, and ``LoginAccountRateThrottle`` bounds guesses per
|
|
613
|
+
*account* (hashed username) across all source IPs — closing the distributed
|
|
614
|
+
credential-stuffing gap where a rotating IP pool gets the full per-IP allowance
|
|
615
|
+
from every fresh IP. Both must pass for a request to reach authentication.
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
# RUF012: throttle_classes is inherited from the simplejwt base; ruff reads
|
|
619
|
+
# this as a fresh mutable default it can't resolve.
|
|
620
|
+
throttle_classes = [ScopedRateThrottle, LoginAccountRateThrottle] # noqa: RUF012
|
|
621
|
+
throttle_scope = "login"
|
|
622
|
+
|
|
623
|
+
@extend_schema(
|
|
624
|
+
summary="Log in and obtain a JWT access token",
|
|
625
|
+
description=(
|
|
626
|
+
"Returns the short-lived **access** token in the JSON body. The "
|
|
627
|
+
"long-lived **refresh** token is set as an httpOnly, Secure, "
|
|
628
|
+
"SameSite=Strict cookie scoped to the refresh endpoint; it is never "
|
|
629
|
+
"present in the response body (#897)."
|
|
630
|
+
),
|
|
631
|
+
request=_LoginRequestSerializer,
|
|
632
|
+
responses={
|
|
633
|
+
200: _LoginResponseSerializer,
|
|
634
|
+
401: OpenApiResponse(
|
|
635
|
+
response=AuthErrorDetailSerializer,
|
|
636
|
+
description=(
|
|
637
|
+
"Invalid credentials. The body is identical for an unknown "
|
|
638
|
+
"user and a wrong password, so it never reveals which "
|
|
639
|
+
"accounts exist."
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
403: OpenApiResponse(
|
|
643
|
+
response=AuthErrorDetailSerializer,
|
|
644
|
+
description=(
|
|
645
|
+
"Password sign-in is disabled for this account by policy "
|
|
646
|
+
"(e.g. enforced SSO); use single sign-on instead."
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
},
|
|
650
|
+
)
|
|
651
|
+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
|
652
|
+
serializer = TokenObtainPairSerializer(data=request.data)
|
|
653
|
+
try:
|
|
654
|
+
serializer.is_valid(raise_exception=True)
|
|
655
|
+
except TokenError as exc:
|
|
656
|
+
raise InvalidToken(exc.args[0]) from exc
|
|
657
|
+
except AuthenticationFailed:
|
|
658
|
+
# Bad credentials (simplejwt raises AuthenticationFailed with code
|
|
659
|
+
# "no_active_account"). Before giving up, retry ONCE with the submitted
|
|
660
|
+
# identifier resolved as an email address (#3468): the sign-in form asks
|
|
661
|
+
# for an email, invited users choose a *username* at accept time, and
|
|
662
|
+
# ModelBackend matches on username only — so a user typing exactly what
|
|
663
|
+
# the label asked for was refused.
|
|
664
|
+
#
|
|
665
|
+
# Ordering is the security property, not a convenience. The username
|
|
666
|
+
# attempt runs FIRST and unchanged, so this can only ever ADD a way in:
|
|
667
|
+
# an existing username keeps working, and an account whose *username* is
|
|
668
|
+
# email-shaped is never shadowed by a different account that happens to
|
|
669
|
+
# carry that string as its *email*.
|
|
670
|
+
#
|
|
671
|
+
# This lives in the view rather than in an AUTHENTICATION_BACKENDS entry
|
|
672
|
+
# on purpose. A backend would widen every ``authenticate()`` caller in the
|
|
673
|
+
# process — the Django admin login among them — and would bypass this
|
|
674
|
+
# view's post-authentication policy seam below.
|
|
675
|
+
resolved = _resolve_login_email_fallback(request.data)
|
|
676
|
+
if resolved is None:
|
|
677
|
+
_emit_login_failure_event(request)
|
|
678
|
+
raise
|
|
679
|
+
|
|
680
|
+
# Charge AND enforce the resolved account's own throttle bucket. The
|
|
681
|
+
# per-account throttle keys on the identifier as submitted (it runs before
|
|
682
|
+
# this view), so an attacker alternating username and email would otherwise
|
|
683
|
+
# get two independent buckets against one account and double the guess
|
|
684
|
+
# allowance #1717 exists to bound.
|
|
685
|
+
#
|
|
686
|
+
# Recording alone is not enough, and is the shape that looks finished while
|
|
687
|
+
# being half-broken: the canonical bucket already accumulates every attempt
|
|
688
|
+
# in either form, but if nothing CHECKS it here, an attacker who spends the
|
|
689
|
+
# username budget first arrives at an untouched email bucket and gets a
|
|
690
|
+
# second full allowance. Only the email-first order would be capped. So the
|
|
691
|
+
# refusal happens here, before the second password comparison is spent.
|
|
692
|
+
wait = LoginAccountRateThrottle.consume(resolved)
|
|
693
|
+
if wait is not None:
|
|
694
|
+
_emit_login_failure_event(request, canonical_identifier=resolved)
|
|
695
|
+
raise Throttled(wait=wait) from None
|
|
696
|
+
|
|
697
|
+
# Re-validate through the SAME serializer so every downstream step —
|
|
698
|
+
# the enterprise password-login policy seam, remember_me, the refresh
|
|
699
|
+
# cookie, the OutstandingToken row — runs exactly as it does for a
|
|
700
|
+
# username login. A resolution that short-circuited any of them would
|
|
701
|
+
# be a second, weaker login path wearing the first one's name.
|
|
702
|
+
# Build the retry body from the guarded dict, not from ``request.data``:
|
|
703
|
+
# every other field the caller sent (``password``, ``remember_me``) rides
|
|
704
|
+
# through untouched, and only the identifier is rewritten.
|
|
705
|
+
serializer = TokenObtainPairSerializer(
|
|
706
|
+
data={**_login_body(request.data), "username": resolved}
|
|
707
|
+
)
|
|
708
|
+
try:
|
|
709
|
+
serializer.is_valid(raise_exception=True)
|
|
710
|
+
except TokenError as exc:
|
|
711
|
+
raise InvalidToken(exc.args[0]) from exc
|
|
712
|
+
except AuthenticationFailed:
|
|
713
|
+
# Right email, wrong password. Identical 401, identical body, and the
|
|
714
|
+
# same audit line as every other refusal — the caller must not be able
|
|
715
|
+
# to tell "no account with this email" from "wrong password".
|
|
716
|
+
_emit_login_failure_event(request, canonical_identifier=resolved)
|
|
717
|
+
raise
|
|
718
|
+
|
|
719
|
+
# Password-login policy seam (ADR-0187 §4). OSS always allows password
|
|
720
|
+
# login (the default returns True); trueppm-enterprise registers a policy
|
|
721
|
+
# that can block local login when an admin enforces org-wide SSO. Imported
|
|
722
|
+
# lazily so this auth path carries no hard dependency on the sso app at
|
|
723
|
+
# module load. The check runs only after a *successful* credential
|
|
724
|
+
# validation, so it never leaks whether an account exists.
|
|
725
|
+
from trueppm_api.apps.sso.extensions import local_login_allowed
|
|
726
|
+
|
|
727
|
+
if not local_login_allowed(serializer.user):
|
|
728
|
+
return Response(
|
|
729
|
+
{"detail": "Password sign-in is disabled for this account. Use single sign-on."},
|
|
730
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# "Remember me" (#2246): a remember_me of JSON `true` opts into a long-lived,
|
|
734
|
+
# browser-persistent session; anything else (unchecked / omitted / non-object
|
|
735
|
+
# body / a non-boolean like the string "false") falls back to the safe session
|
|
736
|
+
# default. We require an actual boolean `is True` rather than bool()-coercing,
|
|
737
|
+
# so a client that sends the *string* "false"/"0" is not silently upgraded to a
|
|
738
|
+
# 30-day persistent credential — the coercion must err toward session-only. The
|
|
739
|
+
# dict guard mirrors _emit_login_failure_event (a non-object body has no .get).
|
|
740
|
+
remember = isinstance(request.data, dict) and request.data.get("remember_me") is True
|
|
741
|
+
|
|
742
|
+
data = dict(serializer.validated_data)
|
|
743
|
+
refresh_token = data.pop("refresh", None)
|
|
744
|
+
|
|
745
|
+
response = Response(data, status=status.HTTP_200_OK)
|
|
746
|
+
if refresh_token:
|
|
747
|
+
# Reuse the token the serializer already minted (same jti / OutstandingToken
|
|
748
|
+
# row) rather than issuing a second one, then stamp the remember lifetime.
|
|
749
|
+
refresh = RefreshToken(refresh_token)
|
|
750
|
+
_apply_remember(refresh, remember=remember)
|
|
751
|
+
_record_outstanding(refresh)
|
|
752
|
+
_set_refresh_cookie(
|
|
753
|
+
response, str(refresh), persistent_seconds=_cookie_seconds(remember)
|
|
754
|
+
)
|
|
755
|
+
# LAST, deliberately (#3552, ADR-1120): the session now exists. Emitting this
|
|
756
|
+
# where the credentials validated would report a success for a login the
|
|
757
|
+
# ``local_login_allowed`` seam above still refuses with a 403 and no cookie.
|
|
758
|
+
emit_login_success(request, user=serializer.user, method="password", remember=remember)
|
|
759
|
+
return response
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class _CookieRefreshResponseSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
763
|
+
"""Response shape for the cookie-based refresh endpoint (access token only)."""
|
|
764
|
+
|
|
765
|
+
access = serializers.CharField()
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class CookieTokenRefreshView(APIView):
|
|
769
|
+
"""JWT refresh that reads the refresh token from the httpOnly cookie.
|
|
770
|
+
|
|
771
|
+
The refresh token is never accepted from the request body — it is read only
|
|
772
|
+
from the cookie set at login — so the SPA does not need JavaScript access to
|
|
773
|
+
it. The token is rotated (a new refresh token is minted and re-cookied) when
|
|
774
|
+
``ROTATE_REFRESH_TOKENS`` is enabled, and the previous token is blacklisted
|
|
775
|
+
when the blacklist app is installed (``BLACKLIST_AFTER_ROTATION``). The new
|
|
776
|
+
access token is returned in the body.
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
# The default permission class is IsAuthenticated, but refresh must be
|
|
780
|
+
# callable by a client whose access token has already expired. Authentication
|
|
781
|
+
# here is the possession of a valid refresh cookie, validated below.
|
|
782
|
+
permission_classes: list[Any] = [] # noqa: RUF012
|
|
783
|
+
authentication_classes: list[Any] = [] # noqa: RUF012
|
|
784
|
+
throttle_classes = [ScopedRateThrottle] # noqa: RUF012
|
|
785
|
+
throttle_scope = "refresh"
|
|
786
|
+
|
|
787
|
+
@extend_schema(
|
|
788
|
+
request=None,
|
|
789
|
+
responses={
|
|
790
|
+
200: _CookieRefreshResponseSerializer,
|
|
791
|
+
401: OpenApiResponse(description="Missing or invalid refresh cookie."),
|
|
792
|
+
403: OpenApiResponse(
|
|
793
|
+
description=(
|
|
794
|
+
"Refused: the request carries a cross-site Sec-Fetch-Site or "
|
|
795
|
+
"Origin signal (#3556). The refresh cookie is left untouched."
|
|
796
|
+
)
|
|
797
|
+
),
|
|
798
|
+
},
|
|
799
|
+
summary="Refresh the access token using the httpOnly refresh cookie",
|
|
800
|
+
)
|
|
801
|
+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
|
802
|
+
# Checked FIRST, before the cookie is even read (#3556): a refused
|
|
803
|
+
# request must leave the refresh cookie completely untouched, and the
|
|
804
|
+
# simplest way to guarantee that is to never reach the code that could
|
|
805
|
+
# set or clear it.
|
|
806
|
+
if _is_cross_site_auth_request(request):
|
|
807
|
+
logger.warning("auth.refresh_cross_site_rejected client_ip=%s", _client_ip(request))
|
|
808
|
+
return Response(
|
|
809
|
+
{"detail": "Cross-site request refused."}, status=status.HTTP_403_FORBIDDEN
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
raw_token = request.COOKIES.get(settings.AUTH_REFRESH_COOKIE_NAME)
|
|
813
|
+
if not raw_token:
|
|
814
|
+
return Response(
|
|
815
|
+
{"detail": "No refresh token cookie present."},
|
|
816
|
+
status=status.HTTP_401_UNAUTHORIZED,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
# RefreshToken accepts an encoded token string at runtime; the stub
|
|
821
|
+
# only types the Token object form.
|
|
822
|
+
refresh = RefreshToken(raw_token) # type: ignore[arg-type]
|
|
823
|
+
except TokenError as exc:
|
|
824
|
+
# False positive: simplejwt's TokenError carries a curated, client-safe
|
|
825
|
+
# reason ("Token is invalid or expired"), never a trace/path/internal.
|
|
826
|
+
return Response(
|
|
827
|
+
{"detail": str(exc)}, # codeql[py/stack-trace-exposure]
|
|
828
|
+
status=status.HTTP_401_UNAUTHORIZED,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Defense in depth (#2999): blacklisting is the primary revocation channel,
|
|
832
|
+
# but it only reaches tokens we have a row for. A deactivated account must
|
|
833
|
+
# not be able to mint access tokens regardless — off-boarding sets
|
|
834
|
+
# ``is_active=False`` and every credential derived from the account has to
|
|
835
|
+
# stop working at that moment, not at the refresh token's TTL. The view is
|
|
836
|
+
# deliberately unauthenticated (the access token may already have expired),
|
|
837
|
+
# so this is the only place the account's live state is consulted.
|
|
838
|
+
if not _token_subject_is_active(refresh):
|
|
839
|
+
return Response(
|
|
840
|
+
{"detail": "Token is invalid or expired."},
|
|
841
|
+
status=status.HTTP_401_UNAUTHORIZED,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
access_token = str(refresh.access_token)
|
|
845
|
+
|
|
846
|
+
rotate = settings.SIMPLE_JWT.get("ROTATE_REFRESH_TOKENS", False)
|
|
847
|
+
response = Response({"access": access_token}, status=status.HTTP_200_OK)
|
|
848
|
+
|
|
849
|
+
if rotate:
|
|
850
|
+
# Preserve the "remember me" choice across rotation (#2246): the incoming
|
|
851
|
+
# token's ``remember`` claim decides the rotated token's lifetime and
|
|
852
|
+
# cookie persistence. A missing claim marks a pre-#2246 token — keep the
|
|
853
|
+
# legacy 7-day persistent behavior so those live sessions are never
|
|
854
|
+
# disrupted (they adopt the new model at their next fresh login).
|
|
855
|
+
remember_claim = refresh.payload.get("remember")
|
|
856
|
+
|
|
857
|
+
# Blacklist the just-used refresh token before issuing a new one, so a
|
|
858
|
+
# leaked token cannot be replayed once the legitimate client rotates.
|
|
859
|
+
# The token_blacklist app is installed by default (#910); the
|
|
860
|
+
# AttributeError suppression is belt-and-braces for a lean deploy that
|
|
861
|
+
# removes it, in which case rotation degrades to TTL-only expiry.
|
|
862
|
+
if settings.SIMPLE_JWT.get("BLACKLIST_AFTER_ROTATION", False):
|
|
863
|
+
with contextlib.suppress(AttributeError):
|
|
864
|
+
refresh.blacklist()
|
|
865
|
+
refresh.set_jti()
|
|
866
|
+
if remember_claim is None:
|
|
867
|
+
legacy_lifetime = cast(timedelta, settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"])
|
|
868
|
+
refresh.set_exp(lifetime=legacy_lifetime)
|
|
869
|
+
persistent_seconds: int | None = int(legacy_lifetime.total_seconds())
|
|
870
|
+
else:
|
|
871
|
+
remember = bool(remember_claim)
|
|
872
|
+
_apply_remember(refresh, remember=remember)
|
|
873
|
+
persistent_seconds = _cookie_seconds(remember)
|
|
874
|
+
refresh.set_iat()
|
|
875
|
+
# Record the rotated jti so the new token is revocable (#2999). simplejwt
|
|
876
|
+
# writes an OutstandingToken only in ``for_user`` (login); without this the
|
|
877
|
+
# live token is the one row-less token, and every server-side revocation
|
|
878
|
+
# path silently misses it.
|
|
879
|
+
_record_outstanding(refresh)
|
|
880
|
+
_set_refresh_cookie(response, str(refresh), persistent_seconds=persistent_seconds)
|
|
881
|
+
|
|
882
|
+
return response
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class CookieTokenLogoutView(APIView):
|
|
886
|
+
"""Log out: clear the refresh cookie and best-effort blacklist the token.
|
|
887
|
+
|
|
888
|
+
Always returns 205 (reset content) — logout is idempotent. If the
|
|
889
|
+
``token_blacklist`` app is installed, the presented refresh token is
|
|
890
|
+
blacklisted so it cannot be reused after the cookie is cleared; otherwise the
|
|
891
|
+
refresh token remains valid only until its (short) TTL expires.
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
permission_classes: list[Any] = [] # noqa: RUF012
|
|
895
|
+
authentication_classes: list[Any] = [] # noqa: RUF012
|
|
896
|
+
|
|
897
|
+
@extend_schema(
|
|
898
|
+
request=None,
|
|
899
|
+
responses={
|
|
900
|
+
205: OpenApiResponse(description="Logged out; refresh cookie cleared."),
|
|
901
|
+
403: OpenApiResponse(
|
|
902
|
+
description=(
|
|
903
|
+
"Refused: the request carries a cross-site Sec-Fetch-Site or "
|
|
904
|
+
"Origin signal (#3556). The refresh cookie is left untouched."
|
|
905
|
+
)
|
|
906
|
+
),
|
|
907
|
+
},
|
|
908
|
+
summary="Log out — clear the refresh cookie and revoke the token",
|
|
909
|
+
)
|
|
910
|
+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
|
911
|
+
# Same defense-in-depth check as CookieTokenRefreshView, checked first
|
|
912
|
+
# for the same reason: a refused request must leave the refresh cookie
|
|
913
|
+
# untouched, which is only guaranteed by never reaching the blacklist /
|
|
914
|
+
# delete_cookie calls below (#3556).
|
|
915
|
+
if _is_cross_site_auth_request(request):
|
|
916
|
+
logger.warning("auth.logout_cross_site_rejected client_ip=%s", _client_ip(request))
|
|
917
|
+
return Response(
|
|
918
|
+
{"detail": "Cross-site request refused."}, status=status.HTTP_403_FORBIDDEN
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
raw_token = request.COOKIES.get(settings.AUTH_REFRESH_COOKIE_NAME)
|
|
922
|
+
response = Response(status=status.HTTP_205_RESET_CONTENT)
|
|
923
|
+
|
|
924
|
+
if raw_token:
|
|
925
|
+
try:
|
|
926
|
+
token = RefreshToken(raw_token) # type: ignore[arg-type]
|
|
927
|
+
token.blacklist()
|
|
928
|
+
except (TokenError, AttributeError):
|
|
929
|
+
# TokenError: already-expired/invalid token — nothing to revoke.
|
|
930
|
+
# AttributeError: token_blacklist app not installed.
|
|
931
|
+
pass
|
|
932
|
+
|
|
933
|
+
_clear_refresh_cookie(response)
|
|
934
|
+
return response
|