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,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth Service for Quantum Admin
|
|
3
|
+
JWT-based authentication with bcrypt password hashing
|
|
4
|
+
"""
|
|
5
|
+
import copy
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Try to import JWT library
|
|
16
|
+
try:
|
|
17
|
+
import jwt
|
|
18
|
+
JWT_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
JWT_AVAILABLE = False
|
|
21
|
+
logger.warning("PyJWT not installed. Run: pip install PyJWT")
|
|
22
|
+
|
|
23
|
+
# Try to import bcrypt
|
|
24
|
+
try:
|
|
25
|
+
import bcrypt
|
|
26
|
+
BCRYPT_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
BCRYPT_AVAILABLE = False
|
|
29
|
+
logger.warning("bcrypt not installed. Run: pip install bcrypt")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class User:
|
|
34
|
+
id: int
|
|
35
|
+
username: str
|
|
36
|
+
role: str = "user" # user, admin
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TokenPayload:
|
|
41
|
+
user_id: int
|
|
42
|
+
username: str
|
|
43
|
+
role: str
|
|
44
|
+
exp: datetime
|
|
45
|
+
iat: datetime
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AuthConfigError(RuntimeError):
|
|
49
|
+
"""The admin is configured in a way that cannot be served safely."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthService:
|
|
54
|
+
"""JWT authentication service with bcrypt password hashing"""
|
|
55
|
+
|
|
56
|
+
# Values that used to be the defaults, and are published in this file.
|
|
57
|
+
# Refused outright: someone setting them explicitly is almost always
|
|
58
|
+
# copying an old README, not making a decision.
|
|
59
|
+
UNSAFE_SECRETS = frozenset({
|
|
60
|
+
"quantum-admin-secret-change-in-production",
|
|
61
|
+
"change-me-in-production",
|
|
62
|
+
"changeme",
|
|
63
|
+
"secret",
|
|
64
|
+
})
|
|
65
|
+
UNSAFE_PASSWORDS = frozenset({"admin", "password", "changeme", "123456"})
|
|
66
|
+
|
|
67
|
+
# The admin account. The password is set per instance in __init__ — never
|
|
68
|
+
# here, because a value on the class is shared by every instance.
|
|
69
|
+
DEFAULT_USERS = {
|
|
70
|
+
"admin": {
|
|
71
|
+
"id": 1,
|
|
72
|
+
"password_hash": None,
|
|
73
|
+
"role": "admin"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def __init__(self, secret_key: str = None, expiry_hours: int = 24):
|
|
78
|
+
"""
|
|
79
|
+
Initialize auth service.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
secret_key: JWT signing key. Falls back to JWT_SECRET_KEY, and
|
|
83
|
+
to a random per-process key when neither is set.
|
|
84
|
+
expiry_hours: Token expiry in hours
|
|
85
|
+
|
|
86
|
+
There used to be constant fallbacks here: the signing key defaulted to
|
|
87
|
+
"quantum-admin-secret-change-in-production" and the admin password to
|
|
88
|
+
"admin". Both are in the public source, so an unconfigured admin
|
|
89
|
+
accepted a token anyone could forge — full admin without ever seeing
|
|
90
|
+
the login screen — and admin/admin as well.
|
|
91
|
+
|
|
92
|
+
Now an unconfigured instance gets random values. Development still
|
|
93
|
+
works (the generated password is printed once at boot); an attacker
|
|
94
|
+
gains nothing from reading this file. With
|
|
95
|
+
QUANTUM_ADMIN_ENV=production the service refuses to start at all
|
|
96
|
+
without explicit configuration, because a random key means every
|
|
97
|
+
restart and every extra worker invalidates the sessions.
|
|
98
|
+
"""
|
|
99
|
+
strict = os.environ.get("QUANTUM_ADMIN_ENV", "").lower() in (
|
|
100
|
+
"production", "prod")
|
|
101
|
+
|
|
102
|
+
configured_key = secret_key or os.environ.get("JWT_SECRET_KEY")
|
|
103
|
+
if configured_key in self.UNSAFE_SECRETS:
|
|
104
|
+
raise AuthConfigError(
|
|
105
|
+
"JWT_SECRET_KEY is set to the placeholder value that ships in "
|
|
106
|
+
"the source. Anyone can forge an admin token with it. "
|
|
107
|
+
"Generate one: python -c \"import secrets; "
|
|
108
|
+
"print(secrets.token_hex(32))\""
|
|
109
|
+
)
|
|
110
|
+
if not configured_key:
|
|
111
|
+
if strict:
|
|
112
|
+
raise AuthConfigError(
|
|
113
|
+
"QUANTUM_ADMIN_ENV=production requires JWT_SECRET_KEY. "
|
|
114
|
+
"Without it each process signs with its own random key "
|
|
115
|
+
"and users are logged out at random."
|
|
116
|
+
)
|
|
117
|
+
configured_key = secrets.token_hex(32)
|
|
118
|
+
logger.warning(
|
|
119
|
+
"JWT_SECRET_KEY not set — signing with a random key for this "
|
|
120
|
+
"process only. Sessions will not survive a restart, and a "
|
|
121
|
+
"second worker will reject this one's tokens."
|
|
122
|
+
)
|
|
123
|
+
self.secret_key = configured_key
|
|
124
|
+
|
|
125
|
+
self.expiry_hours = expiry_hours
|
|
126
|
+
self.algorithm = "HS256"
|
|
127
|
+
|
|
128
|
+
# deepcopy, not copy: DEFAULT_USERS' inner dict is shared with the
|
|
129
|
+
# class, so a shallow copy let the first instance write the password
|
|
130
|
+
# hash onto the class and every later instance inherit it — including
|
|
131
|
+
# after ADMIN_PASSWORD changed.
|
|
132
|
+
self._users = copy.deepcopy(self.DEFAULT_USERS)
|
|
133
|
+
|
|
134
|
+
password = os.environ.get("ADMIN_PASSWORD")
|
|
135
|
+
if password in self.UNSAFE_PASSWORDS:
|
|
136
|
+
raise AuthConfigError(
|
|
137
|
+
f"ADMIN_PASSWORD is {password!r}, which is the first thing "
|
|
138
|
+
"anyone tries. Choose another."
|
|
139
|
+
)
|
|
140
|
+
if not password:
|
|
141
|
+
if strict:
|
|
142
|
+
raise AuthConfigError(
|
|
143
|
+
"QUANTUM_ADMIN_ENV=production requires ADMIN_PASSWORD."
|
|
144
|
+
)
|
|
145
|
+
password = secrets.token_urlsafe(18)
|
|
146
|
+
self.generated_password = password
|
|
147
|
+
# Printed, not just logged: a generated credential nobody can read
|
|
148
|
+
# is the same as no login at all.
|
|
149
|
+
banner = (
|
|
150
|
+
"\n" + "=" * 68 +
|
|
151
|
+
"\n Quantum Admin: no ADMIN_PASSWORD set, so one was generated"
|
|
152
|
+
"\n for this process. It changes on every restart."
|
|
153
|
+
f"\n\n user: admin\n password: {password}\n\n"
|
|
154
|
+
" Set ADMIN_PASSWORD to keep a stable login.\n" +
|
|
155
|
+
"=" * 68 + "\n"
|
|
156
|
+
)
|
|
157
|
+
print(banner)
|
|
158
|
+
logger.warning(
|
|
159
|
+
"ADMIN_PASSWORD not set — generated a random admin password "
|
|
160
|
+
"for this process (printed at startup)."
|
|
161
|
+
)
|
|
162
|
+
self._users["admin"]["password_hash"] = self._hash_password(password)
|
|
163
|
+
|
|
164
|
+
def _hash_password(self, password: str) -> str:
|
|
165
|
+
"""Hash a password with bcrypt (secure, with salt)"""
|
|
166
|
+
if not BCRYPT_AVAILABLE:
|
|
167
|
+
# Fallback to less secure method if bcrypt not installed
|
|
168
|
+
import hashlib
|
|
169
|
+
logger.warning("Using SHA-256 fallback - install bcrypt for production!")
|
|
170
|
+
return "sha256:" + hashlib.sha256(password.encode()).hexdigest()
|
|
171
|
+
|
|
172
|
+
# bcrypt automatically generates a salt and includes it in the hash
|
|
173
|
+
salt = bcrypt.gensalt(rounds=12) # Cost factor of 12 is secure and fast enough
|
|
174
|
+
password_bytes = password.encode('utf-8')
|
|
175
|
+
hashed = bcrypt.hashpw(password_bytes, salt)
|
|
176
|
+
return "bcrypt:" + hashed.decode('utf-8')
|
|
177
|
+
|
|
178
|
+
def _verify_password(self, password: str, password_hash: str) -> bool:
|
|
179
|
+
"""Verify a password against its hash"""
|
|
180
|
+
if password_hash.startswith("bcrypt:"):
|
|
181
|
+
if not BCRYPT_AVAILABLE:
|
|
182
|
+
logger.error("bcrypt required to verify password but not installed")
|
|
183
|
+
return False
|
|
184
|
+
stored_hash = password_hash[7:].encode('utf-8') # Remove "bcrypt:" prefix
|
|
185
|
+
password_bytes = password.encode('utf-8')
|
|
186
|
+
return bcrypt.checkpw(password_bytes, stored_hash)
|
|
187
|
+
elif password_hash.startswith("sha256:"):
|
|
188
|
+
# Legacy SHA-256 support (for migration)
|
|
189
|
+
import hashlib
|
|
190
|
+
stored_hash = password_hash[7:] # Remove "sha256:" prefix
|
|
191
|
+
return hashlib.sha256(password.encode()).hexdigest() == stored_hash
|
|
192
|
+
else:
|
|
193
|
+
# Very old format without prefix (assume SHA-256)
|
|
194
|
+
import hashlib
|
|
195
|
+
return hashlib.sha256(password.encode()).hexdigest() == password_hash
|
|
196
|
+
|
|
197
|
+
# =========================================================================
|
|
198
|
+
# Authentication
|
|
199
|
+
# =========================================================================
|
|
200
|
+
|
|
201
|
+
def authenticate(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
|
202
|
+
"""
|
|
203
|
+
Authenticate user and return tokens
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
username: Username
|
|
207
|
+
password: Plain text password
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict with access_token and user info, or None if invalid
|
|
211
|
+
"""
|
|
212
|
+
if not JWT_AVAILABLE:
|
|
213
|
+
logger.error("PyJWT not available")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
user_data = self._users.get(username)
|
|
217
|
+
if not user_data:
|
|
218
|
+
logger.warning(f"Authentication failed: user '{username}' not found")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
if not self._verify_password(password, user_data["password_hash"]):
|
|
222
|
+
logger.warning(f"Authentication failed: invalid password for '{username}'")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
# Migrate old SHA-256 passwords to bcrypt on successful login
|
|
226
|
+
if BCRYPT_AVAILABLE and not user_data["password_hash"].startswith("bcrypt:"):
|
|
227
|
+
logger.info(f"Migrating password hash to bcrypt for user '{username}'")
|
|
228
|
+
user_data["password_hash"] = self._hash_password(password)
|
|
229
|
+
|
|
230
|
+
# Generate token
|
|
231
|
+
user = User(
|
|
232
|
+
id=user_data["id"],
|
|
233
|
+
username=username,
|
|
234
|
+
role=user_data["role"]
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
token = self._create_token(user)
|
|
238
|
+
|
|
239
|
+
logger.info(f"User '{username}' authenticated successfully")
|
|
240
|
+
return {
|
|
241
|
+
"access_token": token,
|
|
242
|
+
"token_type": "bearer",
|
|
243
|
+
"expires_in": self.expiry_hours * 3600,
|
|
244
|
+
"user": {
|
|
245
|
+
"id": user.id,
|
|
246
|
+
"username": user.username,
|
|
247
|
+
"role": user.role
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
def _create_token(self, user: User) -> str:
|
|
252
|
+
"""Create JWT token for user"""
|
|
253
|
+
now = datetime.utcnow()
|
|
254
|
+
payload = {
|
|
255
|
+
"sub": str(user.id), # JWT sub claim must be a string
|
|
256
|
+
"user_id": user.id,
|
|
257
|
+
"username": user.username,
|
|
258
|
+
"role": user.role,
|
|
259
|
+
"iat": now,
|
|
260
|
+
"exp": now + timedelta(hours=self.expiry_hours)
|
|
261
|
+
}
|
|
262
|
+
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
263
|
+
|
|
264
|
+
def verify_token(self, token: str) -> Optional[TokenPayload]:
|
|
265
|
+
"""
|
|
266
|
+
Verify JWT token and return payload
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
token: JWT token string
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
TokenPayload or None if invalid
|
|
273
|
+
"""
|
|
274
|
+
if not JWT_AVAILABLE:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
payload = jwt.decode(
|
|
279
|
+
token,
|
|
280
|
+
self.secret_key,
|
|
281
|
+
algorithms=[self.algorithm]
|
|
282
|
+
)
|
|
283
|
+
return TokenPayload(
|
|
284
|
+
user_id=payload.get("user_id", int(payload["sub"])),
|
|
285
|
+
username=payload["username"],
|
|
286
|
+
role=payload["role"],
|
|
287
|
+
exp=datetime.fromtimestamp(payload["exp"]),
|
|
288
|
+
iat=datetime.fromtimestamp(payload["iat"])
|
|
289
|
+
)
|
|
290
|
+
except jwt.ExpiredSignatureError:
|
|
291
|
+
logger.warning("Token expired")
|
|
292
|
+
return None
|
|
293
|
+
except jwt.InvalidTokenError as e:
|
|
294
|
+
logger.warning(f"Invalid token: {e}")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def get_current_user(self, token: str) -> Optional[User]:
|
|
298
|
+
"""Get current user from token"""
|
|
299
|
+
payload = self.verify_token(token)
|
|
300
|
+
if not payload:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
return User(
|
|
304
|
+
id=payload.user_id,
|
|
305
|
+
username=payload.username,
|
|
306
|
+
role=payload.role
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# =========================================================================
|
|
310
|
+
# User Management
|
|
311
|
+
# =========================================================================
|
|
312
|
+
|
|
313
|
+
def create_user(
|
|
314
|
+
self,
|
|
315
|
+
username: str,
|
|
316
|
+
password: str,
|
|
317
|
+
role: str = "user"
|
|
318
|
+
) -> Optional[User]:
|
|
319
|
+
"""Create a new user"""
|
|
320
|
+
if username in self._users:
|
|
321
|
+
logger.error(f"User '{username}' already exists")
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
user_id = max(u["id"] for u in self._users.values()) + 1
|
|
325
|
+
|
|
326
|
+
self._users[username] = {
|
|
327
|
+
"id": user_id,
|
|
328
|
+
"password_hash": self._hash_password(password),
|
|
329
|
+
"role": role
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
logger.info(f"User '{username}' created with role '{role}'")
|
|
333
|
+
return User(id=user_id, username=username, role=role)
|
|
334
|
+
|
|
335
|
+
def change_password(self, username: str, new_password: str) -> bool:
|
|
336
|
+
"""Change user password"""
|
|
337
|
+
if username not in self._users:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
self._users[username]["password_hash"] = self._hash_password(new_password)
|
|
341
|
+
logger.info(f"Password changed for user '{username}'")
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
def delete_user(self, username: str) -> bool:
|
|
345
|
+
"""Delete a user"""
|
|
346
|
+
if username not in self._users:
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
if username == "admin":
|
|
350
|
+
logger.error("Cannot delete admin user")
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
del self._users[username]
|
|
354
|
+
logger.info(f"User '{username}' deleted")
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
def list_users(self) -> list:
|
|
358
|
+
"""List all users (without passwords)"""
|
|
359
|
+
return [
|
|
360
|
+
{"id": data["id"], "username": username, "role": data["role"]}
|
|
361
|
+
for username, data in self._users.items()
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
# =========================================================================
|
|
365
|
+
# Role Checking
|
|
366
|
+
# =========================================================================
|
|
367
|
+
|
|
368
|
+
def require_role(self, token: str, required_role: str) -> bool:
|
|
369
|
+
"""Check if token has required role"""
|
|
370
|
+
payload = self.verify_token(token)
|
|
371
|
+
if not payload:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
# Admin has access to everything
|
|
375
|
+
if payload.role == "admin":
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
return payload.role == required_role
|
|
379
|
+
|
|
380
|
+
def is_admin(self, token: str) -> bool:
|
|
381
|
+
"""Check if token belongs to admin"""
|
|
382
|
+
return self.require_role(token, "admin")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Singleton instance
|
|
386
|
+
_auth_service = None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def get_auth_service() -> AuthService:
|
|
390
|
+
"""Get singleton instance of AuthService"""
|
|
391
|
+
global _auth_service
|
|
392
|
+
if _auth_service is None:
|
|
393
|
+
_auth_service = AuthService()
|
|
394
|
+
return _auth_service
|