From 24520f1e8b49f4c704d938cb6e752baa0eef083d Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:48:54 +0800 Subject: [PATCH 1/8] perf: add ANSIBLE_DOCKER_ENABLED configuration (#16920) * perf: test ansible executor github ci * perf: for test ci * fix: update Dockerfile to include docker-cli and change ansible container network to jms_net * fix: Removed cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES deprecation warning * perf: update ansible and ansible-runner versions in execution environment and requirements * fix: local python interpreter error * perf: add ANSIBLE_DOCKER_ENABLED configuration * perf: add latest tag for ansible-executor image in build workflow --------- Co-authored-by: wangruidong <940853815@qq.com> --- .github/workflows/build-ansible-executor.yml | 4 +- Dockerfile | 3 +- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 1 + apps/libs/ansible/ansible.cfg | 2 +- apps/ops/ansible/docker.py | 76 +++++++++++++++++++ apps/ops/ansible/exception.py | 6 +- apps/ops/ansible/runner.py | 56 +++++--------- apps/settings/serializers/feature.py | 14 ++++ .../execution-environment.yml | 4 +- .../ansible_executor/requirements-python.txt | 8 +- 11 files changed, 129 insertions(+), 46 deletions(-) create mode 100644 apps/ops/ansible/docker.py diff --git a/.github/workflows/build-ansible-executor.yml b/.github/workflows/build-ansible-executor.yml index 5e9d456ad..3ccb58f4c 100644 --- a/.github/workflows/build-ansible-executor.yml +++ b/.github/workflows/build-ansible-executor.yml @@ -70,4 +70,6 @@ jobs: push: true context: utils/ansible_executor/context file: utils/ansible_executor/context/Containerfile - tags: jumpserver/ansible-executor:${{ env.IMAGE_TAG }} + tags: | + jumpserver/ansible-executor:${{ env.IMAGE_TAG }} + jumpserver/ansible-executor:latest diff --git a/Dockerfile b/Dockerfile index 08f43dd09..7cb33fae0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,8 @@ ARG TOOLS=" \ postgresql-client \ openssh-client \ sshpass \ - bubblewrap" + bubblewrap \ + docker-cli" ARG APT_MIRROR=http://deb.debian.org diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d3f065d85..89de0c1b0 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -602,6 +602,7 @@ class Config(dict): 'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True, 'SECURITY_MFA_BY_EMAIL': False, 'SECURITY_COMMAND_EXECUTION': False, + 'ANSIBLE_DOCKER_ENABLED': True, 'SECURITY_COMMAND_BLACKLIST': [ 'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' ], diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 5048c2ca1..14030e248 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -43,6 +43,7 @@ SECURITY_MFA_BY_EMAIL = CONFIG.SECURITY_MFA_BY_EMAIL SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute SECURITY_MAX_SESSION_TIME = CONFIG.SECURITY_MAX_SESSION_TIME # Unit: hour SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION +ANSIBLE_DOCKER_ENABLED = CONFIG.ANSIBLE_DOCKER_ENABLED SECURITY_COMMAND_BLACKLIST = CONFIG.SECURITY_COMMAND_BLACKLIST SECURITY_PASSWORD_EXPIRATION_TIME_ADMIN = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME_ADMIN # Unit: day SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day diff --git a/apps/libs/ansible/ansible.cfg b/apps/libs/ansible/ansible.cfg index 73bd7089d..26360f5ec 100644 --- a/apps/libs/ansible/ansible.cfg +++ b/apps/libs/ansible/ansible.cfg @@ -7,7 +7,7 @@ timeout = 65 [privilege_escalation] [paramiko_connection] [ssh_connection] -# Docker 隔离下 ControlMaster 在 bind mount 上无法创建 mux socket(macOS 尤其明显) +# for test ssh_args = -o ControlMaster=no -o ControlPersist=no pipelining = True diff --git a/apps/ops/ansible/docker.py b/apps/ops/ansible/docker.py new file mode 100644 index 000000000..1e4a7d43a --- /dev/null +++ b/apps/ops/ansible/docker.py @@ -0,0 +1,76 @@ +import os +import shutil + +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + +from common.utils.safe import safe_run_cmd +from .exception import AnsibleDockerImageNotFound + +ANSIBLE_EE_IMAGE = 'jumpserver/ansible-executor:latest' +ANSIBLE_EE_PYTHON_INTERPRETER = '/usr/bin/python3.11' + +__all__ = [ + 'ANSIBLE_EE_IMAGE', + 'ANSIBLE_EE_PYTHON_INTERPRETER', + 'use_ansible_docker_isolation', + 'docker_extravars', + 'docker_isolation_kwargs', + 'prepare_isolated_ansible_cfg', + 'stage_inventory_for_docker', + 'ensure_ansible_docker_image', +] + + +def use_ansible_docker_isolation(): + return settings.ANSIBLE_DOCKER_ENABLED + + +def docker_extravars(extra_vars): + extravars = dict(extra_vars or {}) + if use_ansible_docker_isolation(): + extravars.setdefault('local_python_interpreter', ANSIBLE_EE_PYTHON_INTERPRETER) + return extravars + + +def docker_isolation_kwargs(): + return { + 'process_isolation': True, + 'process_isolation_executable': 'docker', + 'container_image': ANSIBLE_EE_IMAGE, + 'container_options': ['--network=jms_net'], + } + + +def prepare_isolated_ansible_cfg(project_dir): + if not use_ansible_docker_isolation(): + return + src = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg') + dst = os.path.join(project_dir, 'ansible.cfg') + shutil.copyfile(src, dst) + + +def stage_inventory_for_docker(project_dir, inventory_path): + if not use_ansible_docker_isolation(): + return inventory_path + standard_dir = os.path.join(project_dir, 'inventory') + standard_path = os.path.join(standard_dir, 'hosts') + if os.path.realpath(inventory_path) == os.path.realpath(standard_path): + return standard_path + os.makedirs(standard_dir, mode=0o700, exist_ok=True) + shutil.copy2(inventory_path, standard_path) + return standard_path + + +def ensure_ansible_docker_image(): + if not use_ansible_docker_isolation(): + return + result = safe_run_cmd(['docker', 'image', 'inspect', ANSIBLE_EE_IMAGE]) + if not result or result.returncode != 0: + raise AnsibleDockerImageNotFound( + _('Ansible Docker image "%(image)s" not found. ' + 'You can disable this option in System Settings - Feature Settings - Job Center - ' + 'Ansible Docker isolation to run locally. ' + 'Please run: docker pull %(image)s') + % {'image': ANSIBLE_EE_IMAGE} + ) diff --git a/apps/ops/ansible/exception.py b/apps/ops/ansible/exception.py index dc5926f05..64cc34be1 100644 --- a/apps/ops/ansible/exception.py +++ b/apps/ops/ansible/exception.py @@ -1,5 +1,9 @@ -__all__ = ['CommandInBlackListException'] +__all__ = ['CommandInBlackListException', 'AnsibleDockerImageNotFound'] class CommandInBlackListException(Exception): pass + + +class AnsibleDockerImageNotFound(Exception): + pass diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index ccb4bd260..757972be0 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -10,43 +10,25 @@ from common.utils.yml import sanitize_ansible_inventory_json, sanitize_ansible_p from ..utils import get_ansible_log_verbosity from .callback import DefaultCallback +from .docker import ( + docker_extravars, + docker_isolation_kwargs, + ensure_ansible_docker_image, + prepare_isolated_ansible_cfg, + stage_inventory_for_docker, + use_ansible_docker_isolation, +) from .exception import CommandInBlackListException from .interface import interface __all__ = ['AdHocRunner', 'PlaybookRunner', 'SuperPlaybookRunner', 'UploadFileRunner'] -ANSIBLE_EE_IMAGE = 'jumpserver/ansible-executor:latest' - - -def use_ansible_docker_isolation(): - """Production runs ansible in EE container; dev runs in celery worker.""" - return not settings.DEBUG_DEV - - -def docker_isolation_kwargs(): - return { - 'process_isolation': True, - 'process_isolation_executable': 'docker', - 'container_image': ANSIBLE_EE_IMAGE, - 'container_options': ['--network=host'], - } - - -def prepare_isolated_ansible_cfg(project_dir): - """Copy ansible.cfg into job dir so the EE container picks up SSH settings.""" - if not use_ansible_docker_isolation(): - return - src = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg') - dst = os.path.join(project_dir, 'ansible.cfg') - shutil.copyfile(src, dst) - class AdHocRunner: cmd_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell') need_local_connection_modules_choices = ("mysql", "postgresql", "sqlserver", "huawei") - def __init__(self, inventory, job_module, module, module_args='', pattern='*', project_dir='/tmp/', - extra_vars=None, + def __init__(self, inventory, job_module, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars=None, dry_run=False, timeout=-1): if extra_vars is None: extra_vars = {} @@ -82,7 +64,7 @@ class AdHocRunner: verbosity = get_ansible_log_verbosity(verbosity) if not os.path.exists(self.project_dir): - os.mkdir(self.project_dir, 0o755) + os.makedirs(self.project_dir, 0o755, exist_ok=True) private_env = safe_join(self.project_dir, 'env') if os.path.exists(private_env): shutil.rmtree(private_env) @@ -91,7 +73,7 @@ class AdHocRunner: run_kwargs = { 'timeout': self.timeout if self.timeout > 0 else None, - 'extravars': self.extra_vars, + 'extravars': docker_extravars(self.extra_vars), 'envvars': self.envs, 'host_pattern': self.pattern, 'private_data_dir': self.project_dir, @@ -105,7 +87,7 @@ class AdHocRunner: } if use_ansible_docker_isolation(): run_kwargs.update(docker_isolation_kwargs()) - + ensure_ansible_docker_image() interface.run(**run_kwargs) return self.cb @@ -135,7 +117,8 @@ class PlaybookRunner: entry = os.path.basename(self.playbook) playbook_dir = os.path.dirname(self.playbook) project_playbook_dir = os.path.join(self.project_dir, "project") - shutil.copytree(playbook_dir, project_playbook_dir, dirs_exist_ok=True) + if os.path.realpath(playbook_dir) != os.path.realpath(project_playbook_dir): + shutil.copytree(playbook_dir, project_playbook_dir, dirs_exist_ok=True) self.playbook = entry def prepare_safe_inputs(self): @@ -169,17 +152,19 @@ class PlaybookRunner: shutil.rmtree(private_env) prepare_isolated_ansible_cfg(self.project_dir) + inventory = stage_inventory_for_docker(self.project_dir, self.inventory) kwargs = dict(kwargs) if use_ansible_docker_isolation(): kwargs.update(docker_isolation_kwargs()) + ensure_ansible_docker_image() elif self.isolate and not is_macos(): kwargs['process_isolation'] = True kwargs['process_isolation_executable'] = 'bwrap' interface.run( private_data_dir=self.project_dir, - inventory=self.inventory, + inventory=inventory, playbook=self.playbook, verbosity=verbosity, event_handler=self.cb.event_handler, @@ -187,7 +172,7 @@ class PlaybookRunner: # Docker EE workdir must be the staged playbook dir (not private_data_dir root). host_cwd=self.playbook_project_dir, envvars=self.envs, - extravars=self.extra_vars, + extravars=docker_extravars(self.extra_vars), **kwargs ) return self.cb @@ -224,7 +209,7 @@ class UploadFileRunner: def run(self, verbosity=0, **kwargs): if not os.path.exists(self.project_dir): - os.makedirs(self.project_dir, mode=0o755) + os.makedirs(self.project_dir, mode=0o755, exist_ok=True) prepare_isolated_ansible_cfg(self.project_dir) src_path = self.stage_upload_files() @@ -244,11 +229,10 @@ class UploadFileRunner: } if use_ansible_docker_isolation(): run_kwargs.update(docker_isolation_kwargs()) - + ensure_ansible_docker_image() interface.run(**run_kwargs) try: shutil.rmtree(self.share_src_dir) except OSError as e: print(f"del upload tmp dir {self.share_src_dir} failed! {e}") return self.cb - return self.cb diff --git a/apps/settings/serializers/feature.py b/apps/settings/serializers/feature.py index f1253af17..4a4080cf9 100644 --- a/apps/settings/serializers/feature.py +++ b/apps/settings/serializers/feature.py @@ -6,6 +6,7 @@ from rest_framework import serializers from common.serializers.fields import EncryptedField from common.utils import date_expired_default +from ops.ansible.docker import ANSIBLE_EE_IMAGE __all__ = [ 'AnnouncementSettingSerializer', 'OpsSettingSerializer', 'VaultSettingSerializer', @@ -17,6 +18,14 @@ from settings.const import ( ChatAITypeChoices, GPTModelChoices, DeepSeekModelChoices, ChatAIMethodChoices ) +ANSIBLE_DOCKER_HELP_TEXT = _( + 'Run Ansible jobs in Docker execution environment (%(image)s). ' + 'You can disable this option in System Settings - Feature Settings - Job Center - ' + 'Ansible Docker isolation to run locally. ' + 'If the image is missing, pull it on the ansible worker: ' + 'docker pull %(image)s' +) % {'image': ANSIBLE_EE_IMAGE} + class AnnouncementSerializer(serializers.Serializer): ID = serializers.CharField(required=False, allow_blank=True, allow_null=True) @@ -204,6 +213,11 @@ class OpsSettingSerializer(serializers.Serializer): required=False, label=_('Adhoc command'), help_text=_('Allow users to execute batch commands in the Workbench - Job Center - Adhoc') ) + ANSIBLE_DOCKER_ENABLED = serializers.BooleanField( + required=False, + label=_('Ansible Docker isolation'), + help_text=ANSIBLE_DOCKER_HELP_TEXT, + ) SECURITY_COMMAND_BLACKLIST = serializers.ListField( child=serializers.CharField(max_length=1024), label=_('Command blacklist'), diff --git a/utils/ansible_executor/execution-environment.yml b/utils/ansible_executor/execution-environment.yml index 06169dfbd..27416eb1e 100644 --- a/utils/ansible_executor/execution-environment.yml +++ b/utils/ansible_executor/execution-environment.yml @@ -15,9 +15,9 @@ dependencies: # 与 pyproject.toml [tool.uv.sources] 保持一致,不要用 PyPI 官方包 ansible_core: - package_pip: https://github.com/jumpserver-dev/ansible/archive/refs/tags/v2.14.1.7.zip + package_pip: https://github.com/jumpserver-dev/ansible/archive/refs/tags/v2.16.18.3.zip ansible_runner: - package_pip: https://github.com/jumpserver-dev/ansible-runner/archive/refs/tags/2.4.0.1.zip + package_pip: https://github.com/jumpserver-dev/ansible-runner/archive/refs/tags/v2.4.0.2.zip galaxy: collections: diff --git a/utils/ansible_executor/requirements-python.txt b/utils/ansible_executor/requirements-python.txt index 05978f43a..f3061bf5c 100644 --- a/utils/ansible_executor/requirements-python.txt +++ b/utils/ansible_executor/requirements-python.txt @@ -1,10 +1,10 @@ # Aligned with JumpServer pyproject.toml ansible-related Python deps. -paramiko==3.2.0 +paramiko==3.5.1 sshtunnel==0.4.0 pywinrm==0.4.3 telnetlib3==4.0.2 mysqlclient==2.2.4 -pymssql==2.3.4 +pymssql==2.3.10 pymongo==4.6.3 -oracledb==1.4.0 -pyfreerdp==0.0.2 +oracledb==3.4.2 +pyfreerdp==0.0.4 From 128aeed5f266dbd3c4342985630e71d93a81783a Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Fri, 3 Jul 2026 14:21:16 +0800 Subject: [PATCH 2/8] fix(reports): secure PDF export URL and authentication --- apps/reports/views.py | 90 ++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/apps/reports/views.py b/apps/reports/views.py index da5ccaaea..9b73184e8 100644 --- a/apps/reports/views.py +++ b/apps/reports/views.py @@ -5,8 +5,11 @@ from io import BytesIO from urllib.parse import parse_qsl, urlencode, urlparse, urlsplit, urlunsplit from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.mail import EmailMultiAlternatives -from django.http import FileResponse, HttpResponseBadRequest, JsonResponse +from django.http import ( + FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse, +) from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ @@ -58,30 +61,51 @@ charts_map = { } -def export_chart_to_pdf(chart_name, sessionid, request=None): +def get_trusted_site_url(): + # Playwright navigation and cookie scope must never depend on request Host. + site_url = getattr(settings, 'SITE_URL', '') + if not isinstance(site_url, str): + raise ValueError('Invalid SITE_URL') + site_url = site_url.rstrip('/') + parsed_site = urlparse(site_url) + if ( + not site_url or parsed_site.scheme not in ('http', 'https') or + not parsed_site.hostname or parsed_site.username or + parsed_site.password or parsed_site.query or parsed_site.fragment + ): + raise ValueError('Invalid SITE_URL') + return site_url, parsed_site + + +def build_report_url(chart_path): + path = urllib.parse.unquote(chart_path) + if not path.startswith('/ui/#/reports/'): + raise ValueError('Invalid report path') + site_url, _ = get_trusted_site_url() + return site_url + path + + +def export_chart_to_pdf(chart_name, sessionid, request): chart_info = charts_map.get(chart_name) if not chart_info: return None, None - if request: - url = request.build_absolute_uri(urllib.parse.unquote(chart_info['path'])) - else: - url = urllib.parse.unquote(chart_info['path']) + url = build_report_url(chart_info['path']) + _, parsed_site = get_trusted_site_url() if settings.DEBUG_DEV: url = url.replace(":8080", ":9528") - if request: - oid = request.COOKIES.get("X-JMS-ORG") - request_data = request.POST if request.method == 'POST' else request.GET - query = dict(parse_qsl(urlsplit(url).query, keep_blank_values=True)) - for key, value in request_data.items(): - if key in {'chart', 'csrfmiddlewaretoken'}: - continue - query[key] = value - query.setdefault('days', 7) - query['oid'] = oid - parts = list(urlsplit(url)) - parts[3] = urlencode(query, doseq=True) - url = urlunsplit(parts) + oid = request.COOKIES.get("X-JMS-ORG") + request_data = request.POST if request.method == 'POST' else request.GET + query = dict(parse_qsl(urlsplit(url).query, keep_blank_values=True)) + for key, value in request_data.items(): + if key in {'chart', 'csrfmiddlewaretoken'}: + continue + query[key] = value + query.setdefault('days', 7) + query['oid'] = oid + parts = list(urlsplit(url)) + parts[3] = urlencode(query, doseq=True) + url = urlunsplit(parts) with sync_playwright() as p: lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) @@ -92,12 +116,12 @@ def export_chart_to_pdf(chart_name, sessionid, request=None): ignore_https_errors=True ) # 设置 sessionid cookie - parsed_url = urlparse(url) context.add_cookies([ { 'name': settings.SESSION_COOKIE_NAME, 'value': sessionid, - 'domain': parsed_url.hostname, + # hostname is derived from SITE_URL and never includes a port. + 'domain': parsed_site.hostname, 'path': '/', 'httpOnly': True, 'secure': False, # 如有 https 可改 True @@ -122,7 +146,7 @@ def export_chart_to_pdf(chart_name, sessionid, request=None): @method_decorator(csrf_exempt, name='dispatch') -class ExportPdfView(View): +class ExportPdfView(LoginRequiredMixin, View): def get(self, request): chart_name = request.GET.get('chart') return self._handle_export(request, chart_name) @@ -132,13 +156,20 @@ class ExportPdfView(View): return self._handle_export(request, chart_name) def _handle_export(self, request, chart_name): + if not request.user.is_authenticated: + return HttpResponse(status=401) if not chart_name: return HttpResponseBadRequest('Missing chart parameter') sessionid = request.COOKIES.get(settings.SESSION_COOKIE_NAME) if not sessionid: return HttpResponseBadRequest('No sessionid found in cookies') - pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request) + try: + pdf_bytes, title = export_chart_to_pdf( + chart_name, sessionid, request=request + ) + except ValueError: + return HttpResponseBadRequest('Invalid report configuration') if not pdf_bytes: return HttpResponseBadRequest('Failed to generate PDF') filename = f"{title}-{timezone.now().strftime('%Y%m%d%H%M%S')}.pdf" @@ -147,9 +178,11 @@ class ExportPdfView(View): return response -class SendMailView(View): +class SendMailView(LoginRequiredMixin, View): def post(self, request): + if not request.user.is_authenticated: + return HttpResponse(status=401) chart_name = request.GET.get('chart') or request.POST.get('chart') if not chart_name: return HttpResponseBadRequest('Missing chart parameter') @@ -161,7 +194,12 @@ class SendMailView(View): return HttpResponseBadRequest('No sessionid found in cookies') # 1. 生成 PDF - pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request) + try: + pdf_bytes, title = export_chart_to_pdf( + chart_name, sessionid, request=request + ) + except ValueError: + return HttpResponseBadRequest('Invalid report configuration') if not pdf_bytes: return HttpResponseBadRequest('Failed to generate PDF') @@ -188,4 +226,4 @@ class SendMailView(View): msg.send() except Exception as e: return JsonResponse({"error": _('Failed to send email: ') + str(e)}) - return JsonResponse({"message": _('Email sent successfully to ') + email}) \ No newline at end of file + return JsonResponse({"message": _('Email sent successfully to ') + email}) From 394b65fce52dc81aaab6a75e5f17467b8c0f8f0f Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Wed, 1 Jul 2026 17:39:17 +0800 Subject: [PATCH 3/8] fix(accounts): prevent SQL injection in MSSQL account automation --- .../change_secret/database/sqlserver/main.yml | 72 ++++++++++++++++++- .../push_account/database/sqlserver/main.yml | 63 +++++++++++++++- .../database/sqlserver/main.yml | 17 ++++- apps/libs/ansible/modules/mssql_script.py | 3 +- 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/apps/accounts/automations/change_secret/database/sqlserver/main.yml b/apps/accounts/automations/change_secret/database/sqlserver/main.yml index 90495be45..747372874 100644 --- a/apps/accounts/automations/change_secret/database/sqlserver/main.yml +++ b/apps/accounts/automations/change_secret/database/sqlserver/main.yml @@ -33,7 +33,9 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" + script: "SELECT 1 FROM sys.sql_logins WHERE name=%(username)s;" + params: + username: "{{ account.username }}" when: db_info is succeeded register: user_exist @@ -46,7 +48,35 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" + script: | + DECLARE @login_name nvarchar(256) = %(username)s; + DECLARE @password nvarchar(4000) = %(password)s; + DECLARE @db_name nvarchar(256) = %(db_name)s; + DECLARE @sql nvarchar(max); + + IF @login_name IS NULL OR LEN(@login_name) = 0 OR LEN(@login_name) > 128 + BEGIN + RAISERROR('Invalid login name', 16, 1); + RETURN; + END; + + IF @db_name IS NULL OR LEN(@db_name) = 0 OR LEN(@db_name) > 128 OR DB_ID(@db_name) IS NULL + BEGIN + RAISERROR('Invalid default database', 16, 1); + RETURN; + END; + + -- 标识符使用 QUOTENAME 防止名称逃逸,密码在拼接前转义单引号。 + SET @sql = N'ALTER LOGIN ' + QUOTENAME(@login_name) + + N' WITH PASSWORD = N''' + REPLACE(@password, N'''', N'''''') + N''', DEFAULT_DATABASE = ' + + QUOTENAME(@db_name) + N';'; + + EXEC sys.sp_executesql @sql; + SELECT @@VERSION AS version; + params: + username: "{{ account.username }}" + password: "{{ account.secret }}" + db_name: "{{ jms_asset.spec_info.db_name }}" ignore_errors: true when: user_exist.query_results[0] | length != 0 @@ -59,7 +89,43 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version" + script: | + DECLARE @login_name nvarchar(256) = %(username)s; + DECLARE @password nvarchar(4000) = %(password)s; + DECLARE @db_name nvarchar(256) = %(db_name)s; + DECLARE @sql nvarchar(max); + + IF @login_name IS NULL OR LEN(@login_name) = 0 OR LEN(@login_name) > 128 + BEGIN + RAISERROR('Invalid login name', 16, 1); + RETURN; + END; + + IF @db_name IS NULL OR LEN(@db_name) = 0 OR LEN(@db_name) > 128 OR DB_ID(@db_name) IS NULL + BEGIN + RAISERROR('Invalid default database', 16, 1); + RETURN; + END; + + -- 标识符使用 QUOTENAME 防止名称逃逸,密码在拼接前转义单引号。 + SET @sql = N'CREATE LOGIN ' + QUOTENAME(@login_name) + + N' WITH PASSWORD = N''' + REPLACE(@password, N'''', N'''''') + N''', DEFAULT_DATABASE = ' + + QUOTENAME(@db_name) + N';'; + + EXEC sys.sp_executesql @sql; + + IF USER_ID(@login_name) IS NULL + BEGIN + SET @sql = N'CREATE USER ' + QUOTENAME(@login_name) + + N' FOR LOGIN ' + QUOTENAME(@login_name) + N';'; + EXEC sys.sp_executesql @sql; + END; + + SELECT @@VERSION AS version; + params: + username: "{{ account.username }}" + password: "{{ account.secret }}" + db_name: "{{ jms_asset.spec_info.db_name }}" ignore_errors: true when: user_exist.query_results[0] | length == 0 diff --git a/apps/accounts/automations/push_account/database/sqlserver/main.yml b/apps/accounts/automations/push_account/database/sqlserver/main.yml index b3c5a8ab4..ce76ffbb2 100644 --- a/apps/accounts/automations/push_account/database/sqlserver/main.yml +++ b/apps/accounts/automations/push_account/database/sqlserver/main.yml @@ -33,7 +33,9 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" + script: "SELECT 1 FROM sys.sql_logins WHERE name=%(username)s;" + params: + username: "{{ account.username }}" when: db_info is succeeded register: user_exist @@ -46,7 +48,35 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" + script: | + DECLARE @login_name nvarchar(256) = %(username)s; + DECLARE @password nvarchar(4000) = %(password)s; + DECLARE @db_name nvarchar(256) = %(db_name)s; + DECLARE @sql nvarchar(max); + + IF @login_name IS NULL OR LEN(@login_name) = 0 OR LEN(@login_name) > 128 + BEGIN + RAISERROR('Invalid login name', 16, 1); + RETURN; + END; + + IF @db_name IS NULL OR LEN(@db_name) = 0 OR LEN(@db_name) > 128 OR DB_ID(@db_name) IS NULL + BEGIN + RAISERROR('Invalid default database', 16, 1); + RETURN; + END; + + -- 标识符使用 QUOTENAME 防止名称逃逸,密码在拼接前转义单引号。 + SET @sql = N'ALTER LOGIN ' + QUOTENAME(@login_name) + + N' WITH PASSWORD = N''' + REPLACE(@password, N'''', N'''''') + N''', DEFAULT_DATABASE = ' + + QUOTENAME(@db_name) + N';'; + + EXEC sys.sp_executesql @sql; + SELECT @@VERSION AS version; + params: + username: "{{ account.username }}" + password: "{{ account.secret }}" + db_name: "{{ jms_asset.spec_info.db_name }}" ignore_errors: true when: user_exist.query_results[0] | length != 0 register: change_info @@ -60,7 +90,34 @@ name: '{{ jms_asset.spec_info.db_name }}' encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version" + script: | + DECLARE @login_name nvarchar(256) = %(username)s; + DECLARE @password nvarchar(4000) = %(password)s; + DECLARE @sql nvarchar(max); + + IF @login_name IS NULL OR LEN(@login_name) = 0 OR LEN(@login_name) > 128 + BEGIN + RAISERROR('Invalid login name', 16, 1); + RETURN; + END; + + -- 标识符使用 QUOTENAME 防止名称逃逸,密码在拼接前转义单引号。 + SET @sql = N'CREATE LOGIN ' + QUOTENAME(@login_name) + + N' WITH PASSWORD = N''' + REPLACE(@password, N'''', N'''''') + N''';'; + + EXEC sys.sp_executesql @sql; + + IF USER_ID(@login_name) IS NULL + BEGIN + SET @sql = N'CREATE USER ' + QUOTENAME(@login_name) + + N' FOR LOGIN ' + QUOTENAME(@login_name) + N';'; + EXEC sys.sp_executesql @sql; + END; + + SELECT @@VERSION AS version; + params: + username: "{{ account.username }}" + password: "{{ account.secret }}" ignore_errors: true when: user_exist.query_results[0] | length == 0 register: change_info diff --git a/apps/accounts/automations/remove_account/database/sqlserver/main.yml b/apps/accounts/automations/remove_account/database/sqlserver/main.yml index aa074633d..ce50232f1 100644 --- a/apps/accounts/automations/remove_account/database/sqlserver/main.yml +++ b/apps/accounts/automations/remove_account/database/sqlserver/main.yml @@ -13,5 +13,20 @@ name: "{{ jms_asset.spec_info.db_name }}" encryption: "{{ jms_asset.encryption | default(None) }}" tds_version: "{{ jms_asset.tds_version | default(None) }}" - script: "DROP LOGIN {{ account.username }}; select @@version" + script: | + DECLARE @login_name nvarchar(256) = %(username)s; + DECLARE @sql nvarchar(max); + IF @login_name IS NULL OR LEN(@login_name) = 0 OR LEN(@login_name) > 128 + BEGIN + RAISERROR('Invalid login name', 16, 1); + RETURN; + END; + + -- 标识符使用 QUOTENAME 防止 login name 逃逸。 + SET @sql = N'DROP LOGIN ' + QUOTENAME(@login_name) + N';'; + EXEC sys.sp_executesql @sql; + + SELECT @@VERSION AS version; + params: + username: "{{ account.username }}" diff --git a/apps/libs/ansible/modules/mssql_script.py b/apps/libs/ansible/modules/mssql_script.py index f8984aa4d..f8aeb52a5 100644 --- a/apps/libs/ansible/modules/mssql_script.py +++ b/apps/libs/ansible/modules/mssql_script.py @@ -280,7 +280,8 @@ def main(): login_port=dict(type='int', default=1433), script=dict(required=True), output=dict(default='default', choices=['dict', 'default']), - params=dict(type='dict'), + # 防止 params 中的密码出现在日志中 + params=dict(type='dict', no_log=True), transaction=dict(type='bool', default=True), tds_version=dict(type='str', required=False, default=None), encryption=dict(type='str', required=False, default=None) From 7e948f2919995920656bf918f45672250749fbed Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Mon, 6 Jul 2026 15:44:34 +0800 Subject: [PATCH 4/8] fix: Login log reason translate --- ..._reason_code_userloginlog_reason_params.py | 23 +++ apps/audits/models.py | 24 ++- apps/audits/signal_handlers/login_log.py | 10 +- apps/authentication/backends/base.py | 5 +- apps/authentication/errors/const.py | 20 +- apps/authentication/errors/failed.py | 39 +++- apps/authentication/errors/redirect.py | 2 +- apps/authentication/signal_handlers.py | 7 +- apps/i18n/core/zh/LC_MESSAGES/django.po | 184 +++++++++--------- 9 files changed, 206 insertions(+), 108 deletions(-) create mode 100644 apps/audits/migrations/0008_userloginlog_reason_code_userloginlog_reason_params.py diff --git a/apps/audits/migrations/0008_userloginlog_reason_code_userloginlog_reason_params.py b/apps/audits/migrations/0008_userloginlog_reason_code_userloginlog_reason_params.py new file mode 100644 index 000000000..eaed33ed1 --- /dev/null +++ b/apps/audits/migrations/0008_userloginlog_reason_code_userloginlog_reason_params.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.13 on 2026-07-06 07:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0007_auto_20250610_1704'), + ] + + operations = [ + migrations.AddField( + model_name='userloginlog', + name='reason_code', + field=models.CharField(blank=True, default='', max_length=64, verbose_name='Reason code'), + ), + migrations.AddField( + model_name='userloginlog', + name='reason_params', + field=models.JSONField(blank=True, default=dict, verbose_name='Reason params'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 018ec4a69..aef9cb480 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -218,6 +218,12 @@ class UserLoginLog(models.Model): reason = models.CharField( default="", max_length=128, blank=True, verbose_name=_("Reason") ) + reason_code = models.CharField( + default='', max_length=64, blank=True, verbose_name=_("Reason code") + ) + reason_params = models.JSONField( + default=dict, blank=True, verbose_name=_("Reason params") + ) status = models.BooleanField( default=LoginStatusChoices.success, choices=LoginStatusChoices.choices, @@ -259,7 +265,23 @@ class UserLoginLog(models.Model): @property def reason_display(self) -> str: - from authentication.errors import reason_choices, old_reason_choices + from authentication.errors import ( + reason_choices, reason_template_choices, old_reason_choices + ) + + template = reason_template_choices.get(self.reason_code) + if template: + reason_params = self.reason_params + if not isinstance(reason_params, dict): + reason_params = {} + try: + return template.format(**reason_params) + except (AttributeError, KeyError, IndexError, TypeError, ValueError): + pass + + reason = reason_choices.get(self.reason_code) + if reason: + return reason reason = reason_choices.get(self.reason) if reason: diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index c8ab68773..7bd235551 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -144,5 +144,13 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs): def on_user_auth_failed(sender, username, request, reason='', **kwargs): logger.debug('User login failed: {}'.format(username)) data = generate_data(username, request) - data.update({'reason': reason[:128], 'status': False}) + reason_params = kwargs.get('reason_params') or {} + if not isinstance(reason_params, dict): + reason_params = {} + data.update({ + 'reason': reason[:128], + 'reason_code': kwargs.get('reason_code') or '', + 'reason_params': reason_params, + 'status': False, + }) write_login_log(**data) diff --git a/apps/authentication/backends/base.py b/apps/authentication/backends/base.py index fd31f7ecb..e39cac81e 100644 --- a/apps/authentication/backends/base.py +++ b/apps/authentication/backends/base.py @@ -74,7 +74,10 @@ class RedirectAuthBackend(JMSBaseAuthBackend): def send_backend_auth_failed_signal(self, request, username=None, reason=None): default_reason = reason_choices.get(reason_user_invalid, reason) + reason_code = reason_user_invalid if reason is None else '' + if reason in reason_choices: + reason_code = reason backend_auth_failed.send( sender=self.__class__, username=username, request=request, - reason=default_reason, backend=self.backend + reason=default_reason, backend=self.backend, reason_code=reason_code ) diff --git a/apps/authentication/errors/const.py b/apps/authentication/errors/const.py index e9a617f97..ffa7ce54a 100644 --- a/apps/authentication/errors/const.py +++ b/apps/authentication/errors/const.py @@ -13,6 +13,11 @@ reason_user_expired = 'user_expired' reason_backend_not_match = 'backend_not_match' reason_acl_not_allow = 'acl_not_allow' only_local_users_are_allowed = 'only_local_users_are_allowed' +reason_invalid_login = 'invalid_login' +reason_block_user_login = 'block_user_login' +reason_block_ip_login = 'block_ip_login' +reason_mfa_error = 'mfa_error' +reason_block_mfa = 'block_mfa' reason_choices = { reason_password_failed: _('Username/password check failed'), @@ -45,23 +50,30 @@ invalid_login_msg = _( ) block_user_login_msg = _( "The account has been locked " - "(please contact admin to unlock it or try again after {} minutes)" + "(please contact admin to unlock it or try again after {block_time} minutes)" ) block_ip_login_msg = _( "The address has been locked " - "(please contact admin to unlock it or try again after {} minutes)" + "(please contact admin to unlock it or try again after {block_time} minutes)" ) block_mfa_msg = _( "The account has been locked " - "(please contact admin to unlock it or try again after {} minutes)" + "(please contact admin to unlock it or try again after {block_time} minutes)" ) mfa_error_msg = _( "{error}, " "You can also try {times_try} times " "(The account will be temporarily locked for {block_time} minutes)" ) +reason_template_choices = { + reason_invalid_login: invalid_login_msg, + reason_block_user_login: block_user_login_msg, + reason_block_ip_login: block_ip_login_msg, + reason_mfa_error: mfa_error_msg, + reason_block_mfa: block_mfa_msg, +} mfa_required_msg = _("MFA required") mfa_unset_msg = _("MFA not set, please set it first") login_confirm_required_msg = _("Login confirm required") login_confirm_wait_msg = _("Wait login confirm ticket for accept") -login_confirm_error_msg = _("Login confirm ticket was {}") +login_confirm_error_msg = _("Login confirm ticket was {status}") diff --git a/apps/authentication/errors/failed.py b/apps/authentication/errors/failed.py index 645884291..9dee6351a 100644 --- a/apps/authentication/errors/failed.py +++ b/apps/authentication/errors/failed.py @@ -18,7 +18,9 @@ class AuthFailedNeedLogMixin: super().__init__(*args, **kwargs) post_auth_failed.send( sender=self.__class__, username=self.username, - request=self.request, reason=self.msg + request=self.request, reason=self.msg, + reason_code=getattr(self, 'reason_code', self.error), + reason_params=getattr(self, 'reason_params', {}), ) @@ -61,7 +63,9 @@ class BlockGlobalIpLoginError(AuthFailedError): def __init__(self, username, ip, **kwargs): if not self.msg: - self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME) + self.reason_code = const.reason_block_ip_login + self.reason_params = {'block_time': settings.SECURITY_LOGIN_IP_LIMIT_TIME} + self.msg = const.block_ip_login_msg.format(**self.reason_params) LoginIpBlockUtil(ip).set_block_if_need() super().__init__(username=username, ip=ip, **kwargs) @@ -75,14 +79,23 @@ class CredentialError( times_remainder = util.get_remainder_times() block_time = settings.SECURITY_LOGIN_LIMIT_TIME if times_remainder < 1: - self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.reason_code = const.reason_block_user_login + self.reason_params = {'block_time': block_time} + self.msg = const.block_user_login_msg.format(**self.reason_params) else: + invalid_login_params = { + 'times_try': times_remainder, 'block_time': block_time, + } default_msg = const.invalid_login_msg.format( - times_try=times_remainder, block_time=block_time + **invalid_login_params ) if error == const.reason_password_failed: + self.reason_code = const.reason_invalid_login + self.reason_params = invalid_login_params self.msg = default_msg else: + self.reason_code = error + self.reason_params = {} self.msg = const.reason_choices.get(error, default_msg) # 先处理 msg 在 super,记录日志时原因才准确 super().__init__(error=error, username=username, ip=ip, request=request) @@ -98,11 +111,17 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError): block_time = settings.SECURITY_LOGIN_LIMIT_TIME if times_remainder: + self.reason_code = const.reason_mfa_error + self.reason_params = { + 'error': str(error), 'times_try': times_remainder, 'block_time': block_time, + } self.msg = const.mfa_error_msg.format( - error=error, times_try=times_remainder, block_time=block_time + **self.reason_params ) else: - self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.reason_code = const.reason_block_mfa + self.reason_params = {'block_time': block_time} + self.msg = const.block_mfa_msg.format(**self.reason_params) super().__init__(username=username, request=request) @@ -110,7 +129,9 @@ class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError): error = 'block_mfa' def __init__(self, username, request, ip): - self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.reason_code = const.reason_block_mfa + self.reason_params = {'block_time': settings.SECURITY_LOGIN_LIMIT_TIME} + self.msg = const.block_mfa_msg.format(**self.reason_params) super().__init__(username=username, request=request, ip=ip) @@ -118,7 +139,9 @@ class BlockLoginError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFail error = 'block_login' def __init__(self, username, ip, request): - self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.reason_code = const.reason_block_user_login + self.reason_params = {'block_time': settings.SECURITY_LOGIN_LIMIT_TIME} + self.msg = const.block_user_login_msg.format(**self.reason_params) super().__init__(username=username, ip=ip, request=request) diff --git a/apps/authentication/errors/redirect.py b/apps/authentication/errors/redirect.py index 5e7c5412d..d8c8ba65b 100644 --- a/apps/authentication/errors/redirect.py +++ b/apps/authentication/errors/redirect.py @@ -71,7 +71,7 @@ class LoginConfirmOtherError(LoginConfirmBaseError): def __init__(self, ticket_id, status, username): self.username = username - msg = const.login_confirm_error_msg.format(status) + msg = const.login_confirm_error_msg.format(status=status) super().__init__(ticket_id=ticket_id, msg=msg) def as_data(self): diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py index 0f175f915..fe3847b17 100644 --- a/apps/authentication/signal_handlers.py +++ b/apps/authentication/signal_handlers.py @@ -46,5 +46,8 @@ def on_user_auth_login_success(sender, user, request, **kwargs): @receiver(backend_auth_failed) def on_user_login_failed(sender, username, request, reason, backend, **kwargs): request.session['auth_backend'] = backend - post_auth_failed.send(sender, username=username, request=request, reason=reason) - + post_auth_failed.send( + sender, username=username, request=request, reason=reason, + reason_code=kwargs.get('reason_code') or '', + reason_params=kwargs.get('reason_params') or {}, + ) diff --git a/apps/i18n/core/zh/LC_MESSAGES/django.po b/apps/i18n/core/zh/LC_MESSAGES/django.po index 98ec9c98f..aa2cc1d7b 100644 --- a/apps/i18n/core/zh/LC_MESSAGES/django.po +++ b/apps/i18n/core/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-01 16:45+0800\n" +"POT-Creation-Date: 2026-07-06 15:45+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -347,7 +347,7 @@ msgstr "SFTP" #: accounts/const/automation.py:116 #: accounts/serializers/automations/change_secret.py:172 audits/const.py:65 #: audits/models.py:65 audits/signal_handlers/activity_log.py:34 -#: common/const/choices.py:66 ops/const.py:75 ops/serializers/celery.py:48 +#: common/const/choices.py:66 ops/const.py:75 ops/serializers/celery.py:42 #: terminal/const.py:80 terminal/models/session/sharing.py:128 #: tickets/views/approve.py:128 msgid "Success" @@ -467,7 +467,7 @@ msgstr "Vault 操作失败,请重试,或者检查 Vault 上的账号信息 #: accounts/templates/accounts/push_account_report.html:118 #: acls/notifications.py:70 acls/serializers/base.py:110 #: assets/models/asset/common.py:102 assets/models/asset/common.py:427 -#: assets/models/cmd_filter.py:36 audits/models.py:59 audits/models.py:336 +#: assets/models/cmd_filter.py:36 audits/models.py:59 audits/models.py:358 #: audits/reporting.py:109 audits/serializers.py:256 #: authentication/models/connection_token.py:42 #: perms/models/asset_permission.py:69 terminal/backends/command/models.py:17 @@ -535,7 +535,7 @@ msgstr "改密状态" #: accounts/serializers/automations/change_secret.py:150 #: acls/serializers/base.py:111 #: acls/templates/acls/asset_login_reminder.html:11 -#: assets/serializers/gateway.py:33 audits/models.py:60 audits/models.py:337 +#: assets/serializers/gateway.py:33 audits/models.py:60 audits/models.py:359 #: audits/reporting.py:114 audits/serializers.py:257 #: authentication/api/connection_token.py:643 ops/models/base.py:18 #: perms/models/asset_permission.py:75 settings/serializers/msg.py:33 @@ -783,7 +783,7 @@ msgstr "结束日期" #: accounts/models/automations/gather_account.py:26 #: accounts/serializers/automations/check_account.py:39 #: assets/models/automations/base.py:137 -#: assets/serializers/automations/base.py:47 audits/models.py:224 +#: assets/serializers/automations/base.py:47 audits/models.py:230 #: audits/reporting.py:607 audits/serializers.py:77 ops/models/base.py:49 #: ops/models/job.py:233 terminal/models/applet/applet.py:391 #: terminal/models/applet/host.py:140 terminal/models/component/status.py:30 @@ -863,7 +863,7 @@ msgid "Authorized keys changed" msgstr "密钥变更" #: accounts/models/automations/check_account.py:49 -#: authentication/errors/const.py:23 +#: authentication/errors/const.py:28 msgid "Password expired" msgstr "密码已过期" @@ -1310,7 +1310,7 @@ msgstr "ID" #: acls/templates/acls/asset_login_reminder.html:8 #: acls/templates/acls/user_login_reminder.html:8 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:55 -#: audits/models.py:97 audits/models.py:179 audits/models.py:296 +#: audits/models.py:97 audits/models.py:179 audits/models.py:318 #: audits/reporting.py:104 audits/reporting.py:317 audits/reporting.py:414 #: audits/serializers.py:223 authentication/models/connection_token.py:38 #: authentication/models/ssh_key.py:22 authentication/models/sso_token.py:16 @@ -2026,7 +2026,7 @@ msgstr "用户登录提醒" #: acls/notifications.py:17 acls/notifications.py:67 #: acls/templates/acls/user_login_reminder.html:10 audits/models.py:210 -#: audits/models.py:290 authentication/notifications.py:15 +#: audits/models.py:312 authentication/notifications.py:15 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:13 msgid "Login city" @@ -2041,7 +2041,7 @@ msgid "Recipient username" msgstr "收件人用户名" #: acls/notifications.py:22 acls/templates/acls/user_login_reminder.html:12 -#: audits/models.py:213 audits/models.py:291 audits/serializers.py:91 +#: audits/models.py:213 audits/models.py:313 audits/serializers.py:91 msgid "User agent" msgstr "用户代理" @@ -3611,7 +3611,7 @@ msgid "Job audit log" msgstr "作业审计日志" #: audits/models.py:57 audits/models.py:107 audits/models.py:182 -#: audits/models.py:333 audits/reporting.py:119 audits/reporting.py:327 +#: audits/models.py:355 audits/reporting.py:119 audits/reporting.py:327 #: audits/reporting.py:424 authentication/models/connection_token.py:63 #: terminal/models/session/session.py:36 terminal/models/session/sharing.py:120 #: terminal/notifications.py:171 terminal/notifications.py:238 @@ -3654,7 +3654,7 @@ msgid "Resource" msgstr "资源" #: audits/models.py:108 audits/models.py:154 audits/models.py:184 -#: audits/models.py:338 audits/serializers.py:258 +#: audits/models.py:360 audits/serializers.py:258 #: terminal/serializers/command.py:75 msgid "Datetime" msgstr "日期" @@ -3679,11 +3679,11 @@ msgstr "修改者" msgid "Password change log" msgstr "改密日志" -#: audits/models.py:206 audits/models.py:292 +#: audits/models.py:206 audits/models.py:314 msgid "Login type" msgstr "登录方式" -#: audits/models.py:208 audits/models.py:288 +#: audits/models.py:208 audits/models.py:310 #: tickets/models/ticket/login_confirm.py:12 msgid "Login IP" msgstr "登录 IP" @@ -3701,41 +3701,49 @@ msgstr "MFA" msgid "Reason" msgstr "原因" -#: audits/models.py:226 authentication/notifications.py:19 +#: audits/models.py:222 +msgid "Reason code" +msgstr "原因" + +#: audits/models.py:225 +msgid "Reason params" +msgstr "原因参数" + +#: audits/models.py:232 authentication/notifications.py:19 #: authentication/templates/authentication/_msg_different_city.html:10 #: tickets/models/ticket/login_confirm.py:14 msgid "Login Date" msgstr "登录日期" -#: audits/models.py:228 audits/models.py:293 audits/reporting.py:226 +#: audits/models.py:234 audits/models.py:315 audits/reporting.py:226 msgid "Auth backend" msgstr "认证令牌" -#: audits/models.py:281 +#: audits/models.py:303 msgid "User login log" msgstr "用户登录日志" -#: audits/models.py:289 +#: audits/models.py:311 msgid "Session key" msgstr "会话标识" -#: audits/models.py:294 +#: audits/models.py:316 msgid "Login date" msgstr "登录日期" -#: audits/models.py:325 +#: audits/models.py:347 msgid "User session" msgstr "用户会话" -#: audits/models.py:327 +#: audits/models.py:349 msgid "Offline user session" msgstr "下线用户会话" -#: audits/models.py:334 +#: audits/models.py:356 msgid "Application" msgstr "应用" -#: audits/models.py:335 +#: audits/models.py:357 msgid "Application ID" msgstr "应用 ID" @@ -4052,7 +4060,7 @@ msgstr "耗时分布" msgid "Duration range" msgstr "耗时范围" -#: audits/serializers.py:39 ops/serializers/celery.py:33 +#: audits/serializers.py:39 ops/serializers/celery.py:27 msgid "Execution cycle" msgstr "周期执行" @@ -4430,55 +4438,55 @@ msgstr "Radius" msgid "Custom" msgstr "自定义" -#: authentication/errors/const.py:18 +#: authentication/errors/const.py:23 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors/const.py:19 +#: authentication/errors/const.py:24 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors/const.py:20 +#: authentication/errors/const.py:25 msgid "MFA failed" msgstr "MFA 校验失败" -#: authentication/errors/const.py:21 +#: authentication/errors/const.py:26 msgid "MFA unset" msgstr "MFA 没有设定" -#: authentication/errors/const.py:22 +#: authentication/errors/const.py:27 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors/const.py:24 +#: authentication/errors/const.py:29 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors/const.py:25 +#: authentication/errors/const.py:30 msgid "This account is inactive." msgstr "此账号已禁用" -#: authentication/errors/const.py:26 +#: authentication/errors/const.py:31 msgid "This account is expired" msgstr "此账号已过期" -#: authentication/errors/const.py:27 +#: authentication/errors/const.py:32 msgid "Auth backend not match" msgstr "没有匹配到认证后端" -#: authentication/errors/const.py:28 +#: authentication/errors/const.py:33 msgid "ACL is not allowed" msgstr "登录访问控制不被允许" -#: authentication/errors/const.py:29 +#: authentication/errors/const.py:34 msgid "Only local users are allowed" msgstr "仅允许本地用户" -#: authentication/errors/const.py:39 +#: authentication/errors/const.py:44 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors/const.py:41 +#: authentication/errors/const.py:46 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -4488,21 +4496,19 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次 (账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors/const.py:47 authentication/errors/const.py:55 -#, python-brace-format +#: authentication/errors/const.py:52 authentication/errors/const.py:60 msgid "" "The account has been locked (please contact admin to unlock it or try again " -"after {} minutes)" -msgstr "账号已被锁定 (请联系管理员解锁或{}分钟后重试)" +"after {block_time} minutes)" +msgstr "账号已被锁定 (请联系管理员解锁或{block_time}分钟后重试)" -#: authentication/errors/const.py:51 -#, python-brace-format +#: authentication/errors/const.py:56 msgid "" "The address has been locked (please contact admin to unlock it or try again " -"after {} minutes)" -msgstr "IP 已被锁定 (请联系管理员解锁或 {} 分钟后重试)" +"after {block_time} minutes)" +msgstr "IP 已被锁定 (请联系管理员解锁或 {block_time} 分钟后重试)" -#: authentication/errors/const.py:59 +#: authentication/errors/const.py:64 #, python-brace-format msgid "" "{error}, You can also try {times_try} times (The account will be temporarily " @@ -4510,40 +4516,39 @@ msgid "" msgstr "" "{error} 您还可以尝试 {times_try} 次 (账号将被临时锁定 {block_time} 分钟)" -#: authentication/errors/const.py:63 +#: authentication/errors/const.py:75 msgid "MFA required" msgstr "需要 MFA 认证" -#: authentication/errors/const.py:64 +#: authentication/errors/const.py:76 msgid "MFA not set, please set it first" msgstr "MFA 没有设置,请先完成设置" -#: authentication/errors/const.py:65 +#: authentication/errors/const.py:77 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors/const.py:66 +#: authentication/errors/const.py:78 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors/const.py:67 -#, python-brace-format -msgid "Login confirm ticket was {}" -msgstr "登录复核: {}" +#: authentication/errors/const.py:79 +msgid "Login confirm ticket was {status}" +msgstr "登录复核: {status}" -#: authentication/errors/failed.py:149 +#: authentication/errors/failed.py:172 msgid "Current login is prohibited by ACL rules" msgstr "当前登录ACL规则禁止登录" -#: authentication/errors/failed.py:154 +#: authentication/errors/failed.py:177 msgid "Please enter MFA code" msgstr "请输入 MFA 验证码" -#: authentication/errors/failed.py:159 +#: authentication/errors/failed.py:182 msgid "Please enter SMS code" msgstr "请输入短信验证码" -#: authentication/errors/failed.py:164 users/exceptions.py:14 +#: authentication/errors/failed.py:187 users/exceptions.py:14 msgid "Phone not set" msgstr "手机号没有设置" @@ -4708,7 +4713,7 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:95 settings/utils/ldap.py:767 +#: authentication/middleware.py:95 settings/utils/ldap.py:771 #, python-brace-format msgid "Authentication failed (before login check failed): {}" msgstr "认证失败 (登录前检查失败): {}" @@ -6128,16 +6133,12 @@ msgstr "Ansible 已禁用" msgid "Skip hosts below:" msgstr "跳过以下主机: " -#: ops/api/celery.py:66 ops/api/celery.py:81 -msgid "Waiting task start" -msgstr "等待任务开始" - -#: ops/api/celery.py:269 +#: ops/api/celery.py:204 #, python-brace-format msgid "Task {} not found" msgstr "任务 {} 不存在" -#: ops/api/celery.py:276 +#: ops/api/celery.py:211 #, python-brace-format msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" @@ -6529,7 +6530,7 @@ msgstr "内存使用率超过 {max_threshold}%: => {value}" msgid "CPU load more than {max_threshold}: => {value}" msgstr "CPU 使用率超过 {max_threshold}: => {value}" -#: ops/serializers/celery.py:35 +#: ops/serializers/celery.py:29 msgid "Next execution time" msgstr "下次执行时间" @@ -8993,119 +8994,119 @@ msgstr "已同步用户" msgid "No user synchronization required" msgstr "没有用户需要同步" -#: settings/utils/ldap.py:580 +#: settings/utils/ldap.py:583 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" -#: settings/utils/ldap.py:594 +#: settings/utils/ldap.py:597 #, python-brace-format msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" -#: settings/utils/ldap.py:596 +#: settings/utils/ldap.py:599 #, python-brace-format msgid "The port is not the port of the LDAP service: {}" msgstr "端口不是LDAP服务端口: {}" -#: settings/utils/ldap.py:598 +#: settings/utils/ldap.py:601 #, python-brace-format msgid "Please add certificate: {}" msgstr "请添加证书: {}" -#: settings/utils/ldap.py:602 settings/utils/ldap.py:629 -#: settings/utils/ldap.py:659 settings/utils/ldap.py:687 +#: settings/utils/ldap.py:605 settings/utils/ldap.py:632 +#: settings/utils/ldap.py:662 settings/utils/ldap.py:690 #, python-brace-format msgid "Unknown error: {}" msgstr "未知错误: {}" -#: settings/utils/ldap.py:616 +#: settings/utils/ldap.py:619 msgid "Bind DN or Password incorrect" msgstr "绑定DN或密码错误" -#: settings/utils/ldap.py:623 +#: settings/utils/ldap.py:626 #, python-brace-format msgid "Please enter Bind DN: {}" msgstr "请输入绑定DN: {}" -#: settings/utils/ldap.py:625 +#: settings/utils/ldap.py:628 #, python-brace-format msgid "Please enter Password: {}" msgstr "请输入密码: {}" -#: settings/utils/ldap.py:627 +#: settings/utils/ldap.py:630 #, python-brace-format msgid "Please enter correct Bind DN and Password: {}" msgstr "请输入正确的绑定DN和密码: {}" -#: settings/utils/ldap.py:645 +#: settings/utils/ldap.py:648 #, python-brace-format msgid "Invalid User OU or User search filter: {}" msgstr "不合法的用户OU或用户过滤器: {}" -#: settings/utils/ldap.py:676 +#: settings/utils/ldap.py:679 #, python-brace-format msgid "LDAP User attr map not include: {}" msgstr "LDAP属性映射没有包含: {}" -#: settings/utils/ldap.py:683 +#: settings/utils/ldap.py:686 msgid "LDAP User attr map is not dict" msgstr "LDAP属性映射不合法" -#: settings/utils/ldap.py:702 +#: settings/utils/ldap.py:705 msgid "LDAP authentication is not enabled" msgstr "LDAP认证没有启用" -#: settings/utils/ldap.py:720 +#: settings/utils/ldap.py:724 #, python-brace-format msgid "Error (Invalid LDAP server): {}" msgstr "错误 (不合法的LDAP服务器地址): {}" -#: settings/utils/ldap.py:722 +#: settings/utils/ldap.py:726 #, python-brace-format msgid "Error (Invalid Bind DN): {}" msgstr "错误 (不合法的绑定DN): {}" -#: settings/utils/ldap.py:724 +#: settings/utils/ldap.py:728 #, python-brace-format msgid "Error (Invalid LDAP User attr map): {}" msgstr "错误 (不合法的LDAP属性映射): {}" -#: settings/utils/ldap.py:726 +#: settings/utils/ldap.py:730 #, python-brace-format msgid "Error (Invalid User OU or User search filter): {}" msgstr "错误 (不合法的用户OU或用户过滤器): {}" -#: settings/utils/ldap.py:728 +#: settings/utils/ldap.py:732 #, python-brace-format msgid "Error (Not enabled LDAP authentication): {}" msgstr "错误 (没有启用LDAP认证): {}" -#: settings/utils/ldap.py:730 +#: settings/utils/ldap.py:734 #, python-brace-format msgid "Error (Unknown): {}" msgstr "错误 (未知): {}" -#: settings/utils/ldap.py:733 +#: settings/utils/ldap.py:737 #, python-brace-format msgid "Succeed: Match {} users" msgstr "成功匹配 {} 个用户" -#: settings/utils/ldap.py:765 +#: settings/utils/ldap.py:769 #, python-brace-format msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败 (配置错误): {}" -#: settings/utils/ldap.py:769 +#: settings/utils/ldap.py:773 #, python-brace-format msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" -#: settings/utils/ldap.py:771 +#: settings/utils/ldap.py:775 #, python-brace-format msgid "Authentication failed (Unknown): {}" msgstr "认证失败: (未知): {}" -#: settings/utils/ldap.py:774 +#: settings/utils/ldap.py:778 #, python-brace-format msgid "Authentication success: {}" msgstr "认证成功: {}" @@ -12668,5 +12669,8 @@ msgstr "许可证导入成功" msgid "Invalid license" msgstr "许可证无效" +#~ msgid "Waiting task start" +#~ msgstr "等待任务开始" + #~ msgid "Offline video player" #~ msgstr "离线录像播放器" From e6526e77a698f4cefcc59d8cf97e86e041f9873f Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Mon, 6 Jul 2026 16:02:14 +0800 Subject: [PATCH 5/8] fix: Login log city translate --- apps/audits/models.py | 17 ++++++++++++++++- apps/audits/serializers.py | 2 ++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/audits/models.py b/apps/audits/models.py index aef9cb480..c07b9de0f 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext, gettext_lazy as _ from common.db.encoder import ModelJSONFieldEncoder from common.sessions.cache import user_session_manager -from common.utils import lazyproperty, i18n_trans +from common.utils import lazyproperty, i18n_trans, get_ip_city from ops.models import JobExecution from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org @@ -38,6 +38,13 @@ __all__ = [ ] +def _get_city_display(ip, city='') -> str: + try: + return get_ip_city(ip) or gettext(city or '') + except Exception: + return gettext(city or '') + + class JobLog(JobExecution): @property def creator_name(self) -> str: @@ -241,6 +248,10 @@ class UserLoginLog(models.Model): def backend_display(self) -> str: return gettext(self.backend) + @lazyproperty + def city_display(self) -> str: + return _get_city_display(self.ip, self.city) + @classmethod def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None): login_logs = cls.objects.all() @@ -321,6 +332,10 @@ class UserSession(models.Model): def __str__(self): return '%s(%s)' % (self.user, self.ip) + @lazyproperty + def city_display(self) -> str: + return _get_city_display(self.ip, self.city) + @property def backend_display(self) -> str: return gettext(self.backend) diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index bfcf1e81c..357010601 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -75,6 +75,7 @@ class UserLoginLogSerializer(serializers.ModelSerializer): mfa = LabeledChoiceField(choices=MFAChoices.choices, label=_("MFA")) type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type")) status = LabeledChoiceField(choices=LoginStatusChoices.choices, label=_("Status")) + city = serializers.ReadOnlyField(source='city_display', label=_("Login city")) class Meta: model = models.UserLoginLog @@ -223,6 +224,7 @@ class UserSessionSerializer(serializers.ModelSerializer): user = ObjectRelatedField(required=False, queryset=User.objects, label=_('User')) date_expired = serializers.DateTimeField(format="%Y/%m/%d %H:%M:%S", label=_('Date expired')) is_current_user_session = serializers.SerializerMethodField() + city = serializers.ReadOnlyField(source='city_display', label=_("Login city")) class Meta: model = models.UserSession From 74c9319d5c0a2767ff3a2c6a703945f8265cbd1d Mon Sep 17 00:00:00 2001 From: lory <3239225235@qq.com> Date: Fri, 3 Jul 2026 11:45:52 +0800 Subject: [PATCH 6/8] fix:face_capature_create_token --- .../templates/authentication/face_capture.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/authentication/templates/authentication/face_capture.html b/apps/authentication/templates/authentication/face_capture.html index 6b9a967d2..17574378e 100644 --- a/apps/authentication/templates/authentication/face_capture.html +++ b/apps/authentication/templates/authentication/face_capture.html @@ -39,7 +39,11 @@ let token; function createFaceCaptureToken() { - const csrf = getCookie('jms_csrftoken'); + let prefix = getCookie('SESSION_COOKIE_NAME_PREFIX'); + if (!prefix || [`""`, `''`].includes(prefix)) { + prefix = ''; + } + const csrf = getCookie(`${prefix}csrftoken`); $.ajax({ url: apiUrl, method: 'POST', From 57ade8f93ffa13034906ee36f5c3cb15f708cfde Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Wed, 1 Jul 2026 17:54:29 +0800 Subject: [PATCH 7/8] fix: validate flash message redirect URLs --- apps/common/views/msg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/common/views/msg.py b/apps/common/views/msg.py index f18e08756..6b4435d83 100644 --- a/apps/common/views/msg.py +++ b/apps/common/views/msg.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache from django.views.generic.base import TemplateView -from common.utils import bulk_get, FlashMessageUtil +from common.utils import bulk_get, FlashMessageUtil, safe_next_url @method_decorator(never_cache, name='dispatch') @@ -23,6 +23,8 @@ class FlashMessageMsgView(TemplateView): items = ('title', 'message', 'error', 'redirect_url', 'confirm_button', 'cancel_url') title, msg, error, redirect_url, confirm_btn, cancel_url = bulk_get(message_data, items) + redirect_url = safe_next_url(redirect_url, request=request) + cancel_url = safe_next_url(cancel_url, request=request) interval = message_data.get('interval', 3) auto_redirect = message_data.get('auto_redirect', True) From c4c6eebe4285ed21252d3eab07c175f1b3e39e25 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Thu, 18 Jun 2026 18:43:51 +0800 Subject: [PATCH 8/8] fix: SSH automation add local_python_interpreter --- apps/accounts/automations/change_secret/custom/ssh/main.yml | 1 + apps/accounts/automations/push_account/custom/ssh/main.yml | 3 ++- apps/accounts/automations/verify_account/custom/ssh/main.yml | 1 + apps/assets/automations/ping/custom/ssh/main.yml | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml index 5fe0e8b25..6595ca39e 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/main.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_connection: local + ansible_python_interpreter: "{{ local_python_interpreter }}" ansible_become: false tasks: diff --git a/apps/accounts/automations/push_account/custom/ssh/main.yml b/apps/accounts/automations/push_account/custom/ssh/main.yml index f2d67905f..a14dfd7b8 100644 --- a/apps/accounts/automations/push_account/custom/ssh/main.yml +++ b/apps/accounts/automations/push_account/custom/ssh/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_connection: local + ansible_python_interpreter: "{{ local_python_interpreter }}" ansible_become: false tasks: @@ -67,4 +68,4 @@ recv_timeout: "{{ params.recv_timeout | default(30) }}" delegate_to: localhost connection: local - when: check_conn_after_change \ No newline at end of file + when: check_conn_after_change diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index 831c1c783..3f36b2648 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -3,6 +3,7 @@ vars: ansible_connection: local ansible_shell_type: sh + ansible_python_interpreter: "{{ local_python_interpreter }}" ansible_become: false tasks: diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml index f95236081..69a4fc8e7 100644 --- a/apps/assets/automations/ping/custom/ssh/main.yml +++ b/apps/assets/automations/ping/custom/ssh/main.yml @@ -3,6 +3,7 @@ vars: ansible_connection: local ansible_shell_type: sh + ansible_python_interpreter: "{{ local_python_interpreter }}" ansible_become: false ansible_timeout: 30 tasks: @@ -22,4 +23,3 @@ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" recv_timeout: "{{ params.recv_timeout | default(30) }}" -