diff --git a/_audit_serializers.py b/_audit_serializers.py new file mode 100644 index 0000000..f10f993 --- /dev/null +++ b/_audit_serializers.py @@ -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]}') \ No newline at end of file diff --git a/application/settings.py b/application/settings.py index 76540fe..c7bbf31 100644 --- a/application/settings.py +++ b/application/settings.py @@ -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/ diff --git a/conf/env.example.py b/conf/env.example.py index 2d7607d..3f55d11 100644 --- a/conf/env.example.py +++ b/conf/env.example.py @@ -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 *************** # # ================================================= # @@ -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 ******************* # # ================================================= # @@ -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 = [] \ No newline at end of file +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', '') \ No newline at end of file diff --git a/taurus/editions/base.py b/taurus/editions/base.py index 127c03a..cf22844 100644 --- a/taurus/editions/base.py +++ b/taurus/editions/base.py @@ -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", # 前端路由占位 }, - } + } \ No newline at end of file diff --git a/taurus/ee_fallback.py b/taurus/ee_fallback.py index f04be07..1846e52 100644 --- a/taurus/ee_fallback.py +++ b/taurus/ee_fallback.py @@ -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 @@ -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 # --------------------------------------------------------------------------- @@ -94,4 +93,4 @@ def ee_service_or_none(service_name: str) -> Optional[Any]: "_EEFallbackSerializer", "ee_service_or_403", "ee_service_or_none", -] +] \ No newline at end of file diff --git a/taurus/serializers.py b/taurus/serializers.py index cd12e2e..c664830 100644 --- a/taurus/serializers.py +++ b/taurus/serializers.py @@ -6,19 +6,11 @@ from dvadmin.utils.validator import CustomUniqueValidator from .models import ( Workflow, WorkflowCategory, WorkflowStep, WorkflowExecution, WorkflowStepExecution, - WorkflowApprove, - WorkflowApprovalRule, WorkflowApprovalNode, WorkflowApprovalInstance, WorkflowApprovalNodeExecution, Host, Schedule, ScheduleExecution, RegistrationToken, HostHeartbeat, HeartbeatServer, ProgramInstallConfig, ProgramInstallPolicy, ProgramCommand, - HostLog, ManagedProgram, ProgramInstallTemplate, ProgramHostBinding, - LogCommand, OpsExecution, OpsExecutionApproval, - ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, + ManagedProgram, ProgramInstallTemplate, ProgramHostBinding, + OpsExecution, ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, ScriptTaskExecution, - ScriptAudit, ScriptApprove, - ScriptApprovalRule, ScriptApprovalNode, ScriptApprovalInstance, ScriptApprovalNodeExecution, - ScriptCheckRule, - SharePermissionDef, ScriptSharePermission, WorkflowSharePermission, - ShareLink, ShareLinkAccessLog, ) @@ -842,18 +834,34 @@ class WorkflowExecuteSerializer(serializers.Serializer): # [M2.2 Thin Wrapper] WorkflowDAGVersionSerializer → EE 实现 +# +# 先提供 CE 基础版本(真实 Meta.model),否则 EE 缺失时 WorkflowViewSet.publish +# (CE 功能,无 EE Gate)在成功返回时会访问 WorkflowDAGVersionSerializer(dag_ver).data +# 触发 DRF get_field_info → AttributeError: 'NoneType' object has no attribute '_meta'。 +from taurus.workflow.models import WorkflowDAGVersion as _WorkflowDAGVersionModel # noqa: E402 (circular import guard) +class WorkflowDAGVersionSerializer(CustomModelSerializer): + """Workflow DAG published version serializer (CE base — EE overrides via Thin Wrapper below).""" + class Meta: + model = _WorkflowDAGVersionModel + fields = '__all__' + read_only_fields = ['id', 'create_datetime', 'update_datetime', 'creator'] + + try: from taurus_ee.serializers.workflow_dag import ( WorkflowDAGVersionSerializer as _EEWorkflowDAGVersionSerializer, ) + _EE_WORKFLOW_DAG_SER_OK = True except ImportError: + _EE_WORKFLOW_DAG_SER_OK = False from taurus.ee_fallback import _EEFallbackSerializer as _FBSer class _EEWorkflowDAGVersionSerializer(_FBSer): pass -class WorkflowDAGVersionSerializer(_EEWorkflowDAGVersionSerializer): - """Thin wrapper — EE implementation in taurus_ee.serializers.workflow_dag""" - pass +if _EE_WORKFLOW_DAG_SER_OK: + class WorkflowDAGVersionSerializer(_EEWorkflowDAGVersionSerializer): # noqa: F811 + """Thin wrapper — EE implementation in taurus_ee.serializers.workflow_dag""" + pass class WorkflowNodeExecutionSerializer(CustomModelSerializer): @@ -1269,6 +1277,12 @@ class Meta: # [M2.4 Thin Wrapper] Supervisor 程序管理高级 Serializers(F_PROGRAM_INSTALL_TEMPLATE / # F_PROGRAM_HOST_BINDING / F_PROGRAM_INSTALL_CONFIG / F_PROGRAM_COMMAND_BATCH / # F_PROGRAM_INSTALL_POLICY — EE 专属。原类名保留不动,thin pass 继承 taurus_ee.*。 +# +# 注意:这些 Serializer 均已在上方定义了 CE 基础版本(含正确的 Meta.model)。 +# Thin Wrapper 仅在 EE 成功导入时才重新定义(继承 EE 扩展);若 EE 缺失, +# 必须保留原 CE 版本不变,否则继承 _EEFallbackSerializer (model=None) 会导致 +# DRF 在 get_field_info 阶段抛出 AttributeError: 'NoneType' object has no attribute '_meta' +# (尤其是 ProgramCommandViewSet,其基础 CRUD 不走 EE Gate)。 # ============================================================================ try: from taurus_ee.serializers.supervisor_program import ( @@ -1288,7 +1302,9 @@ class Meta: _EEProgramInstallPolicyCreateSerializer as _EE_PIP_CreateSer, _EEProgramInstallPolicyUpdateSerializer as _EE_PIP_UpdateSer, ) + _EE_SUPERVISOR_PROGRAM_OK = True except ImportError: + _EE_SUPERVISOR_PROGRAM_OK = False from taurus.ee_fallback import _EEFallbackSerializer as _FBSer class _EE_PC_CreateSer(_FBSer): pass class _EE_PC_Ser(_FBSer): pass @@ -1307,24 +1323,30 @@ class _EE_PIT_Ser(_FBSer): pass class _EE_PIT_UpdateSer(_FBSer): pass -class ProgramInstallTemplateSerializer(_EE_PIT_Ser): pass # noqa: E701 Thin Wrapper -class ProgramInstallTemplateCreateSerializer(_EE_PIT_CreateSer): pass # noqa: E701 -class ProgramInstallTemplateUpdateSerializer(_EE_PIT_UpdateSer): pass # noqa: E701 -class ProgramHostBindingSerializer(_EE_PHB_Ser): pass # noqa: E701 -class ProgramHostBindingCreateSerializer(_EE_PHB_CreateSer): pass # noqa: E701 -class ProgramHostBindingUpdateSerializer(_EE_PHB_UpdateSer): pass # noqa: E701 -class ProgramInstallConfigSerializer(_EE_PIC_Ser): pass # noqa: E701 -class ProgramInstallConfigCreateSerializer(_EE_PIC_CreateSer): pass # noqa: E701 -class ProgramInstallConfigUpdateSerializer(_EE_PIC_UpdateSer): pass # noqa: E701 -class ProgramCommandSerializer(_EE_PC_Ser): pass # noqa: E701 -class ProgramCommandCreateSerializer(_EE_PC_CreateSer): pass # noqa: E701 -class ProgramCommandUpdateSerializer(_EE_PC_UpdateSer): pass # noqa: E701 -class ProgramInstallPolicySerializer(_EE_PIP_Ser): pass # noqa: E701 -class ProgramInstallPolicyCreateSerializer(_EE_PIP_CreateSer): pass # noqa: E701 -class ProgramInstallPolicyUpdateSerializer(_EE_PIP_UpdateSer): pass # noqa: E701 +if _EE_SUPERVISOR_PROGRAM_OK: + class ProgramInstallTemplateSerializer(_EE_PIT_Ser): pass # noqa: E701 Thin Wrapper + class ProgramInstallTemplateCreateSerializer(_EE_PIT_CreateSer): pass # noqa: E701 + class ProgramInstallTemplateUpdateSerializer(_EE_PIT_UpdateSer): pass # noqa: E701 + class ProgramHostBindingSerializer(_EE_PHB_Ser): pass # noqa: E701 + class ProgramHostBindingCreateSerializer(_EE_PHB_CreateSer): pass # noqa: E701 + class ProgramHostBindingUpdateSerializer(_EE_PHB_UpdateSer): pass # noqa: E701 + class ProgramInstallConfigSerializer(_EE_PIC_Ser): pass # noqa: E701 + class ProgramInstallConfigCreateSerializer(_EE_PIC_CreateSer): pass # noqa: E701 + class ProgramInstallConfigUpdateSerializer(_EE_PIC_UpdateSer): pass # noqa: E701 + class ProgramCommandSerializer(_EE_PC_Ser): pass # noqa: E701 + class ProgramCommandCreateSerializer(_EE_PC_CreateSer): pass # noqa: E701 + class ProgramCommandUpdateSerializer(_EE_PC_UpdateSer): pass # noqa: E701 + class ProgramInstallPolicySerializer(_EE_PIP_Ser): pass # noqa: E701 + class ProgramInstallPolicyCreateSerializer(_EE_PIP_CreateSer): pass # noqa: E701 + class ProgramInstallPolicyUpdateSerializer(_EE_PIP_UpdateSer): pass # noqa: E701 # ------------------ [M2.5 Thin Wrapper] HostLog & LogCommand (log center EE) ------------------ +# 注意:这些 Serializer 均被 taurus/views.py 顶层 `from taurus.serializers import X` 导入, +# 因此 class 名称必须始终存在(否则 ImportError)。好在 HostLogViewSet / LogCommandViewSet / +# TaskCenterViewSet 等都带 dispatch 级 EE Gate(403 拦截),即使 EE 缺失时 Thin Wrapper +# 继承 Meta.model=None 的 _EEFallbackSerializer 也不会真正走到 Serializer 实例化。 +# 因此这里不使用 if-guard 做 class 重定义覆盖,直接无条件 Thin Wrapper 即可。 try: from taurus_ee.serializers.log_ext_center import ( _EEHostLogSerializer, @@ -1381,6 +1403,7 @@ class Meta: + class LogCommandSerializer(_EELogCommandSerializer): """Thin Wrapper — EE impl: taurus_ee.serializers.log_ext_center._EELogCommandSerializer""" pass @@ -1653,6 +1676,9 @@ def get_duration(self, obj): # ========================== Execution Task Approval Serializers (Thin Wrapper, 真实实现迁 taurus_ee.serializers.ops_approval) ========================== +# 注意:OpsExecutionApproval 四个 Serializer 均被 taurus/views.py 顶层 `from taurus.serializers import X` +# 导入,因此 class 名称必须始终存在(否则 ImportError)。相关 ViewSet 均带 dispatch 级 EE Gate +# (403 拦截),EE 缺失时 Thin Wrapper 继承 Meta.model=None 的 fallback 也不会走到实例化。 try: from taurus_ee.serializers.ops_approval import ( _EEOpsExecutionApprovalSerializer, @@ -2133,6 +2159,14 @@ def get_host_summary(self, obj) -> dict: # Script* EE 系列 Thin Wrapper(实现在 taurus_ee.serializers.*) # 保留原名:taurus/urls.py + views.py 中 `from taurus.serializers import X` # 所有旧引用保持 0 改动。 +# +# 策略:这些 Serializer 绝大多数为 EE 专属、由 dispatch 级 EE Gate 保护(403 拦截); +# 少数(SharePermissionDefSerializer 等 4 个 + TaskCenterItemSerializer)被 views.py +# 顶层 `from taurus.serializers import X` 导入,因此 class 名称必须始终存在(否则 ImportError)。 +# 为保持一致性与最小风险,本块所有 Thin Wrapper 一律无条件定义(移除 _EE_*_OK 守卫), +# EE 缺失时继承 Meta.model=None 的 fallback 类不会真正走到 Serializer 实例化。 +# 只有 M2.4(ProgramCommand 等已有 CE 基础类)与 WorkflowDAGVersion 保留守卫, +# 目的是"不要覆盖掉原本就存在的正确 CE 类定义(含真实 Meta.model)"。 # ================================================================ try: from taurus_ee.serializers.script_audit import ScriptAuditSerializer as _EEScriptAuditSerializer @@ -2382,9 +2416,24 @@ class SchedulerAlertEventSerializer(_EESchedulerAlertEventSerializer): pass -class ContactLeadSerializer(_EEContactLeadSerializer): - """Thin Wrapper — EE impl: taurus_ee.serializers.log_ext_center._EEContactLeadSerializer""" - pass +class ContactLeadSerializer(CustomModelSerializer): + """Portal contact form submission serializer. + + CE-compatible: this serializer is used by ContactLeadViewSet.create which + is NOT gated (AllowAny, anonymous portal POST). Must NOT inherit from + _EEFallbackSerializer (Meta.model=None) — drf-spectacular schema scan and + real CE traffic both need an instantiable ModelSerializer. + + EE override: taurus_ee may shadow _EEContactLeadSerializer with extended + fields, but ContactLeadSerializer stays CE-safe as the canonical path. + """ + + class Meta: + from taurus.models import ContactLead + + model = ContactLead + fields = '__all__' + read_only_fields = ('source', 'ip', 'user_agent', 'create_datetime', 'update_datetime') # -------- 6 extension center placeholder Ser (M2.5 EE 空壳) -------- diff --git a/taurus/urls.py b/taurus/urls.py index 06a9cb4..fc26beb 100644 --- a/taurus/urls.py +++ b/taurus/urls.py @@ -20,6 +20,7 @@ ContactLeadViewSet, LightweightUserOptionsView, workflow_callback_view, + _WFAPP_EE_OK, _SCRIPT_EE_OK, ) from taurus.views_edition import EditionInfoView, EditionFeaturesView, EditionDescribeView @@ -29,11 +30,11 @@ router.register('workflow', WorkflowViewSet, basename='workflow') router.register('workflow-category', WorkflowCategoryViewSet, basename='workflow-category') router.register('workflow-execution', WorkflowExecutionViewSet, basename='workflow-execution') -router.register('workflow-approve', WorkflowApproveViewSet, basename='workflow-approve') -# Workflow approval rules (rule-driven approval) -router.register('workflow-approval-rule', WorkflowApprovalRuleViewSet, basename='workflow-approval-rule') -router.register('workflow-approval-node', WorkflowApprovalRuleNodeViewSet, basename='workflow-approval-node') -router.register('workflow-approval-instance', WorkflowApprovalInstanceViewSet, basename='workflow-approval-instance') +if _WFAPP_EE_OK: + router.register('workflow-approve', WorkflowApproveViewSet, basename='workflow-approve') + router.register('workflow-approval-rule', WorkflowApprovalRuleViewSet, basename='workflow-approval-rule') + router.register('workflow-approval-node', WorkflowApprovalRuleNodeViewSet, basename='workflow-approval-node') + router.register('workflow-approval-instance', WorkflowApprovalInstanceViewSet, basename='workflow-approval-instance') # Scheduled task management router.register('schedule', ScheduleViewSet, basename='schedule') @@ -106,20 +107,18 @@ # Script audit log management router.register('script-audit', ScriptAuditViewSet, basename='script-audit') -# Script approval management -router.register('script-approve', ScriptApproveViewSet, basename='script-approve') - -# Script approval rules (rule-driven approval) -router.register('script-approval-rule', ScriptApprovalRuleViewSet, basename='script-approval-rule') -router.register('script-approval-node', ScriptApprovalRuleNodeViewSet, basename='script-approval-node') -router.register('script-approval-instance', ScriptApprovalInstanceViewSet, basename='script-approval-instance') - -# Script check rules -router.register('script-check-rule', ScriptCheckRuleViewSet, basename='script-check-rule') - -# Sharing: permission dictionary & share link -router.register('share-perm-def', SharePermissionDefViewSet, basename='share-perm-def') -router.register('share-link', ShareLinkViewSet, basename='share-link') +if _SCRIPT_EE_OK: + # Script approval management + router.register('script-approve', ScriptApproveViewSet, basename='script-approve') + # Script approval rules (rule-driven approval) + router.register('script-approval-rule', ScriptApprovalRuleViewSet, basename='script-approval-rule') + router.register('script-approval-node', ScriptApprovalRuleNodeViewSet, basename='script-approval-node') + router.register('script-approval-instance', ScriptApprovalInstanceViewSet, basename='script-approval-instance') + # Script check rules + router.register('script-check-rule', ScriptCheckRuleViewSet, basename='script-check-rule') + # Sharing: permission dictionary & share link + router.register('share-perm-def', SharePermissionDefViewSet, basename='share-perm-def') + router.register('share-link', ShareLinkViewSet, basename='share-link') # Task center (aggregate script scheduled tasks + scheduled workflows) router.register('task-center', TaskCenterViewSet, basename='task-center') @@ -163,22 +162,36 @@ path('script//effective-perms/', ScriptViewSet.as_view({'get': 'effective_perms'})), # Script scheduled task custom actions path('script-task//toggle/', ScriptTaskViewSet.as_view({'post': 'toggle_enabled'})), - # Script approval custom actions - path('script-approve//approve/', ScriptApproveViewSet.as_view({'post': 'approve'})), - path('script-approve//reject/', ScriptApproveViewSet.as_view({'post': 'reject'})), +] + +if _SCRIPT_EE_OK: + urlpatterns += [ + # Script approval custom actions + path('script-approve//approve/', ScriptApproveViewSet.as_view({'post': 'approve'})), + path('script-approve//reject/', ScriptApproveViewSet.as_view({'post': 'reject'})), + ] + +urlpatterns += [ # Workflow orchestration custom actions path('workflow//toggle-status/', WorkflowViewSet.as_view({'post': 'toggle_status'})), path('workflow//copy/', WorkflowViewSet.as_view({'post': 'copy_workflow'})), path('workflow//submit-approve/', WorkflowViewSet.as_view({'post': 'submit_approve'})), path('workflow//risk-assessment/', WorkflowViewSet.as_view({'get': 'risk_assessment', 'post': 'risk_assessment'})), path('workflow/stats/', WorkflowViewSet.as_view({'get': 'get_stats'})), - # Workflow share permission related + # Workflow share permission related (community 版可用) path('workflow//shares/', WorkflowViewSet.as_view({'get': 'shares', 'post': 'shares'})), path('workflow//shares//', WorkflowViewSet.as_view({'put': 'share_detail', 'delete': 'share_detail'})), path('workflow//effective-perms/', WorkflowViewSet.as_view({'get': 'effective_perms'})), - # Workflow orchestration approval custom actions - path('workflow-approve//approve/', WorkflowApproveViewSet.as_view({'post': 'approve'})), - path('workflow-approve//reject/', WorkflowApproveViewSet.as_view({'post': 'reject'})), +] + +if _WFAPP_EE_OK: + urlpatterns += [ + # Workflow orchestration approval custom actions + path('workflow-approve//approve/', WorkflowApproveViewSet.as_view({'post': 'approve'})), + path('workflow-approve//reject/', WorkflowApproveViewSet.as_view({'post': 'reject'})), + ] + +urlpatterns += [ # -------- Edition Gate (M1):edition 能力查询 -------- path('edition/info/', EditionInfoView.as_view()), path('edition/features/', EditionFeaturesView.as_view()), diff --git a/taurus/views.py b/taurus/views.py index 9552a1b..68e86cb 100644 --- a/taurus/views.py +++ b/taurus/views.py @@ -15,21 +15,16 @@ from taurus.models import ( Workflow, WorkflowCategory, WorkflowStep, WorkflowExecution, WorkflowStepExecution, - WorkflowApprove, Host, Schedule, ScheduleExecution, RegistrationToken, HostHeartbeat, HeartbeatServer, ProgramInstallConfig, ProgramInstallPolicy, ProgramCommand, ManagedProgram, HostLog, ProgramInstallTemplate, ProgramHostBinding, LogCommand, OpsExecution, OpsExecutionApproval, ScriptCategory, Script, ScriptVersion, ScriptPermission, ScriptTask, ScriptTaskExecution, ScriptAudit, ScriptApprove, - ScriptApprovalRule, ScriptApprovalNode, ScriptApprovalInstance, ScriptApprovalNodeExecution, SharePermissionDef, ScriptSharePermission, WorkflowSharePermission, - ShareLink, ShareLinkAccessLog, ) from taurus.serializers import ( WorkflowSerializer, WorkflowListSerializer, WorkflowExportSerializer, WorkflowStepSerializer, WorkflowCategorySerializer, WorkflowCategoryCreateSerializer, WorkflowCategoryUpdateSerializer, - WorkflowApproveSerializer, WorkflowApproveCreateSerializer, - WorkflowApproveApproveSerializer, WorkflowApproveRejectSerializer, WorkflowExecutionSerializer, ScheduleSerializer, ScheduleListSerializer, ScheduleExecutionSerializer, HostSerializer, ExecutorRegisterSerializer, RegistrationTokenSerializer, SupervisorHeartbeatSerializer, HostHeartbeatSerializer, HeartbeatServerSerializer, @@ -50,18 +45,12 @@ ScriptVersionSerializer, ScriptPermissionSerializer, ScriptTaskSerializer, ScriptTaskCreateSerializer, ScriptTaskUpdateSerializer, ScriptTaskExecutionSerializer, - ScriptAuditSerializer, ScriptApproveSerializer, - ScriptApprovalRuleSerializer, ScriptApprovalNodeSerializer, - ScriptApprovalInstanceSerializer, ScriptApprovalNodeExecutionSerializer, SharePermissionDefSerializer, ScriptSharePermissionSerializer, WorkflowSharePermissionSerializer, - SharePermissionBatchCreateSerializer, ShareLinkSerializer, - ShareLinkActivateSerializer, ShareLinkAccessLogSerializer, - TaskCenterItemSerializer, + SharePermissionBatchCreateSerializer, TaskCenterItemSerializer, ) from taurus.utils.share_permission import ( SharePermissionChecker, - ShareLinkService, ShareVisibleQS, require_share_perm, ) @@ -75,7 +64,6 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response from rest_framework import status as drf_status -from rest_framework.permissions import AllowAny logger = logging.getLogger(__name__)