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.
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/PKG-INFO +8 -8
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app/run.py +12 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/PKG-INFO +8 -8
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/SOURCES.txt +2 -4
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/requires.txt +7 -7
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/pyproject.toml +10 -8
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/main.py +0 -18
- databricks_tellr_app-0.3.13.dev4/src/core/encryption.py +125 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/lakebase.py +0 -114
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/__init__.py +2 -0
- databricks_tellr_app-0.3.13.dev4/src/database/models/encryption_key.py +30 -0
- databricks_tellr_app-0.3.13.dev2/src/api/middleware/csrf.py +0 -109
- databricks_tellr_app-0.3.13.dev2/src/api/middleware/security_headers.py +0 -97
- databricks_tellr_app-0.3.13.dev2/src/core/encryption.py +0 -86
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/README.md +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/setup.cfg +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/chat_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/database.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/html_to_pptx.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/src/utils/spotlight.py +0 -0
- {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.
|
|
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
|
|
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
|
|
36
|
-
Requires-Dist: google-api-python-client
|
|
37
|
-
Requires-Dist: google-auth-oauthlib
|
|
38
|
-
Requires-Dist: google-auth-httplib2
|
|
39
|
-
Requires-Dist: cryptography
|
|
40
|
-
Requires-Dist: sse-starlette
|
|
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
|
{databricks_tellr_app-0.3.13.dev2 → databricks_tellr_app-0.3.13.dev4}/databricks_tellr_app/run.py
RENAMED
|
@@ -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.
|
|
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
|
|
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
|
|
36
|
-
Requires-Dist: google-api-python-client
|
|
37
|
-
Requires-Dist: google-auth-oauthlib
|
|
38
|
-
Requires-Dist: google-auth-httplib2
|
|
39
|
-
Requires-Dist: cryptography
|
|
40
|
-
Requires-Dist: sse-starlette
|
|
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
|
|
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
|
|
30
|
-
google-api-python-client
|
|
31
|
-
google-auth-oauthlib
|
|
32
|
-
google-auth-httplib2
|
|
33
|
-
cryptography
|
|
34
|
-
sse-starlette
|
|
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.
|
|
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
|
|
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
|
|
41
|
-
"google-api-python-client
|
|
42
|
-
"google-auth-oauthlib
|
|
43
|
-
"google-auth-httplib2
|
|
44
|
-
"cryptography
|
|
45
|
-
"sse-starlette
|
|
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 ""
|