databricks-tellr-app 0.3.13.dev2__tar.gz → 0.3.13.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/PKG-INFO +8 -8
  2. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app/run.py +12 -0
  3. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/PKG-INFO +8 -8
  4. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/SOURCES.txt +2 -4
  5. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/requires.txt +7 -7
  6. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/pyproject.toml +10 -8
  7. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/main.py +0 -18
  8. databricks_tellr_app-0.3.13.dev4/src/core/encryption.py +125 -0
  9. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/lakebase.py +0 -114
  10. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/__init__.py +2 -0
  11. databricks_tellr_app-0.3.13.dev4/src/database/models/encryption_key.py +30 -0
  12. databricks_tellr_app-0.3.13.dev2/src/api/middleware/csrf.py +0 -109
  13. databricks_tellr_app-0.3.13.dev2/src/api/middleware/security_headers.py +0 -97
  14. databricks_tellr_app-0.3.13.dev2/src/core/encryption.py +0 -86
  15. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/README.md +0 -0
  16. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app/__init__.py +0 -0
  17. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  18. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  19. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/setup.cfg +0 -0
  20. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/setup.py +0 -0
  21. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/__init__.py +0 -0
  22. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/__init__.py +0 -0
  23. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/_otel_bootstrap.py +0 -0
  24. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/fixtures/tour_demo_deck.json +0 -0
  25. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/mcp_auth.py +0 -0
  26. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/mcp_server.py +0 -0
  27. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/middleware/__init__.py +0 -0
  28. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/middleware/request_logging.py +0 -0
  29. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/__init__.py +0 -0
  30. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/admin.py +0 -0
  31. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/admin_usage.py +0 -0
  32. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/agent_config.py +0 -0
  33. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/chat.py +0 -0
  34. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/deck_contributors.py +0 -0
  35. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/export.py +0 -0
  36. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/feedback.py +0 -0
  37. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/google_slides.py +0 -0
  38. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/images.py +0 -0
  39. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/local_version.py +0 -0
  40. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/profiles.py +0 -0
  41. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/sessions.py +0 -0
  42. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/__init__.py +0 -0
  43. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/contributors.py +0 -0
  44. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/deck_prompts.py +0 -0
  45. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/identities.py +0 -0
  46. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/slide_styles.py +0 -0
  47. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/setup.py +0 -0
  48. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/slides.py +0 -0
  49. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/tools.py +0 -0
  50. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/tour.py +0 -0
  51. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/verification.py +0 -0
  52. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/version.py +0 -0
  53. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/__init__.py +0 -0
  54. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/agent_config.py +0 -0
  55. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/feedback.py +0 -0
  56. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/requests.py +0 -0
  57. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/responses.py +0 -0
  58. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/__init__.py +0 -0
  59. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/requests.py +0 -0
  60. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/responses.py +0 -0
  61. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/streaming.py +0 -0
  62. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/__init__.py +0 -0
  63. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/chat_service.py +0 -0
  64. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/export_job_queue.py +0 -0
  65. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/feedback_service.py +0 -0
  66. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/job_queue.py +0 -0
  67. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/session_manager.py +0 -0
  68. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/session_naming.py +0 -0
  69. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/usage_events.py +0 -0
  70. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/usage_service.py +0 -0
  71. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/utils/__init__.py +0 -0
  72. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/utils/validation.py +0 -0
  73. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/__init__.py +0 -0
  74. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/config_loader.py +0 -0
  75. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/context_utils.py +0 -0
  76. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/database.py +0 -0
  77. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/databricks_client.py +0 -0
  78. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/defaults.py +0 -0
  79. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/init_default_profile.py +0 -0
  80. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/migrate_profiles_to_agent_config.py +0 -0
  81. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/mlflow_agent_spans.py +0 -0
  82. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/mlflow_tracing.py +0 -0
  83. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/otel_logging.py +0 -0
  84. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/permission_context.py +0 -0
  85. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/prompt_modules.py +0 -0
  86. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/settings_db.py +0 -0
  87. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/user_context.py +0 -0
  88. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/__init__.py +0 -0
  89. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/deck_contributor.py +0 -0
  90. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/feedback.py +0 -0
  91. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/genie_space.py +0 -0
  92. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/google_global_credentials.py +0 -0
  93. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/google_oauth_token.py +0 -0
  94. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/identity.py +0 -0
  95. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/image.py +0 -0
  96. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/profile.py +0 -0
  97. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/profile_contributor.py +0 -0
  98. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/prompts.py +0 -0
  99. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/request_log.py +0 -0
  100. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/session.py +0 -0
  101. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/slide_deck_prompt.py +0 -0
  102. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/slide_style_library.py +0 -0
  103. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/usage_event.py +0 -0
  104. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/user_preference.py +0 -0
  105. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/__init__.py +0 -0
  106. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/slide.py +0 -0
  107. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/slide_deck.py +0 -0
  108. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/__init__.py +0 -0
  109. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/agent.py +0 -0
  110. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/agent_factory.py +0 -0
  111. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/config_service.py +0 -0
  112. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/config_validator.py +0 -0
  113. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/drive_uploader.py +0 -0
  114. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/evaluation/__init__.py +0 -0
  115. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/evaluation/llm_judge.py +0 -0
  116. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/genie_service.py +0 -0
  117. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/google_slides_auth.py +0 -0
  118. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/google_slides_prompts_defaults.py +0 -0
  119. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/html_to_google_slides.py +0 -0
  120. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/html_to_pptx.py +0 -0
  121. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_provider.py +0 -0
  122. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/__init__.py +0 -0
  123. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/account_provider.py +0 -0
  124. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/local_provider.py +0 -0
  125. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/workspace_provider.py +0 -0
  126. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/image_service.py +0 -0
  127. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/image_tools.py +0 -0
  128. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/permission_service.py +0 -0
  129. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_from_html_huashu.py +0 -0
  130. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_from_records.py +0 -0
  131. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_prompts_defaults.py +0 -0
  132. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/profile_service.py +0 -0
  133. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/streaming_callback.py +0 -0
  134. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/__init__.py +0 -0
  135. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/agent_bricks_tool.py +0 -0
  136. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/genie_tool.py +0 -0
  137. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/mcp_tool.py +0 -0
  138. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/model_endpoint_tool.py +0 -0
  139. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/vector_tool.py +0 -0
  140. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/validator.py +0 -0
  141. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/__init__.py +0 -0
  142. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/css_utils.py +0 -0
  143. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/error_handling.py +0 -0
  144. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/html_safety.py +0 -0
  145. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/html_utils.py +0 -0
  146. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/image_utils.py +0 -0
  147. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/js_validator.py +0 -0
  148. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/logging_config.py +0 -0
  149. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/pi_filter.py +0 -0
  150. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/slide_hash.py +0 -0
  151. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/spotlight.py +0 -0
  152. {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/text_caps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.13.dev2
3
+ Version: 0.3.13.dev4
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -24,7 +24,7 @@ Requires-Dist: tinycss2==1.3.0
24
24
  Requires-Dist: esprima==4.0.1
25
25
  Requires-Dist: sqlalchemy==2.0.36
26
26
  Requires-Dist: psycopg2-binary==2.9.10
27
- Requires-Dist: databricks-sql-connector>=3.0.0
27
+ Requires-Dist: databricks-sql-connector==3.0.1
28
28
  Requires-Dist: httpx==0.28.1
29
29
  Requires-Dist: jinja2==3.1.2
30
30
  Requires-Dist: python-dotenv==1.2.1
@@ -32,12 +32,12 @@ Requires-Dist: pyyaml==6.0.1
32
32
  Requires-Dist: pandas==2.3.3
33
33
  Requires-Dist: python-pptx==0.6.23
34
34
  Requires-Dist: playwright==1.48.0
35
- Requires-Dist: Pillow>=10.0.0
36
- Requires-Dist: google-api-python-client>=2.100.0
37
- Requires-Dist: google-auth-oauthlib>=1.2.0
38
- Requires-Dist: google-auth-httplib2>=0.2.0
39
- Requires-Dist: cryptography>=42.0.0
40
- Requires-Dist: sse-starlette>=3.0.0
35
+ Requires-Dist: Pillow==11.3.0
36
+ Requires-Dist: google-api-python-client==2.190.0
37
+ Requires-Dist: google-auth-oauthlib==1.2.4
38
+ Requires-Dist: google-auth-httplib2==0.3.0
39
+ Requires-Dist: cryptography==46.0.3
40
+ Requires-Dist: sse-starlette==3.0.3
41
41
  Requires-Dist: databricks-sdk==0.120.0
42
42
  Requires-Dist: mlflow[databricks]==3.14.0
43
43
  Requires-Dist: mlflow-skinny==3.14.0
@@ -62,6 +62,18 @@ def init_database(seed_databricks_defaults: bool = False) -> None:
62
62
  logger.error(f"Failed to seed defaults: {e}\n{tb}")
63
63
  raise SystemExit(1) from e
64
64
 
65
+ # Seed/load the Fernet master key pre-fork (SDR-4437 CRITICAL-3) so all
66
+ # uvicorn workers find the encryption_keys row already present.
67
+ logger.info("Ensuring encryption key...")
68
+ try:
69
+ from src.core.encryption import ensure_encryption_key
70
+ ensure_encryption_key()
71
+ logger.info("Encryption key ready")
72
+ except Exception as e:
73
+ tb = traceback.format_exc()
74
+ logger.error(f"Failed to ensure encryption key: {e}\n{tb}")
75
+ raise SystemExit(1) from e
76
+
65
77
 
66
78
  def main() -> None:
67
79
  """Start the uvicorn server."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.13.dev2
3
+ Version: 0.3.13.dev4
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -24,7 +24,7 @@ Requires-Dist: tinycss2==1.3.0
24
24
  Requires-Dist: esprima==4.0.1
25
25
  Requires-Dist: sqlalchemy==2.0.36
26
26
  Requires-Dist: psycopg2-binary==2.9.10
27
- Requires-Dist: databricks-sql-connector>=3.0.0
27
+ Requires-Dist: databricks-sql-connector==3.0.1
28
28
  Requires-Dist: httpx==0.28.1
29
29
  Requires-Dist: jinja2==3.1.2
30
30
  Requires-Dist: python-dotenv==1.2.1
@@ -32,12 +32,12 @@ Requires-Dist: pyyaml==6.0.1
32
32
  Requires-Dist: pandas==2.3.3
33
33
  Requires-Dist: python-pptx==0.6.23
34
34
  Requires-Dist: playwright==1.48.0
35
- Requires-Dist: Pillow>=10.0.0
36
- Requires-Dist: google-api-python-client>=2.100.0
37
- Requires-Dist: google-auth-oauthlib>=1.2.0
38
- Requires-Dist: google-auth-httplib2>=0.2.0
39
- Requires-Dist: cryptography>=42.0.0
40
- Requires-Dist: sse-starlette>=3.0.0
35
+ Requires-Dist: Pillow==11.3.0
36
+ Requires-Dist: google-api-python-client==2.190.0
37
+ Requires-Dist: google-auth-oauthlib==1.2.4
38
+ Requires-Dist: google-auth-httplib2==0.3.0
39
+ Requires-Dist: cryptography==46.0.3
40
+ Requires-Dist: sse-starlette==3.0.3
41
41
  Requires-Dist: databricks-sdk==0.120.0
42
42
  Requires-Dist: mlflow[databricks]==3.14.0
43
43
  Requires-Dist: mlflow-skinny==3.14.0
@@ -11,9 +11,7 @@ setup.py
11
11
  ./src/api/mcp_server.py
12
12
  ./src/api/fixtures/tour_demo_deck.json
13
13
  ./src/api/middleware/__init__.py
14
- ./src/api/middleware/csrf.py
15
14
  ./src/api/middleware/request_logging.py
16
- ./src/api/middleware/security_headers.py
17
15
  ./src/api/routes/__init__.py
18
16
  ./src/api/routes/admin.py
19
17
  ./src/api/routes/admin_usage.py
@@ -78,6 +76,7 @@ setup.py
78
76
  ./src/database/__init__.py
79
77
  ./src/database/models/__init__.py
80
78
  ./src/database/models/deck_contributor.py
79
+ ./src/database/models/encryption_key.py
81
80
  ./src/database/models/feedback.py
82
81
  ./src/database/models/genie_space.py
83
82
  ./src/database/models/google_global_credentials.py
@@ -156,9 +155,7 @@ src/api/mcp_auth.py
156
155
  src/api/mcp_server.py
157
156
  src/api/fixtures/tour_demo_deck.json
158
157
  src/api/middleware/__init__.py
159
- src/api/middleware/csrf.py
160
158
  src/api/middleware/request_logging.py
161
- src/api/middleware/security_headers.py
162
159
  src/api/routes/__init__.py
163
160
  src/api/routes/admin.py
164
161
  src/api/routes/admin_usage.py
@@ -223,6 +220,7 @@ src/core/user_context.py
223
220
  src/database/__init__.py
224
221
  src/database/models/__init__.py
225
222
  src/database/models/deck_contributor.py
223
+ src/database/models/encryption_key.py
226
224
  src/database/models/feedback.py
227
225
  src/database/models/genie_space.py
228
226
  src/database/models/google_global_credentials.py
@@ -18,7 +18,7 @@ tinycss2==1.3.0
18
18
  esprima==4.0.1
19
19
  sqlalchemy==2.0.36
20
20
  psycopg2-binary==2.9.10
21
- databricks-sql-connector>=3.0.0
21
+ databricks-sql-connector==3.0.1
22
22
  httpx==0.28.1
23
23
  jinja2==3.1.2
24
24
  python-dotenv==1.2.1
@@ -26,12 +26,12 @@ pyyaml==6.0.1
26
26
  pandas==2.3.3
27
27
  python-pptx==0.6.23
28
28
  playwright==1.48.0
29
- Pillow>=10.0.0
30
- google-api-python-client>=2.100.0
31
- google-auth-oauthlib>=1.2.0
32
- google-auth-httplib2>=0.2.0
33
- cryptography>=42.0.0
34
- sse-starlette>=3.0.0
29
+ Pillow==11.3.0
30
+ google-api-python-client==2.190.0
31
+ google-auth-oauthlib==1.2.4
32
+ google-auth-httplib2==0.3.0
33
+ cryptography==46.0.3
34
+ sse-starlette==3.0.3
35
35
  databricks-sdk==0.120.0
36
36
  mlflow[databricks]==3.14.0
37
37
  mlflow-skinny==3.14.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.3.13.dev2"
7
+ version = "0.3.13.dev4"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "esprima==4.0.1",
30
30
  "sqlalchemy==2.0.36",
31
31
  "psycopg2-binary==2.9.10",
32
- "databricks-sql-connector>=3.0.0",
32
+ "databricks-sql-connector==3.0.1",
33
33
  "httpx==0.28.1",
34
34
  "jinja2==3.1.2",
35
35
  "python-dotenv==1.2.1",
@@ -37,12 +37,12 @@ dependencies = [
37
37
  "pandas==2.3.3",
38
38
  "python-pptx==0.6.23",
39
39
  "playwright==1.48.0",
40
- "Pillow>=10.0.0",
41
- "google-api-python-client>=2.100.0",
42
- "google-auth-oauthlib>=1.2.0",
43
- "google-auth-httplib2>=0.2.0",
44
- "cryptography>=42.0.0",
45
- "sse-starlette>=3.0.0",
40
+ "Pillow==11.3.0",
41
+ "google-api-python-client==2.190.0",
42
+ "google-auth-oauthlib==1.2.4",
43
+ "google-auth-httplib2==0.3.0",
44
+ "cryptography==46.0.3",
45
+ "sse-starlette==3.0.3",
46
46
  # --- Resolver-speedup pins: the backtracking-driver cluster ---
47
47
  # Databricks Apps installs this wheel in a platform-managed BUILD phase with a
48
48
  # ~10-minute budget. Left ranged, the databricks-langchain ->
@@ -58,6 +58,8 @@ dependencies = [
58
58
  # via a devloop deploy if these constraints change. Formerly injected at
59
59
  # deploy time as _RESOLVER_SPEEDUP_PINS; now carried in the wheel so a bare
60
60
  # `pip install databricks-tellr-app` is safe.
61
+ # SDR-4437 MEDIUM-7 (July 2026): the 7 formerly-ranged direct deps above
62
+ # this block are now exact-pinned too; same refresh discipline.
61
63
  "databricks-sdk==0.120.0",
62
64
  "mlflow[databricks]==3.14.0",
63
65
  "mlflow-skinny==3.14.0",
@@ -417,24 +417,6 @@ from src.api.middleware.request_logging import RequestLoggingMiddleware
417
417
 
418
418
  app.add_middleware(RequestLoggingMiddleware)
419
419
 
420
- # === SDR-4437 Track A security middleware (PR-1a/PR-1b) ==================
421
- # Keep ALL security-middleware registration inside this delimited block
422
- # (PR-2 adds an exception handler below the middleware block — do not edit
423
- # outside these markers). Starlette wraps later-added middleware outermost;
424
- # ordering here is deliberate:
425
- # SecurityHeaders (outermost, added last) -> CSRF -> RequestLogging ->
426
- # user_auth -> CORS(dev) -> normalize_mcp_path -> routes
427
- # Headers must stamp EVERY response (including CSRF 403s); CSRF must reject
428
- # forged cross-site requests before OBO auth work and before request_logs
429
- # DB writes. CSRF runs outside normalize_mcp_path, so it sees the raw
430
- # un-rewritten /mcp path (its exemption handles both /mcp and /mcp/).
431
- from src.api.middleware.csrf import CSRFProtectionMiddleware # noqa: E402
432
- from src.api.middleware.security_headers import SecurityHeadersMiddleware # noqa: E402
433
-
434
- app.add_middleware(CSRFProtectionMiddleware)
435
- app.add_middleware(SecurityHeadersMiddleware)
436
- # === end SDR-4437 Track A security middleware =============================
437
-
438
420
  # Include API routers
439
421
  app.include_router(admin.router)
440
422
  app.include_router(admin_usage.router)
@@ -0,0 +1,125 @@
1
+ """Fernet symmetric encryption for sensitive data (OAuth credentials, tokens).
2
+
3
+ Uses AES-128-CBC with HMAC-SHA256 authentication via the ``cryptography``
4
+ library. The master key lives in the ``encryption_keys`` table of the
5
+ application database (SDR-4437 CRITICAL-3): dynamic, persistent across
6
+ restarts/restores, unique per deployment, zero operator setup.
7
+
8
+ Resolution is SELECT-first, then race-safe INSERT-if-absent:
9
+
10
+ 1. ``SELECT key_value FROM encryption_keys WHERE id = 1`` — read-first is
11
+ load-bearing: on upgraded installs the table is created by the *deployer*
12
+ role and the app SP holds an explicit SELECT, INSERT grant; reading first
13
+ means INSERT privilege is never exercised when the row already exists.
14
+ 2. If absent, seed from a legacy ``.encryption_key`` file in the project root
15
+ (local dev — keeps pre-existing dev ciphertext decryptable), else generate
16
+ a fresh key.
17
+ 3. ``INSERT ... ON CONFLICT (id) DO NOTHING`` + read-back, so concurrent
18
+ workers/replicas converge on one key.
19
+
20
+ There is deliberately NO env-var fallback: the deploy tool relocates the
21
+ legacy ``GOOGLE_OAUTH_ENCRYPTION_KEY`` into this table at update time, and
22
+ old-tool + new-wheel skew is unsupported (see the SDR-4437 remediation
23
+ design, PR-3).
24
+ """
25
+
26
+ import logging
27
+ from functools import lru_cache
28
+ from pathlib import Path
29
+
30
+ from cryptography.fernet import Fernet, InvalidToken
31
+ from sqlalchemy import text
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ _KEY_FILE = Path(__file__).resolve().parents[2] / ".encryption_key"
36
+
37
+ # NOTE: these statements are deliberately schema-UNQUALIFIED, unlike the
38
+ # _qual()-qualified raw SQL in database.py::_run_migrations. This module must
39
+ # also run against SQLite (unit tests — no schemas) and schema-less local
40
+ # Postgres. On Lakebase the bare name resolves to
41
+ # <LAKEBASE_SCHEMA>.encryption_keys ONLY because _get_database_url appends
42
+ # options=-csearch_path%3D<schema> to the URL (database.py:239,259) and the
43
+ # do_connect listener injects just the password, preserving those options.
44
+ # If that search_path mechanism ever changes, these statements would silently
45
+ # target public.encryption_keys — keep this coupling in mind.
46
+ _SELECT_KEY = text("SELECT key_value FROM encryption_keys WHERE id = 1")
47
+ _INSERT_KEY = text(
48
+ "INSERT INTO encryption_keys (id, key_value, created_at) "
49
+ "VALUES (1, :key_value, CURRENT_TIMESTAMP) "
50
+ "ON CONFLICT (id) DO NOTHING"
51
+ )
52
+
53
+
54
+ def _validated(key: bytes) -> bytes:
55
+ try:
56
+ Fernet(key)
57
+ except (ValueError, TypeError) as exc:
58
+ raise RuntimeError(
59
+ "encryption_keys row id=1 is not a valid Fernet key — refusing to "
60
+ "start with a corrupt master key. Restore the correct key value "
61
+ "before restarting."
62
+ ) from exc
63
+ return key
64
+
65
+
66
+ def _seed_value() -> bytes:
67
+ """Pick the value to seed an empty encryption_keys table with."""
68
+ if _KEY_FILE.exists():
69
+ key = _KEY_FILE.read_text().strip()
70
+ if key:
71
+ logger.info("Seeding encryption_keys from legacy key file %s", _KEY_FILE)
72
+ return key.encode()
73
+ logger.info("Generating new Fernet master key (fresh install)")
74
+ return Fernet.generate_key()
75
+
76
+
77
+ @lru_cache(maxsize=1)
78
+ def get_encryption_key() -> bytes:
79
+ """Return the Fernet master key from the encryption_keys table (id=1)."""
80
+ from src.core.database import get_db_session
81
+
82
+ # 1. Read-first (see module docstring for why this order matters).
83
+ with get_db_session() as session:
84
+ row = session.execute(_SELECT_KEY).first()
85
+ if row and row[0]:
86
+ return _validated(row[0].encode())
87
+
88
+ # 2 + 3. Seed if absent — race-safe across workers/replicas.
89
+ seed = _seed_value()
90
+ with get_db_session() as session:
91
+ session.execute(_INSERT_KEY, {"key_value": seed.decode()})
92
+ with get_db_session() as session:
93
+ row = session.execute(_SELECT_KEY).first()
94
+ if not row or not row[0]:
95
+ raise RuntimeError(
96
+ "Failed to seed encryption_keys: no key row after insert. "
97
+ "Check the app's SELECT, INSERT grants on the data schema."
98
+ )
99
+ return _validated(row[0].encode())
100
+
101
+
102
+ def ensure_encryption_key() -> None:
103
+ """Pre-fork boot hook: load/seed the key before uvicorn workers fork.
104
+
105
+ Called from the deployed app's init_database step so all workers find
106
+ the row already present (the lazy path in get_encryption_key covers
107
+ local dev and replicas regardless).
108
+ """
109
+ get_encryption_key()
110
+
111
+
112
+ def encrypt_data(plaintext: str) -> str:
113
+ """Encrypt *plaintext* and return a base64-encoded ciphertext string."""
114
+ f = Fernet(get_encryption_key())
115
+ return f.encrypt(plaintext.encode()).decode()
116
+
117
+
118
+ def decrypt_data(ciphertext: str) -> str:
119
+ """Decrypt a base64-encoded *ciphertext* string and return the plaintext.
120
+
121
+ Raises:
122
+ cryptography.fernet.InvalidToken: If the key does not match or data is corrupt.
123
+ """
124
+ f = Fernet(get_encryption_key())
125
+ return f.decrypt(ciphertext.encode()).decode()
@@ -280,120 +280,6 @@ def get_lakebase_connection_url(
280
280
  return url
281
281
 
282
282
 
283
- def setup_lakebase_schema(
284
- instance_name: str,
285
- schema: str,
286
- client_id: str,
287
- user: Optional[str] = None,
288
- client: Optional[WorkspaceClient] = None,
289
- endpoint_name: Optional[str] = None,
290
- host: Optional[str] = None,
291
- lakebase_type: str = "provisioned",
292
- ) -> None:
293
- """
294
- Set up schema and grant permissions in Lakebase for an app.
295
-
296
- Supports both provisioned and autoscaling Lakebase.
297
-
298
- This function connects to the Lakebase instance and:
299
- 1. Creates the schema if it doesn't exist
300
- 2. Grants necessary permissions to the app's service principal (client_id)
301
-
302
- Note: This should be called after the app is created and the database
303
- resource is attached, as the client_id comes from the app's service principal.
304
-
305
- Args:
306
- instance_name: Name of the Lakebase instance
307
- schema: Schema name to create
308
- client_id: App's service principal client ID (becomes Postgres role)
309
- user: Postgres username for connection (uses default if not provided)
310
- client: Optional WorkspaceClient
311
- endpoint_name: Full endpoint resource name (autoscaling only)
312
- host: Pre-resolved host (autoscaling only)
313
- lakebase_type: 'autoscaling' or 'provisioned'
314
-
315
- Raises:
316
- LakebaseError: If schema setup fails
317
- """
318
- logger.info(
319
- "Setting up Lakebase schema",
320
- extra={
321
- "instance_name": instance_name, "schema": schema,
322
- "client_id": client_id, "lakebase_type": lakebase_type,
323
- },
324
- )
325
-
326
- try:
327
- # Get connection info
328
- conn_info = get_lakebase_connection_info(
329
- instance_name=instance_name,
330
- user=user,
331
- client=client,
332
- endpoint_name=endpoint_name,
333
- host=host,
334
- lakebase_type=lakebase_type,
335
- )
336
-
337
- # Use psycopg2 directly for DDL operations
338
- import psycopg2
339
-
340
- conn = psycopg2.connect(
341
- host=conn_info["host"],
342
- port=conn_info["port"],
343
- user=conn_info["user"],
344
- password=conn_info["password"],
345
- dbname=conn_info["database"],
346
- sslmode="require",
347
- )
348
- conn.autocommit = True
349
-
350
- with conn.cursor() as cur:
351
- # Create schema if not exists
352
- cur.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
353
- logger.info(f"Schema created or verified: {schema}")
354
-
355
- # Grant permissions to app's service principal
356
- # The client_id is automatically available as a Postgres role when
357
- # the database is added as an app resource
358
- cur.execute(f'GRANT USAGE ON SCHEMA "{schema}" TO "{client_id}"')
359
- cur.execute(f'GRANT CREATE ON SCHEMA "{schema}" TO "{client_id}"')
360
-
361
- # Grant permissions on all tables in schema (for existing tables)
362
- cur.execute(
363
- f'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "{schema}" TO "{client_id}"'
364
- )
365
-
366
- # Grant permissions on all sequences in schema (for auto-increment columns)
367
- cur.execute(
368
- f'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "{schema}" TO "{client_id}"'
369
- )
370
-
371
- # Set default privileges for future tables
372
- cur.execute(
373
- f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema}" '
374
- f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "{client_id}"'
375
- )
376
-
377
- # Set default privileges for future sequences
378
- cur.execute(
379
- f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema}" '
380
- f'GRANT USAGE, SELECT ON SEQUENCES TO "{client_id}"'
381
- )
382
-
383
- logger.info(f"Permissions granted to {client_id} on schema {schema}")
384
-
385
- conn.close()
386
-
387
- except ImportError:
388
- raise LakebaseError(
389
- "psycopg2 is required for Lakebase schema setup. "
390
- "Install with: pip install psycopg2-binary"
391
- )
392
- except Exception as e:
393
- logger.error(f"Failed to setup Lakebase schema: {e}", exc_info=True)
394
- raise LakebaseError(f"Lakebase schema setup failed: {e}") from e
395
-
396
-
397
283
  def initialize_lakebase_tables(
398
284
  instance_name: str,
399
285
  schema: str,
@@ -1,6 +1,7 @@
1
1
  """Database models."""
2
2
 
3
3
  from src.database.models.deck_contributor import DeckContributor
4
+ from src.database.models.encryption_key import EncryptionKey
4
5
  from src.database.models.feedback import FeedbackConversation, SurveyResponse
5
6
  from src.database.models.genie_space import ConfigGenieSpace
6
7
  from src.database.models.google_global_credentials import GoogleGlobalCredentials
@@ -34,6 +35,7 @@ __all__ = [
34
35
  "ChatRequest",
35
36
  "ConfigGenieSpace",
36
37
  "DeckContributor",
38
+ "EncryptionKey",
37
39
  "ConfigProfile",
38
40
  "ConfigProfileContributor",
39
41
  "ConfigPrompts",
@@ -0,0 +1,30 @@
1
+ """Application-managed encryption key storage (SDR-4437 CRITICAL-3).
2
+
3
+ Holds the Fernet master key for OAuth credential/token encryption in the
4
+ ACL-governed Lakebase data schema instead of app.yaml. Single-row table
5
+ (id = 1). Deliberately shares the data schema's grants: the key carries
6
+ the same ACLs as the ciphertext it protects — an explicitly accepted risk
7
+ in the SDR-4437 remediation design. Do NOT add key-specific grant
8
+ tightening here.
9
+ """
10
+
11
+ from sqlalchemy import Column, DateTime, Integer, Text
12
+
13
+ from src.core.database import Base
14
+
15
+
16
+ class EncryptionKey(Base):
17
+ """Fernet master key for OAuth credential/token encryption."""
18
+
19
+ __tablename__ = "encryption_keys"
20
+
21
+ id = Column(Integer, primary_key=True, autoincrement=False)
22
+ key_value = Column(Text, nullable=False)
23
+ # No Python-side default: every insert path (the app's raw-SQL seed and
24
+ # the deploy migration DDL) supplies CURRENT_TIMESTAMP explicitly, so a
25
+ # default would be dead code — and the codebase-conventional
26
+ # datetime.utcnow is deprecated on Python 3.12+.
27
+ created_at = Column(DateTime, nullable=False)
28
+
29
+ def __repr__(self) -> str: # never print key material
30
+ return f"<EncryptionKey(id={self.id}, created_at={self.created_at})>"
@@ -1,109 +0,0 @@
1
- """Origin-validation CSRF protection (SDR-4437 CRITICAL-2, PR-1b).
2
-
3
- Threat model: the app sets no cookies of its own — auth arrives via
4
- proxy-attested ``x-forwarded-*`` headers. The CSRF-relevant ambient
5
- credential is the platform SSO cookie at the Databricks Apps proxy: a
6
- cross-site browser request to the app URL rides that cookie, the proxy
7
- authenticates it and forwards it carrying the victim's identity.
8
- Validating the browser-attested ``Origin`` (falling back to ``Referer``)
9
- against the app's own origin closes this at the app layer.
10
-
11
- Deliberate choices (documented for security review):
12
-
13
- - **Neither header present -> allow.** Cross-origin browser POSTs always
14
- send ``Origin``, so the realistic attack always presents a mismatch;
15
- header-less mutating requests are non-browser clients (scripts/curl via
16
- the proxy), which rejecting would break for no security gain. A strict
17
- mode can be revisited if security review requires it.
18
- - **/mcp is exempt.** It authenticates with bearer tokens, not cookies, so
19
- it is not a CSRF target. The mounted sub-app IS wrapped by the parent
20
- app's middleware (that is how ``normalize_mcp_path`` works), so the
21
- exemption must be this explicit path check, not an assumption that
22
- mounts bypass middleware. This middleware runs OUTSIDE
23
- ``normalize_mcp_path``, so it sees the raw ``/mcp`` (un-rewritten) path
24
- — both ``/mcp`` and ``/mcp/...`` are matched; ``/mcp-anything`` is not.
25
- - **Expected origin:** ``DATABRICKS_APP_URL`` (platform-injected on
26
- Databricks Apps deploys — same assumption as
27
- ``src/api/mcp_server.py::_public_app_url``). If unset, fall back to the
28
- origin reconstructed from ``x-forwarded-proto``/``x-forwarded-host``
29
- (the proxy always sends these; same pattern as
30
- ``google_slides.py::_build_redirect_uri``), so a missing var degrades to
31
- a self-referential check instead of blocking all writes.
32
- - **Inactive outside production** — consistent with the dev-only CORS gate
33
- in ``main.py`` (dev auth is ``DEV_USER_ID`` with no proxy in front).
34
- """
35
-
36
- import logging
37
- import os
38
- from urllib.parse import urlsplit
39
-
40
- from starlette.middleware.base import BaseHTTPMiddleware
41
- from starlette.requests import Request
42
- from starlette.responses import JSONResponse, Response
43
-
44
- logger = logging.getLogger(__name__)
45
-
46
- _UNSAFE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"})
47
-
48
-
49
- def _origin_of(url: str) -> str:
50
- """Normalize a URL (or Origin header value) to ``scheme://host[:port]``.
51
-
52
- Returns "" for values with no parseable scheme+host (e.g. ``null``).
53
- """
54
- parts = urlsplit(url.strip())
55
- if not parts.scheme or not parts.netloc:
56
- return ""
57
- return f"{parts.scheme.lower()}://{parts.netloc.lower()}"
58
-
59
-
60
- class CSRFProtectionMiddleware(BaseHTTPMiddleware):
61
- """Reject state-changing requests whose Origin/Referer is cross-site."""
62
-
63
- def __init__(self, app, enabled: bool | None = None):
64
- super().__init__(app)
65
- self.enabled = (
66
- enabled
67
- if enabled is not None
68
- else os.getenv("ENVIRONMENT", "development") == "production"
69
- )
70
-
71
- async def dispatch(self, request: Request, call_next) -> Response:
72
- if not self.enabled or request.method not in _UNSAFE_METHODS:
73
- return await call_next(request)
74
-
75
- path = request.url.path
76
- if path == "/mcp" or path.startswith("/mcp/"):
77
- return await call_next(request)
78
-
79
- claimed = request.headers.get("origin") or request.headers.get("referer")
80
- if not claimed:
81
- return await call_next(request)
82
-
83
- claimed_origin = _origin_of(claimed)
84
- expected_origin = self._expected_origin(request)
85
- if claimed_origin and expected_origin and claimed_origin == expected_origin:
86
- return await call_next(request)
87
-
88
- logger.warning(
89
- "CSRF: rejected cross-origin %s %s (claimed=%r, expected=%r)",
90
- request.method,
91
- path,
92
- claimed_origin,
93
- expected_origin,
94
- )
95
- return JSONResponse(
96
- status_code=403,
97
- content={"detail": "Cross-origin request rejected"},
98
- )
99
-
100
- @staticmethod
101
- def _expected_origin(request: Request) -> str:
102
- app_url = os.getenv("DATABRICKS_APP_URL", "")
103
- if app_url:
104
- return _origin_of(app_url)
105
- host = request.headers.get("x-forwarded-host", "")
106
- proto = request.headers.get("x-forwarded-proto", "https")
107
- if host:
108
- return f"{proto.strip().lower()}://{host.split(',')[0].strip().lower()}"
109
- return ""