Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions _audit_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Audit all Thin Wrapper Serializers for Meta.model=None bug when EE is absent."""
import os, sys, traceback
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import django
django.setup()

from taurus import serializers as ser_module
import inspect

# Grep: all classes defined in taurus.serializers that have a Meta inner class
# We will check Meta.model and flag those with None (or missing)

SERIALIZERS_TO_CHECK = [
# M2.2 Workflow DAG
'WorkflowDAGVersionSerializer',
# M2.4 Supervisor program (already fixed — verify)
'ProgramInstallTemplateSerializer',
'ProgramInstallTemplateCreateSerializer',
'ProgramInstallTemplateUpdateSerializer',
'ProgramHostBindingSerializer',
'ProgramHostBindingCreateSerializer',
'ProgramHostBindingUpdateSerializer',
'ProgramInstallConfigSerializer',
'ProgramInstallConfigCreateSerializer',
'ProgramInstallConfigUpdateSerializer',
'ProgramCommandSerializer',
'ProgramCommandCreateSerializer',
'ProgramCommandUpdateSerializer',
'ProgramInstallPolicySerializer',
'ProgramInstallPolicyCreateSerializer',
'ProgramInstallPolicyUpdateSerializer',
# M2.5 HostLog / LogCommand
'HostLogSerializer',
'HostLogReceiveSerializer',
'LogCommandSerializer',
'LogCommandCreateSerializer',
'LogCommandUpdateSerializer',
# Ops approval
'OpsExecutionApprovalSerializer',
'OpsExecutionApprovalListSerializer',
'OpsExecutionApprovalCreateSerializer',
'OpsExecutionApprovalActionSerializer',
# Script library EE
'ScriptAuditSerializer',
'ScriptApproveSerializer',
'ScriptApprovalRuleSerializer',
'ScriptApprovalNodeSerializer',
'ScriptApprovalInstanceSerializer',
'ScriptApprovalNodeExecutionSerializer',
'ScriptCheckRuleSerializer',
# Share permission
'SharePermissionDefSerializer',
'ScriptSharePermissionSerializer',
'WorkflowSharePermissionSerializer',
'SharePermissionBatchCreateSerializer',
'ShareLinkSerializer',
'ShareLinkActivateSerializer',
'ShareLinkAccessLogSerializer',
# Workflow approval
'WorkflowApproveSerializer',
'WorkflowApproveCreateSerializer',
'WorkflowApproveApproveSerializer',
'WorkflowApproveRejectSerializer',
'WorkflowApproveCompatSerializer',
'WorkflowApprovalRuleSerializer',
'WorkflowApprovalNodeSerializer',
'WorkflowApprovalInstanceSerializer',
'WorkflowApprovalNodeExecutionSerializer',
# Scheduler HA
'ScheduleExecutionHASerializer',
'ScriptTaskExecutionUnifiedSerializer',
'SchedulerAlertRuleSerializer',
'UnifiedScheduleListRequestSerializer',
'UnifiedScheduleStatsSerializer',
'SchedulerAlertEventSerializer',
# Extension center placeholders
'ContactLeadSerializer',
'TaskCenterItemSerializer',
'KnowledgeBasePlaceholderSerializer',
'InspectionCenterPlaceholderSerializer',
'ToolsCenterPlaceholderSerializer',
'BackupRestoreCenterPlaceholderSerializer',
'DownloadCenterPlaceholderSerializer',
]

problems = []
oks = []
for name in SERIALIZERS_TO_CHECK:
cls = getattr(ser_module, name, None)
if cls is None:
problems.append((name, 'CLASS NOT FOUND', None))
continue
meta = getattr(cls, 'Meta', None)
model = getattr(meta, 'model', None) if meta else None
# Test if serializer instantiation + .fields access works (this triggers DRF get_field_info)
try:
inst = cls()
_ = inst.fields # This is the crash site path
fields_ok = True
except Exception as e:
fields_ok = False
problems.append((name, f'fields lookup ERROR: {type(e).__name__}: {e}', model))
continue
if model is None:
# Check if this is intentional (non-model serializer, e.g. serializers.Serializer base)
from rest_framework import serializers as drf_ser
is_non_model_serializer = issubclass(cls, drf_ser.Serializer) and not issubclass(cls, drf_ser.ModelSerializer)
if is_non_model_serializer:
oks.append((name, f'OK (non-model Serializer)', model))
else:
problems.append((name, 'Meta.model = None (but ModelSerializer subclass)', model))
else:
oks.append((name, f'OK model={model.__name__}', model))

print('='*90)
print(f'CHECKED {len(SERIALIZERS_TO_CHECK)} SERIALIZERS — {len(problems)} PROBLEMS, {len(oks)} OK')
print('='*90)
if problems:
print('\n[PROBLEMS — likely cause HTTP 400/500 when ViewSet has no EE gate]:')
for p in problems:
print(f' ⚠️ {p[0]:<55s} | {p[1]} | model={p[2]}')
print()
print('[OK]:')
for o in oks:
print(f' ✅ {o[0]:<55s} | {o[1]}')
26 changes: 25 additions & 1 deletion application/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,31 @@
# ******************** Dynamic Config ******************** #
# ================================================= #

from conf.env import *
try:
from conf.env import *
except ModuleNotFoundError:
import logging as _lg
_lg.getLogger(__name__).warning(
"conf.env module not found, using default settings (SQLite). "
"Copy conf/env.example.py to conf/env.py and customize it for production use."
)
DATABASE_ENGINE = "django.db.backends.sqlite3"
DATABASE_NAME = os.path.join(BASE_DIR, "db.sqlite3")
DATABASE_USER = ""
DATABASE_PASSWORD = ""
DATABASE_HOST = ""
DATABASE_PORT = ""
TABLE_PREFIX = ""
REDIS_DB = 0
CELERY_BROKER_DB = 1
REDIS_PASSWORD = ""
REDIS_HOST = "127.0.0.1"
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
DEBUG = True
ENABLE_LOGIN_ANALYSIS_LOG = True
LOGIN_NO_CAPTCHA_AUTH = True
ALLOWED_HOSTS = ["*"]
COLUMN_EXCLUDE_APPS = []

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
Expand Down
129 changes: 121 additions & 8 deletions conf/env.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

from application.settings import BASE_DIR

# ================================================= #
# *************** Config Decryption Helpers *************** #
# ================================================= #
def _decrypt_if_needed(value):
"""Automatically decrypt config values (if encrypted)"""
if not value or not isinstance(value, str):
return value

# Check if value is in encrypted format
if not value.startswith('fernet:'):
return value

try:
from taurus.config_crypto import auto_decrypt
return auto_decrypt(value)
except Exception as e:
# Log warning on decryption failure, return original value
import logging
logging.getLogger(__name__).warning(f"Config decryption failed: {e}")
return value

# ================================================= #
# *************** MySQL Database Config *************** #
# ================================================= #
Expand All @@ -20,19 +41,22 @@
DATABASE_PORT = 3306
# # Database username
DATABASE_USER = "root"
# # Database password
DATABASE_PASSWORD = 'AOADMIN3'
# # Database password (supports encrypted format, auto-decrypted on startup)
DATABASE_PASSWORD = _decrypt_if_needed(os.environ.get(
'DATABASE_PASSWORD',
'change_me' # Dev env plaintext, use encrypted format in production
))

# Table prefix
TABLE_PREFIX = "taurus_"
# ================================================= #
# ******** Redis Config, can skip if no Redis ******** #
# ================================================= #
REDIS_DB = 1
CELERY_BROKER_DB = 3
REDIS_PASSWORD = 'AOADMIN3'
REDIS_HOST = '127.0.0.1'
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
# REDIS_DB = 1
# CELERY_BROKER_DB = 3
# REDIS_PASSWORD = _decrypt_if_needed(os.environ.get('REDIS_PASSWORD', 'change_me'))
# REDIS_HOST = '127.0.0.1'
# REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
# ================================================= #
# ****************** Feature Toggles ******************* #
# ================================================= #
Expand All @@ -41,10 +65,99 @@
ENABLE_LOGIN_ANALYSIS_LOG = True
# Login interface /api/token/ does not require captcha auth, for testing; production environment recommends disabling
LOGIN_NO_CAPTCHA_AUTH = True

# ================================================= #
# ***************** Workflow Engine Config *************** #
# ================================================= #
# List of ExecutableUnit adapters auto-imported on startup (just specify module path, triggers their @register_unit_adapter decorator)
# Adding a new adapter only requires appending one line to this list; no engine code changes needed
INSTALLED_UNIT_ADAPTERS: list[str] = [
# Core built-in adapters (bundled with taurus code, implemented progressively during S3 phase)
"taurus.workflow.units.ops_execution", # S2-01/S2-02/S2-03: script + command + file_op adapters (reuse OpsExecution execution pipeline)
"taurus.workflow.units.virtual", # S2-07: start + end + noop virtual nodes
"taurus.workflow.units.program", # S2-04: Program management (install/upgrade/start/stop/restart/remove)
"taurus.workflow.units.approval", # S2-05: Manual approval (async node, is_asynchronous_human=True)
"taurus.workflow.units.sub_workflow", # S2-06: Sub-workflow (recursively triggers WorkflowExecution)
"taurus.workflow.units.http_callback", # S4-01: HTTP callback (externally driven async)
"taurus.workflow.units.condition", # S4-02: Condition branching (flow control)
"taurus.workflow.units.transform", # S4-03: Data transformation (JSONPath/Python/regex)
"taurus.workflow.units.http", # S4-04: Synchronous HTTP request
"taurus.workflow.units.loop", # S4-05: Loop control node
"taurus.workflow.units.webhook_notification", # S4-06: Webhook notification
"taurus.workflow.units.email_notification", # S4-07: Email notification
"taurus.workflow.units.wait", # S4-08: Wait (delay / absolute time / daily schedule)
# "taurus.workflow.units.schedule_gate", # Schedule/window gate (e.g. only allow 00:00~05:00)
]

# testing_* placeholder adapters load only in DEBUG dev environment, to avoid test nodes appearing on the production node panel
if DEBUG:
INSTALLED_UNIT_ADAPTERS = [
"taurus.workflow.units._testing_noop", # Unit test only: always success / output examples
"taurus.workflow.units._testing_fail", # Unit test only: always fail / error code examples
] + INSTALLED_UNIT_ADAPTERS

# ================================================= #
# ****************** Email Notification Config ****************** #
# ================================================= #
# Dev env defaults to console backend (email content printed to backend logs, not actually sent), to avoid local SMTP restrictions
# Production env switches to real SMTP via environment variables:
# EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
# EMAIL_HOST=smtp.example.com EMAIL_PORT=465 EMAIL_USE_SSL=true
# EMAIL_HOST_USER=xxx EMAIL_HOST_PASSWORD=xxx DEFAULT_FROM_EMAIL=xxx
EMAIL_BACKEND = os.environ.get(
'EMAIL_BACKEND',
'django.core.mail.backends.console.EmailBackend'
)
EMAIL_HOST = os.environ.get('EMAIL_HOST', '127.0.0.1')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = _decrypt_if_needed(os.environ.get('EMAIL_HOST_PASSWORD', ''))
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', '25'))
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'false').lower() == 'true'
EMAIL_USE_SSL = os.environ.get('EMAIL_USE_SSL', 'false').lower() == 'true'
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'no-reply@example.com')

# ================================================= #
# ****************** Other Config ******************* #
# ================================================= #

ALLOWED_HOSTS = ["*"]
# Exclude apps from column permissions
COLUMN_EXCLUDE_APPS = []
COLUMN_EXCLUDE_APPS = []

# ================================================= #
# ****************** Request Signing Config ****************** #
# ================================================= #
# Whether to enable request signature verification (Supervisor heartbeat and command reporting endpoints)
# When enabled, newly registered hosts auto-generate a signing key
# Enabled by default for API security
REQUEST_SIGNING_ENABLED = os.environ.get('REQUEST_SIGNING_ENABLED', 'true').lower() == 'true'

# Signing master key (supports encrypted format, auto-decrypted on startup)
# Production must set the encrypted key via environment variable
SIGNING_MASTER_KEY = _decrypt_if_needed(os.environ.get(
'SIGNING_MASTER_KEY',
'change-me-to-a-secure-master-key'
))

# ================================================= #
# ****************** SSL/TLS Config ****************** #
# ================================================= #
# Whether to enable SSL verification (validates server certificate on Supervisor heartbeat requests)
# Disabled by default, dev env uses HTTP
# Production must set to true and configure certificates
SUPERVISOR_SSL_VERIFY = os.environ.get('SUPERVISOR_SSL_VERIFY', 'false').lower() == 'true'

# Custom CA certificate path (optional, needed for self-signed certificates)
SUPERVISOR_SSL_CA_PATH = os.environ.get('SUPERVISOR_SSL_CA_PATH', '')

# ================================================= #
# ****************** Server Cert SAN Config ************ #
# ================================================= #
# Unified gRPC service name (server cert SAN + client ssl_target_name_override)
# Uses fixed DNS name + client target name override to bypass IP hostname verification (Option 2: bare IP direct connect)
GRPC_SERVER_CERT_SAN = os.environ.get('GRPC_SERVER_CERT_SAN', 'taurus-grpc-server')

# Server certificate SAN list config (deprecated, retained for backward compatibility)
# Now uniformly uses GRPC_SERVER_CERT_SAN, no longer writes IP/hostnames into SAN
# Format: comma-separated, e.g. "DNS:localhost,IP:127.0.0.1,DNS:server1.example.com"
SERVER_CERT_SAN_LIST = os.environ.get('SERVER_CERT_SAN_LIST', '')
4 changes: 2 additions & 2 deletions taurus/editions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def info_payload(self) -> Dict[str, Any]:
"quota": self.quota,
"license": self.license_status,
"upgrade": {
"show_banner": self.edition_name == "community",
"show_banner": False,
"contact_url": "/#/taurus/contact-lead", # 前端路由占位
},
}
}
15 changes: 7 additions & 8 deletions taurus/ee_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class SomeViewSet(_EESomeViewSet):
from typing import Any, Callable, Optional

from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.serializers import CustomModelSerializer

from rest_framework.exceptions import PermissionDenied
from rest_framework.serializers import Serializer
from taurus.editions import describe_feature


Expand All @@ -51,17 +51,16 @@ def dispatch(self, request, *args, **kwargs):
# ---------------------------------------------------------------------------
# Serializer fallback
# ---------------------------------------------------------------------------
class _EEFallbackSerializer(CustomModelSerializer):
class _EEFallbackSerializer(Serializer):
"""EE Serializer 缺失时的 fallback 父类。

在 Thin Wrapper 继承链中占位——仅用于让 Python class 定义不崩溃。
实际运行时 ViewSet dispatch 会先被 EditionGate / _EEFallbackViewSet
继承普通 Serializer(非 ModelSerializer),避免 drf-spectacular schema
扫描时触发 model_meta.get_field_info(None) 崩溃。
运行时 ViewSet dispatch 会先被 EditionGate / _EEFallbackViewSet
拦截到 403,永远不会走到 Serializer 实例化。
"""

class Meta:
model = None
fields = []
pass


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -94,4 +93,4 @@ def ee_service_or_none(service_name: str) -> Optional[Any]:
"_EEFallbackSerializer",
"ee_service_or_403",
"ee_service_or_none",
]
]
Loading
Loading