mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-12 02:53:38 +00:00
Merge branches 'dev' and 'dev' of github.com:jumpserver/jumpserver into dev
This commit is contained in:
4
.github/workflows/build-ansible-executor.yml
vendored
4
.github/workflows/build-ansible-executor.yml
vendored
@@ -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
|
||||
|
||||
@@ -36,7 +36,8 @@ ARG TOOLS=" \
|
||||
postgresql-client \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
bubblewrap"
|
||||
bubblewrap \
|
||||
docker-cli"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
when: check_conn_after_change
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_shell_type: sh
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
@@ -218,6 +225,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,
|
||||
@@ -235,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()
|
||||
@@ -259,7 +276,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:
|
||||
@@ -299,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {},
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <ibuler@qq.com>\n"
|
||||
"Language-Team: JumpServer team<ibuler@qq.com>\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 "离线录像播放器"
|
||||
|
||||
@@ -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'
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
76
apps/ops/ansible/docker.py
Normal file
76
apps/ops/ansible/docker.py
Normal file
@@ -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}
|
||||
)
|
||||
@@ -1,5 +1,9 @@
|
||||
__all__ = ['CommandInBlackListException']
|
||||
__all__ = ['CommandInBlackListException', 'AnsibleDockerImageNotFound']
|
||||
|
||||
|
||||
class CommandInBlackListException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleDockerImageNotFound(Exception):
|
||||
pass
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
return JsonResponse({"message": _('Email sent successfully to ') + email})
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user