quantum-framework 0.18.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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schemas for request/response validation
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ============================================================================
|
|
10
|
+
# PROJECT SCHEMAS
|
|
11
|
+
# ============================================================================
|
|
12
|
+
|
|
13
|
+
class ProjectBase(BaseModel):
|
|
14
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
15
|
+
description: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectCreate(ProjectBase):
|
|
19
|
+
source_path: Optional[str] = None
|
|
20
|
+
git_url: Optional[str] = None
|
|
21
|
+
git_branch: Optional[str] = "main"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProjectUpdate(BaseModel):
|
|
25
|
+
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
26
|
+
description: Optional[str] = None
|
|
27
|
+
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
28
|
+
source_path: Optional[str] = None
|
|
29
|
+
git_url: Optional[str] = None
|
|
30
|
+
git_branch: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProjectResponse(ProjectBase):
|
|
34
|
+
id: int
|
|
35
|
+
status: str
|
|
36
|
+
source_path: Optional[str] = None
|
|
37
|
+
git_url: Optional[str] = None
|
|
38
|
+
git_branch: Optional[str] = None
|
|
39
|
+
created_at: datetime
|
|
40
|
+
updated_at: datetime
|
|
41
|
+
|
|
42
|
+
class Config:
|
|
43
|
+
from_attributes = True # Enables ORM mode
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# DATASOURCE SCHEMAS
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
class DatasourceBase(BaseModel):
|
|
51
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
52
|
+
type: str = Field(..., pattern="^(postgres|mysql|mongodb)$")
|
|
53
|
+
connection_type: str = Field(..., pattern="^(docker|direct)$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DatasourceCreate(DatasourceBase):
|
|
57
|
+
# Docker fields
|
|
58
|
+
image: Optional[str] = None
|
|
59
|
+
port: Optional[int] = None
|
|
60
|
+
|
|
61
|
+
# Direct connection fields
|
|
62
|
+
host: Optional[str] = None
|
|
63
|
+
database_name: Optional[str] = None
|
|
64
|
+
username: Optional[str] = None
|
|
65
|
+
password: Optional[str] = None # Will be encrypted before storage
|
|
66
|
+
|
|
67
|
+
auto_start: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DatasourceUpdate(BaseModel):
|
|
71
|
+
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
72
|
+
status: Optional[str] = Field(None, pattern="^(running|stopped|error)$")
|
|
73
|
+
health_status: Optional[str] = Field(None, pattern="^(healthy|unhealthy|unknown)$")
|
|
74
|
+
auto_start: Optional[bool] = None
|
|
75
|
+
|
|
76
|
+
# Dados de conexão. Este schema existia sem estes campos e sem NENHUMA
|
|
77
|
+
# rota que o usasse: dava para criar e excluir um datasource, nunca
|
|
78
|
+
# corrigi-lo. Errar o host ou a senha significava excluir e recriar — o
|
|
79
|
+
# buraco mais visível do núcleo do admin (ADMIN_STATUS.md).
|
|
80
|
+
host: Optional[str] = None
|
|
81
|
+
port: Optional[int] = Field(None, ge=1, le=65535)
|
|
82
|
+
database_name: Optional[str] = None
|
|
83
|
+
username: Optional[str] = None
|
|
84
|
+
password: Optional[str] = None # cifrada antes de gravar
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DatasourceResponse(DatasourceBase):
|
|
88
|
+
id: int
|
|
89
|
+
project_id: int
|
|
90
|
+
container_id: Optional[str]
|
|
91
|
+
image: Optional[str]
|
|
92
|
+
port: Optional[int]
|
|
93
|
+
host: Optional[str]
|
|
94
|
+
database_name: Optional[str]
|
|
95
|
+
username: Optional[str]
|
|
96
|
+
status: str
|
|
97
|
+
health_status: str
|
|
98
|
+
setup_status: str
|
|
99
|
+
auto_start: bool
|
|
100
|
+
created_at: datetime
|
|
101
|
+
updated_at: datetime
|
|
102
|
+
|
|
103
|
+
class Config:
|
|
104
|
+
from_attributes = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ============================================================================
|
|
108
|
+
# COMPONENT SCHEMAS
|
|
109
|
+
# ============================================================================
|
|
110
|
+
|
|
111
|
+
class ComponentResponse(BaseModel):
|
|
112
|
+
id: int
|
|
113
|
+
project_id: int
|
|
114
|
+
name: str
|
|
115
|
+
file_path: str
|
|
116
|
+
status: str
|
|
117
|
+
error_message: Optional[str]
|
|
118
|
+
last_compiled: Optional[datetime]
|
|
119
|
+
|
|
120
|
+
# Metadata counters
|
|
121
|
+
function_count: int = 0
|
|
122
|
+
endpoint_count: int = 0
|
|
123
|
+
query_count: int = 0
|
|
124
|
+
|
|
125
|
+
# Test tracking
|
|
126
|
+
test_count: int = 0
|
|
127
|
+
tests_passing: int = 0
|
|
128
|
+
tests_failing: int = 0
|
|
129
|
+
last_test_run_at: Optional[datetime] = None
|
|
130
|
+
|
|
131
|
+
class Config:
|
|
132
|
+
from_attributes = True
|
|
133
|
+
|
|
134
|
+
def get_has_tests(self) -> bool:
|
|
135
|
+
"""Check if component has tests"""
|
|
136
|
+
return self.test_count > 0
|
|
137
|
+
|
|
138
|
+
def get_test_coverage(self) -> int:
|
|
139
|
+
"""Calculate test coverage percentage"""
|
|
140
|
+
if self.test_count == 0:
|
|
141
|
+
return 0
|
|
142
|
+
return int((self.tests_passing / self.test_count) * 100)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ============================================================================
|
|
146
|
+
# COMPONENT TEST SCHEMAS
|
|
147
|
+
# ============================================================================
|
|
148
|
+
|
|
149
|
+
class ComponentTestCreate(BaseModel):
|
|
150
|
+
"""Schema for creating a component test record"""
|
|
151
|
+
component_id: int
|
|
152
|
+
test_file: str = Field(..., min_length=1, max_length=512)
|
|
153
|
+
test_name: str = Field(..., min_length=1, max_length=255)
|
|
154
|
+
test_type: str = Field(default="unit", pattern="^(unit|integration|api|e2e)$")
|
|
155
|
+
generated_by: Optional[str] = Field(default="auto", pattern="^(auto|manual)$")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ComponentTestUpdate(BaseModel):
|
|
159
|
+
"""Schema for updating a component test record"""
|
|
160
|
+
last_status: Optional[str] = Field(None, pattern="^(passed|failed|skipped|pending|error)$")
|
|
161
|
+
last_run_at: Optional[datetime] = None
|
|
162
|
+
last_duration_seconds: Optional[int] = None
|
|
163
|
+
last_error_message: Optional[str] = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ComponentTestResponse(BaseModel):
|
|
167
|
+
"""Schema for component test response"""
|
|
168
|
+
id: int
|
|
169
|
+
component_id: int
|
|
170
|
+
test_file: str
|
|
171
|
+
test_name: str
|
|
172
|
+
test_type: str
|
|
173
|
+
generated_at: Optional[datetime]
|
|
174
|
+
generated_by: Optional[str]
|
|
175
|
+
last_status: Optional[str]
|
|
176
|
+
last_run_at: Optional[datetime]
|
|
177
|
+
last_duration_seconds: Optional[int]
|
|
178
|
+
last_error_message: Optional[str]
|
|
179
|
+
created_at: datetime
|
|
180
|
+
updated_at: datetime
|
|
181
|
+
|
|
182
|
+
class Config:
|
|
183
|
+
from_attributes = True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class GenerateTestsRequest(BaseModel):
|
|
187
|
+
"""Schema for test generation request"""
|
|
188
|
+
test_types: List[str] = Field(default=["unit", "api"], description="Types of tests to generate")
|
|
189
|
+
output_dir: Optional[str] = Field(default="tests/", description="Output directory for test files")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class GenerateTestsResponse(BaseModel):
|
|
193
|
+
"""Schema for test generation response"""
|
|
194
|
+
component_id: int
|
|
195
|
+
component_name: str
|
|
196
|
+
generated: int
|
|
197
|
+
tests: List[dict]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class RunComponentTestsRequest(BaseModel):
|
|
201
|
+
"""Schema for running component tests"""
|
|
202
|
+
test_types: Optional[List[str]] = Field(default=None, description="Optional filter by test types")
|
|
203
|
+
verbose: bool = Field(default=False, description="Verbose output")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class RunComponentTestsResponse(BaseModel):
|
|
207
|
+
"""Schema for component tests run response"""
|
|
208
|
+
test_run_id: int
|
|
209
|
+
component_id: int
|
|
210
|
+
status: str
|
|
211
|
+
message: str
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# ENDPOINT SCHEMAS
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
class EndpointResponse(BaseModel):
|
|
219
|
+
id: int
|
|
220
|
+
project_id: int
|
|
221
|
+
component_id: Optional[int]
|
|
222
|
+
method: str
|
|
223
|
+
path: str
|
|
224
|
+
function_name: str
|
|
225
|
+
description: Optional[str]
|
|
226
|
+
|
|
227
|
+
class Config:
|
|
228
|
+
from_attributes = True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ============================================================================
|
|
232
|
+
# DETAILED RESPONSES (with nested data)
|
|
233
|
+
# ============================================================================
|
|
234
|
+
|
|
235
|
+
class ProjectDetailResponse(ProjectResponse):
|
|
236
|
+
"""Project with all related data"""
|
|
237
|
+
datasources: List[DatasourceResponse] = []
|
|
238
|
+
components: List[ComponentResponse] = []
|
|
239
|
+
endpoints: List[EndpointResponse] = []
|
|
240
|
+
|
|
241
|
+
class Config:
|
|
242
|
+
from_attributes = True
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ============================================================================
|
|
246
|
+
# TEST EXECUTION SCHEMAS
|
|
247
|
+
# ============================================================================
|
|
248
|
+
|
|
249
|
+
class TestRunCreate(BaseModel):
|
|
250
|
+
"""Schema for creating a new test run"""
|
|
251
|
+
suite_filter: Optional[str] = Field(None, description="Optional suite name to run (e.g., 'phase1', 'phase2')")
|
|
252
|
+
verbose: bool = Field(False, description="Enable verbose output")
|
|
253
|
+
stop_on_fail: bool = Field(False, description="Stop execution on first failure")
|
|
254
|
+
triggered_by: str = Field("manual", description="Who/what triggered the test (manual, ci/cd, schedule)")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestResultResponse(BaseModel):
|
|
258
|
+
"""Schema for test result response"""
|
|
259
|
+
id: int
|
|
260
|
+
test_run_id: int
|
|
261
|
+
suite_name: str
|
|
262
|
+
test_file: str
|
|
263
|
+
status: str
|
|
264
|
+
duration_seconds: Optional[int]
|
|
265
|
+
error_message: Optional[str]
|
|
266
|
+
output: Optional[str]
|
|
267
|
+
|
|
268
|
+
class Config:
|
|
269
|
+
from_attributes = True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TestRunResponse(BaseModel):
|
|
273
|
+
"""Schema for test run response"""
|
|
274
|
+
id: int
|
|
275
|
+
project_id: int
|
|
276
|
+
status: str
|
|
277
|
+
started_at: datetime
|
|
278
|
+
completed_at: Optional[datetime]
|
|
279
|
+
total_tests: int
|
|
280
|
+
passed_tests: int
|
|
281
|
+
failed_tests: int
|
|
282
|
+
duration_seconds: Optional[int]
|
|
283
|
+
triggered_by: Optional[str]
|
|
284
|
+
suite_filter: Optional[str]
|
|
285
|
+
error_message: Optional[str]
|
|
286
|
+
|
|
287
|
+
class Config:
|
|
288
|
+
from_attributes = True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestRunDetailResponse(TestRunResponse):
|
|
292
|
+
"""Test run with all test results"""
|
|
293
|
+
test_results: List[TestResultResponse] = []
|
|
294
|
+
|
|
295
|
+
class Config:
|
|
296
|
+
from_attributes = True
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class TestRunStatusResponse(BaseModel):
|
|
300
|
+
"""Schema for real-time test run status"""
|
|
301
|
+
id: int
|
|
302
|
+
status: str
|
|
303
|
+
total_tests: int
|
|
304
|
+
passed_tests: int
|
|
305
|
+
failed_tests: int
|
|
306
|
+
current_suite: Optional[str]
|
|
307
|
+
progress_percentage: Optional[float]
|
|
308
|
+
estimated_time_remaining: Optional[int]
|
|
309
|
+
|
|
310
|
+
class Config:
|
|
311
|
+
from_attributes = True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ============================================================================
|
|
315
|
+
# CONFIGURATION MANAGEMENT SCHEMAS
|
|
316
|
+
# ============================================================================
|
|
317
|
+
|
|
318
|
+
class EnvironmentVariableCreate(BaseModel):
|
|
319
|
+
"""Schema for creating an environment variable"""
|
|
320
|
+
key: str = Field(..., min_length=1, max_length=255)
|
|
321
|
+
value: str = Field(..., description="Plain text value (will be encrypted)")
|
|
322
|
+
description: Optional[str] = None
|
|
323
|
+
is_secret: bool = Field(False, description="If true, value is masked in responses")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class EnvironmentVariableUpdate(BaseModel):
|
|
327
|
+
"""Schema for updating an environment variable"""
|
|
328
|
+
value: Optional[str] = Field(None, description="New plain text value (will be encrypted)")
|
|
329
|
+
description: Optional[str] = None
|
|
330
|
+
is_secret: Optional[bool] = None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class EnvironmentVariableResponse(BaseModel):
|
|
334
|
+
"""Schema for environment variable response"""
|
|
335
|
+
id: int
|
|
336
|
+
project_id: int
|
|
337
|
+
key: str
|
|
338
|
+
value_masked: str = Field(..., description="Masked value for display (e.g., ****xyz)")
|
|
339
|
+
description: Optional[str]
|
|
340
|
+
is_secret: bool
|
|
341
|
+
created_at: datetime
|
|
342
|
+
updated_at: datetime
|
|
343
|
+
|
|
344
|
+
class Config:
|
|
345
|
+
from_attributes = True
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class ConfigurationHistoryResponse(BaseModel):
|
|
349
|
+
"""Schema for configuration history response"""
|
|
350
|
+
id: int
|
|
351
|
+
project_id: int
|
|
352
|
+
version: int
|
|
353
|
+
changed_at: datetime
|
|
354
|
+
changed_by: Optional[str]
|
|
355
|
+
changes_json: Optional[str]
|
|
356
|
+
|
|
357
|
+
class Config:
|
|
358
|
+
from_attributes = True
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ============================================================================
|
|
362
|
+
# DEPLOYMENT AUTOMATION SCHEMAS
|
|
363
|
+
# ============================================================================
|
|
364
|
+
|
|
365
|
+
class DeploymentTargetCreate(BaseModel):
|
|
366
|
+
"""Schema for creating a deployment target"""
|
|
367
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
368
|
+
type: str = Field(..., pattern="^(local|docker|ssh|kubernetes)$")
|
|
369
|
+
config_json: Optional[str] = Field(None, description="JSON configuration for target")
|
|
370
|
+
is_active: bool = True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class DeploymentTargetResponse(BaseModel):
|
|
374
|
+
"""Schema for deployment target response"""
|
|
375
|
+
id: int
|
|
376
|
+
project_id: int
|
|
377
|
+
name: str
|
|
378
|
+
type: str
|
|
379
|
+
config_json: Optional[str]
|
|
380
|
+
is_active: bool
|
|
381
|
+
created_at: datetime
|
|
382
|
+
updated_at: datetime
|
|
383
|
+
|
|
384
|
+
class Config:
|
|
385
|
+
from_attributes = True
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class DeploymentCreate(BaseModel):
|
|
389
|
+
"""Schema for creating a deployment"""
|
|
390
|
+
target_id: int
|
|
391
|
+
version: Optional[str] = Field(None, description="Git commit, tag, or version")
|
|
392
|
+
triggered_by: str = Field("manual", description="Who/what triggered deployment")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class DeploymentResponse(BaseModel):
|
|
396
|
+
"""Schema for deployment response"""
|
|
397
|
+
id: int
|
|
398
|
+
project_id: int
|
|
399
|
+
target_id: int
|
|
400
|
+
version: Optional[str]
|
|
401
|
+
status: str
|
|
402
|
+
started_at: datetime
|
|
403
|
+
completed_at: Optional[datetime]
|
|
404
|
+
duration_seconds: Optional[int]
|
|
405
|
+
triggered_by: Optional[str]
|
|
406
|
+
error_message: Optional[str]
|
|
407
|
+
|
|
408
|
+
class Config:
|
|
409
|
+
from_attributes = True
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class DeploymentDetailResponse(DeploymentResponse):
|
|
413
|
+
"""Schema for deployment response with logs"""
|
|
414
|
+
build_log: Optional[str]
|
|
415
|
+
deploy_log: Optional[str]
|
|
416
|
+
rollback_from: Optional[int]
|
|
417
|
+
|
|
418
|
+
class Config:
|
|
419
|
+
from_attributes = True
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secret Manager for Quantum Admin
|
|
3
|
+
Handles encryption and decryption of sensitive data using Fernet symmetric encryption
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import base64
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
10
|
+
from cryptography.hazmat.backends import default_backend
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SecretManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages encryption and decryption of secrets using Fernet symmetric encryption
|
|
19
|
+
|
|
20
|
+
The encryption key is derived from environment variable QUANTUM_ENCRYPTION_KEY.
|
|
21
|
+
If not set, a default key is generated (NOT secure for production).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_instance = None
|
|
25
|
+
_cipher = None
|
|
26
|
+
|
|
27
|
+
def __new__(cls):
|
|
28
|
+
"""Singleton pattern to ensure only one instance exists"""
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = super(SecretManager, cls).__new__(cls)
|
|
31
|
+
cls._instance._initialize_cipher()
|
|
32
|
+
return cls._instance
|
|
33
|
+
|
|
34
|
+
def _initialize_cipher(self):
|
|
35
|
+
"""Initialize Fernet cipher with encryption key from environment"""
|
|
36
|
+
encryption_key = os.getenv('QUANTUM_ENCRYPTION_KEY')
|
|
37
|
+
|
|
38
|
+
# Fail-closed em producao. A chave padrao abaixo e DERIVADA DE LITERAIS
|
|
39
|
+
# NO FONTE (senha fixa + salt fixo, PBKDF2 100k): qualquer um com o
|
|
40
|
+
# codigo reconstroi a chave e decifra tudo que foi cifrado com ela. E
|
|
41
|
+
# o admin grava esses valores cifrados em quantum_admin/settings/*.yaml,
|
|
42
|
+
# que ja foi versionado — ou seja, o default re-arma o vazamento a
|
|
43
|
+
# cada save. Em producao isso nao pode ser um aviso que ninguem le;
|
|
44
|
+
# o servico se recusa a subir, espelhando auth_service._load_config.
|
|
45
|
+
if not encryption_key:
|
|
46
|
+
estrito = os.environ.get('QUANTUM_ADMIN_ENV', '').lower() in (
|
|
47
|
+
'production', 'prod')
|
|
48
|
+
if estrito:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"QUANTUM_ADMIN_ENV=production requires QUANTUM_ENCRYPTION_KEY. "
|
|
51
|
+
"The built-in default key is derived from source literals and "
|
|
52
|
+
"provides no confidentiality; refusing to start. Generate one "
|
|
53
|
+
"with: python -m quantum_admin.backend.secret_manager"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
if encryption_key:
|
|
58
|
+
# Use provided key
|
|
59
|
+
key = encryption_key.encode()
|
|
60
|
+
logger.info("✅ Using encryption key from QUANTUM_ENCRYPTION_KEY environment variable")
|
|
61
|
+
else:
|
|
62
|
+
# Generate default key (NOT secure for production!)
|
|
63
|
+
logger.warning("⚠️ QUANTUM_ENCRYPTION_KEY not set! Using default key (NOT SECURE FOR PRODUCTION!)")
|
|
64
|
+
logger.warning("⚠️ Set QUANTUM_ENCRYPTION_KEY environment variable for production use")
|
|
65
|
+
|
|
66
|
+
# Use a deterministic default key derived from a fixed password
|
|
67
|
+
# This allows decryption after restart but is NOT secure
|
|
68
|
+
password = b"quantum-admin-default-key-change-me-in-production"
|
|
69
|
+
salt = b"quantum-salt" # Fixed salt for deterministic key
|
|
70
|
+
kdf = PBKDF2HMAC(
|
|
71
|
+
algorithm=hashes.SHA256(),
|
|
72
|
+
length=32,
|
|
73
|
+
salt=salt,
|
|
74
|
+
iterations=100000,
|
|
75
|
+
backend=default_backend()
|
|
76
|
+
)
|
|
77
|
+
key = base64.urlsafe_b64encode(kdf.derive(password))
|
|
78
|
+
|
|
79
|
+
self._cipher = Fernet(key)
|
|
80
|
+
logger.info("🔐 Secret manager initialized successfully")
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"❌ Failed to initialize secret manager: {e}")
|
|
84
|
+
raise RuntimeError(f"Secret manager initialization failed: {e}")
|
|
85
|
+
|
|
86
|
+
def encrypt(self, plain_text: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Encrypt a plain text string
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
plain_text: The plain text to encrypt
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Base64-encoded encrypted string
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If encryption fails
|
|
98
|
+
"""
|
|
99
|
+
if not plain_text:
|
|
100
|
+
raise ValueError("Cannot encrypt empty string")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
encrypted_bytes = self._cipher.encrypt(plain_text.encode('utf-8'))
|
|
104
|
+
return encrypted_bytes.decode('utf-8')
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"❌ Encryption failed: {e}")
|
|
107
|
+
raise ValueError(f"Encryption failed: {e}")
|
|
108
|
+
|
|
109
|
+
def decrypt(self, encrypted_text: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Decrypt an encrypted string
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
encrypted_text: The encrypted text (base64-encoded)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
str: Decrypted plain text
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If decryption fails
|
|
121
|
+
"""
|
|
122
|
+
if not encrypted_text:
|
|
123
|
+
raise ValueError("Cannot decrypt empty string")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
decrypted_bytes = self._cipher.decrypt(encrypted_text.encode('utf-8'))
|
|
127
|
+
return decrypted_bytes.decode('utf-8')
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"❌ Decryption failed: {e}")
|
|
130
|
+
raise ValueError(f"Decryption failed: {e}")
|
|
131
|
+
|
|
132
|
+
def mask_value(self, value: str, show_chars: int = 4) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Mask a sensitive value for display purposes
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
value: The value to mask
|
|
138
|
+
show_chars: Number of characters to show at the end
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
str: Masked value (e.g., "****xyz")
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> manager = SecretManager()
|
|
145
|
+
>>> manager.mask_value("my-secret-password")
|
|
146
|
+
'****word'
|
|
147
|
+
"""
|
|
148
|
+
if not value:
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
if len(value) <= show_chars:
|
|
152
|
+
return "*" * len(value)
|
|
153
|
+
|
|
154
|
+
masked_length = len(value) - show_chars
|
|
155
|
+
return ("*" * min(masked_length, 8)) + value[-show_chars:]
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def generate_key() -> str:
|
|
159
|
+
"""
|
|
160
|
+
Generate a new Fernet encryption key
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
str: Base64-encoded encryption key
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> key = SecretManager.generate_key()
|
|
167
|
+
>>> print(f"Set this as environment variable:")
|
|
168
|
+
>>> print(f"export QUANTUM_ENCRYPTION_KEY={key}")
|
|
169
|
+
"""
|
|
170
|
+
return Fernet.generate_key().decode('utf-8')
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Singleton instance
|
|
174
|
+
secret_manager = SecretManager()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Convenience functions
|
|
178
|
+
def encrypt_value(plain_text: str) -> str:
|
|
179
|
+
"""Encrypt a value using the global secret manager"""
|
|
180
|
+
return secret_manager.encrypt(plain_text)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def decrypt_value(encrypted_text: str) -> str:
|
|
184
|
+
"""Decrypt a value using the global secret manager"""
|
|
185
|
+
return secret_manager.decrypt(encrypted_text)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def mask_value(value: str, show_chars: int = 4) -> str:
|
|
189
|
+
"""Mask a value for display using the global secret manager"""
|
|
190
|
+
return secret_manager.mask_value(value, show_chars)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _looks_like_fernet_token(stored: str) -> bool:
|
|
194
|
+
"""Is this a Fernet token — regardless of whether OUR key opens it?
|
|
195
|
+
|
|
196
|
+
A Fernet token is urlsafe-base64 over `0x80 | 8-byte timestamp | 16-byte
|
|
197
|
+
IV | ciphertext | 32-byte HMAC`, so the shortest possible one is 57
|
|
198
|
+
bytes and every one of them starts with the version byte 0x80. Plain
|
|
199
|
+
text that happens to be valid base64 (`user:pass`, a JSON blob) does
|
|
200
|
+
not.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
raw = base64.urlsafe_b64decode(stored.encode('ascii'))
|
|
204
|
+
except Exception:
|
|
205
|
+
return False
|
|
206
|
+
return len(raw) >= 57 and raw[0] == 0x80
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DecryptionKeyMismatch(ValueError):
|
|
210
|
+
"""The stored value IS encrypted and the current key does not open it."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def decrypt_or_legacy(stored: str) -> str:
|
|
214
|
+
"""Decrypt a value, tolerating one written before it was encrypted.
|
|
215
|
+
|
|
216
|
+
Cloud integration credentials were saved as plain JSON into a column
|
|
217
|
+
named credentials_encrypted, with a `# TODO: encrypt properly` beside it.
|
|
218
|
+
Rows written back then are still plain text, and refusing to read them
|
|
219
|
+
would lose configuration people already have.
|
|
220
|
+
|
|
221
|
+
So: decrypt when it decrypts, and when it does not, hand back what is
|
|
222
|
+
stored and say why. The next write encrypts it.
|
|
223
|
+
|
|
224
|
+
MAS so quando o guardado nao e um token cifrado.
|
|
225
|
+
|
|
226
|
+
O `except Exception: return stored` valia para os dois motivos de falha,
|
|
227
|
+
e o segundo e desastroso. Se QUANTUM_ENCRYPTION_KEY mudar — trocada,
|
|
228
|
+
perdida, ou simplesmente ausente num deploy novo, quando o modulo cai na
|
|
229
|
+
chave padrao — a decifragem falha com InvalidToken e o texto CIFRADO era
|
|
230
|
+
devolvido como se fosse a senha. Dai:
|
|
231
|
+
|
|
232
|
+
* a conexao com o banco tentava autenticar com o token base64 como
|
|
233
|
+
senha, e o erro que aparecia era "senha invalida", apontando para o
|
|
234
|
+
lugar errado;
|
|
235
|
+
* pior, o proximo save re-cifrava esse token com a chave NOVA. A senha
|
|
236
|
+
de verdade, que so a chave antiga abria, ia embora de vez — e nada
|
|
237
|
+
no caminho dizia que algo tinha se perdido.
|
|
238
|
+
|
|
239
|
+
Um segredo que nao abre e um erro, nao um valor. So texto que nunca foi
|
|
240
|
+
cifrado passa pelo caminho legado.
|
|
241
|
+
"""
|
|
242
|
+
if not stored:
|
|
243
|
+
return stored
|
|
244
|
+
try:
|
|
245
|
+
return secret_manager.decrypt(stored)
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
if _looks_like_fernet_token(stored):
|
|
248
|
+
logger.error(
|
|
249
|
+
"a stored secret is encrypted but the current "
|
|
250
|
+
"QUANTUM_ENCRYPTION_KEY does not decrypt it — the key has "
|
|
251
|
+
"changed or is missing. Restore the original key; saving "
|
|
252
|
+
"over this value would destroy the secret permanently."
|
|
253
|
+
)
|
|
254
|
+
raise DecryptionKeyMismatch(
|
|
255
|
+
"cannot decrypt a stored secret with the current "
|
|
256
|
+
"QUANTUM_ENCRYPTION_KEY (the key changed or is not set). "
|
|
257
|
+
"Restore the key that encrypted it — re-saving this record "
|
|
258
|
+
"would overwrite the secret irrecoverably."
|
|
259
|
+
) from exc
|
|
260
|
+
logger.warning(
|
|
261
|
+
"reading a credential that was stored unencrypted (written "
|
|
262
|
+
"before encryption was wired up); it will be encrypted on the "
|
|
263
|
+
"next save"
|
|
264
|
+
)
|
|
265
|
+
return stored
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
# Generate a new encryption key
|
|
270
|
+
print("="*70)
|
|
271
|
+
print("QUANTUM ADMIN - ENCRYPTION KEY GENERATOR")
|
|
272
|
+
print("="*70)
|
|
273
|
+
print("\nGenerate a new encryption key for production use:\n")
|
|
274
|
+
key = SecretManager.generate_key()
|
|
275
|
+
print(f"export QUANTUM_ENCRYPTION_KEY='{key}'")
|
|
276
|
+
print("\nAdd this to your environment variables or .env file")
|
|
277
|
+
print("="*70)
|