.
\ No newline at end of file
diff --git a/README.md b/README.md
index 05467881f..879be93a2 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
多云环境下更好用的堡垒机
-
+
@@ -13,7 +13,7 @@
-JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
+JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
@@ -126,9 +126,9 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
-Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
-https://www.gnu.org/licenses/gpl-2.0.html
+https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
diff --git a/README_EN.md b/README_EN.md
index b9dbbff3a..4e9bf0e6c 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -2,13 +2,13 @@
Open Source Bastion Host
-
+
-JumpServer is the world's first open-source Bastion Host and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
+JumpServer is the world's first open-source Bastion Host and is licensed under the GPLv3. It is a 4A-compliant professional operation and maintenance security audit system.
JumpServer uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
@@ -85,10 +85,11 @@ If you find a security problem, please contact us directly:
- 400-052-0755
### License & Copyright
-Copyright (c) 2014-2021 Beijing Duizhan Tech, Inc., All rights reserved.
+Copyright (c) 2014-2021 FIT2CLOUD Tech, Inc., All rights reserved.
-Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
-https://www.gnu.org/licenses/gpl-2.0.html
+https://www.gnu.org/licenses/gpl-3.0.htmll
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
diff --git a/apps/acls/serializers/rules/rules.py b/apps/acls/serializers/rules/rules.py
index bfac5f65f..80316147e 100644
--- a/apps/acls/serializers/rules/rules.py
+++ b/apps/acls/serializers/rules/rules.py
@@ -8,7 +8,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
logger = get_logger(__file__)
-__all__ = ['RuleSerializer']
+__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
def ip_group_child_validator(ip_group_child):
@@ -21,13 +21,14 @@ def ip_group_child_validator(ip_group_child):
raise serializers.ValidationError(error)
-class RuleSerializer(serializers.Serializer):
- ip_group_help_text = _(
- 'Format for comma-delimited string, with * indicating a match all. '
- 'Such as: '
- '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
- )
+ip_group_help_text = _(
+ 'Format for comma-delimited string, with * indicating a match all. '
+ 'Such as: '
+ '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
+)
+
+class RuleSerializer(serializers.Serializer):
ip_group = serializers.ListField(
default=['*'], label=_('IP'), help_text=ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]))
diff --git a/apps/applications/migrations/0013_auto_20211105_1605.py b/apps/applications/migrations/0014_auto_20211105_1605.py
similarity index 92%
rename from apps/applications/migrations/0013_auto_20211105_1605.py
rename to apps/applications/migrations/0014_auto_20211105_1605.py
index 5da963fdc..ab0063c3c 100644
--- a/apps/applications/migrations/0013_auto_20211105_1605.py
+++ b/apps/applications/migrations/0014_auto_20211105_1605.py
@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('applications', '0012_auto_20211014_2209'),
+ ('applications', '0013_auto_20211026_1711'),
]
operations = [
diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py
index 321fa5af7..5b4ef7bb3 100644
--- a/apps/assets/api/asset.py
+++ b/apps/assets/api/asset.py
@@ -147,7 +147,7 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
asset = data['asset']
system_users = data.get('system_users')
if not system_users:
- system_users = asset.get_all_systemusers()
+ system_users = asset.get_all_system_users()
if action == 'push_system_user':
task = push_system_users_a_asset.delay(system_users, asset=asset)
elif action == 'test_system_user':
diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py
index d7ed099ed..1c6f19cbb 100644
--- a/apps/assets/api/cmd_filter.py
+++ b/apps/assets/api/cmd_filter.py
@@ -29,7 +29,7 @@ class CommandFilterViewSet(OrgBulkModelViewSet):
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
model = CommandFilterRule
- filterset_fields = ("content",)
+ filterset_fields = ('content',)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py
index a8164cd13..7e4ff61aa 100644
--- a/apps/assets/api/node.py
+++ b/apps/assets/api/node.py
@@ -13,6 +13,7 @@ from django.db.models.signals import m2m_changed
from common.const.http import POST
from common.exceptions import SomeoneIsDoingThis
from common.const.signals import PRE_REMOVE, POST_REMOVE
+from common.mixins.api import SuggestionMixin
from assets.models import Asset
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
@@ -41,7 +42,7 @@ __all__ = [
]
-class NodeViewSet(OrgModelViewSet):
+class NodeViewSet(SuggestionMixin, OrgModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('value', )
diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py
index b9f1007d9..581800bec 100644
--- a/apps/assets/api/system_user.py
+++ b/apps/assets/api/system_user.py
@@ -1,15 +1,18 @@
# ~*~ coding: utf-8 ~*~
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
+from django.db.models import Q
-from common.utils import get_logger
+from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from common.mixins.api import SuggestionMixin
from orgs.utils import tmp_to_root_org
from rest_framework.decorators import action
-from ..models import SystemUser, Asset
+from users.models import User, UserGroup
+from applications.models import Application
+from ..models import SystemUser, Asset, CommandFilter, CommandFilterRule
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
from ..tasks import (
@@ -192,9 +195,42 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
return CommandFilterRuleSerializer
def get_queryset(self):
- pk = self.kwargs.get('pk', None)
- system_user = get_object_or_404(SystemUser, pk=pk)
- return system_user.cmd_filter_rules
+ user_groups = []
+ user_id = self.request.query_params.get('user_id')
+ user = get_object_or_none(User, pk=user_id)
+ if user:
+ user_groups.extend(list(user.groups.all()))
+ user_group_id = self.request.query_params.get('user_group_id')
+ user_group = get_object_or_none(UserGroup, pk=user_group_id)
+ if user_group:
+ user_groups.append(user_group)
+ system_user_id = self.kwargs.get('pk', None)
+ system_user = get_object_or_none(SystemUser, pk=system_user_id)
+ if not system_user:
+ system_user_id = self.request.query_params.get('system_user_id')
+ system_user = get_object_or_none(SystemUser, pk=system_user_id)
+ asset_id = self.request.query_params.get('asset_id')
+ asset = get_object_or_none(Asset, pk=asset_id)
+ application_id = self.request.query_params.get('application_id')
+ application = get_object_or_none(Application, pk=application_id)
+ q = Q()
+ if user:
+ q |= Q(users=user)
+ if user_group:
+ q |= Q(user_groups__in=set(user_groups))
+ if system_user:
+ q |= Q(system_users=system_user)
+ if asset:
+ q |= Q(assets=asset)
+ if application:
+ q |= Q(applications=application)
+ if q:
+ cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
+ rule_ids = cmd_filters.values_list('rules', flat=True)
+ rules = CommandFilterRule.objects.filter(id__in=rule_ids)
+ else:
+ rules = CommandFilterRule.objects.none()
+ return rules
class SystemUserAssetsListView(generics.ListAPIView):
diff --git a/apps/assets/migrations/0079_auto_20211105_1605.py b/apps/assets/migrations/0081_auto_20211105_1605.py
similarity index 92%
rename from apps/assets/migrations/0079_auto_20211105_1605.py
rename to apps/assets/migrations/0081_auto_20211105_1605.py
index 915a659df..d8ab39a78 100644
--- a/apps/assets/migrations/0079_auto_20211105_1605.py
+++ b/apps/assets/migrations/0081_auto_20211105_1605.py
@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('assets', '0078_auto_20211014_2209'),
+ ('assets', '0080_auto_20211104_1347'),
]
operations = [
diff --git a/apps/assets/migrations/0082_auto_20211209_1440.py b/apps/assets/migrations/0082_auto_20211209_1440.py
new file mode 100644
index 000000000..a0576fe13
--- /dev/null
+++ b/apps/assets/migrations/0082_auto_20211209_1440.py
@@ -0,0 +1,74 @@
+# Generated by Django 3.1.13 on 2021-12-09 06:40
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+def migrate_system_users_cmd_filters(apps, schema_editor):
+ system_user_model = apps.get_model("assets", "SystemUser")
+ cmd_filter_model = apps.get_model("assets", "CommandFilter")
+ su_through = system_user_model.cmd_filters.through
+ cf_through = cmd_filter_model.system_users.through
+
+ su_relation_objects = su_through.objects.all()
+ cf_relation_objects = [
+ cf_through(**{
+ 'id': su_relation.id,
+ 'systemuser_id': su_relation.systemuser_id,
+ 'commandfilter_id': su_relation.commandfilter_id
+ })
+ for su_relation in su_relation_objects
+ ]
+ cf_through.objects.bulk_create(cf_relation_objects)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0014_auto_20211105_1605'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('assets', '0081_auto_20211105_1605'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='commandfilter',
+ name='applications',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='applications.Application', verbose_name='Application'),
+ ),
+ migrations.AddField(
+ model_name='commandfilter',
+ name='assets',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='assets.Asset', verbose_name='Asset'),
+ ),
+ migrations.AddField(
+ model_name='commandfilter',
+ name='system_users',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters_pre', to='assets.SystemUser', verbose_name='System user'),
+ ),
+ migrations.AddField(
+ model_name='commandfilter',
+ name='users',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters', to=settings.AUTH_USER_MODEL, verbose_name='User'),
+ ),
+ migrations.AddField(
+ model_name='commandfilter',
+ name='user_groups',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='users.UserGroup', verbose_name='User group'),
+ ),
+ migrations.AlterField(
+ model_name='systemuser',
+ name='cmd_filters',
+ field=models.ManyToManyField(blank=True, related_name='system_users_bak', to='assets.CommandFilter', verbose_name='Command filter'),
+ ),
+ migrations.RunPython(migrate_system_users_cmd_filters),
+ migrations.RemoveField(
+ model_name='systemuser',
+ name='cmd_filters',
+ ),
+ migrations.AlterField(
+ model_name='commandfilter',
+ name='system_users',
+ field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='assets.SystemUser', verbose_name='System user'),
+ ),
+ ]
diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py
index 7d9d6d0d0..aaabab1b7 100644
--- a/apps/assets/models/asset.py
+++ b/apps/assets/models/asset.py
@@ -299,6 +299,12 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
names.append(n.full_value)
return names
+ def labels_display(self):
+ names = []
+ for n in self.labels.all():
+ names.append(n.name + ':' + n.value)
+ return names
+
def as_node(self):
from .node import Node
fake_node = Node()
@@ -338,7 +344,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
tree_node = TreeNode(**data)
return tree_node
- def get_all_systemusers(self):
+ def get_all_system_users(self):
from .user import SystemUser
system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\
.values_list('systemuser_id', flat=True)
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index bf91a16b2..bff1002f8 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -7,9 +7,11 @@ from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils.translation import ugettext_lazy as _
-from common.utils import lazyproperty
+from common.utils import lazyproperty, get_logger
from orgs.mixins.models import OrgModelMixin
+logger = get_logger(__file__)
+
__all__ = [
'CommandFilter', 'CommandFilterRule'
@@ -19,11 +21,32 @@ __all__ = [
class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, verbose_name=_("Name"))
+ users = models.ManyToManyField(
+ 'users.User', related_name='cmd_filters', blank=True,
+ verbose_name=_("User")
+ )
+ user_groups = models.ManyToManyField(
+ 'users.UserGroup', related_name='cmd_filters', blank=True,
+ verbose_name=_("User group"),
+ )
+ assets = models.ManyToManyField(
+ 'assets.Asset', related_name='cmd_filters', blank=True,
+ verbose_name=_("Asset")
+ )
+ system_users = models.ManyToManyField(
+ 'assets.SystemUser', related_name='cmd_filters', blank=True,
+ verbose_name=_("System user"))
+ applications = models.ManyToManyField(
+ 'applications.Application', related_name='cmd_filters', blank=True,
+ verbose_name=_("Application")
+ )
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
- created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
+ created_by = models.CharField(
+ max_length=128, blank=True, default='', verbose_name=_('Created by')
+ )
def __str__(self):
return self.name
@@ -71,28 +94,52 @@ class CommandFilterRule(OrgModelMixin):
verbose_name = _("Command filter rule")
@lazyproperty
- def _pattern(self):
+ def pattern(self):
if self.type == 'command':
- regex = []
- content = self.content.replace('\r\n', '\n')
- for cmd in content.split('\n'):
- cmd = re.escape(cmd)
- cmd = cmd.replace('\\ ', '\s+')
- if cmd[-1].isalpha():
- regex.append(r'\b{0}\b'.format(cmd))
- else:
- regex.append(r'\b{0}'.format(cmd))
- s = r'{}'.format('|'.join(regex))
+ s = self.construct_command_regex(content=self.content)
else:
s = r'{0}'.format(self.content)
+
+ return s
+
+ @classmethod
+ def construct_command_regex(cls, content):
+ regex = []
+ content = content.replace('\r\n', '\n')
+ for _cmd in content.split('\n'):
+ cmd = re.sub(r'\s+', ' ', _cmd)
+ cmd = re.escape(cmd)
+ cmd = cmd.replace('\\ ', '\s+')
+
+ # 有空格就不能 铆钉单词了
+ if ' ' in _cmd:
+ regex.append(cmd)
+ continue
+
+ # 如果是单个字符
+ if cmd[-1].isalpha():
+ regex.append(r'\b{0}\b'.format(cmd))
+ else:
+ regex.append(r'\b{0}'.format(cmd))
+ s = r'(?i){}'.format('|'.join(regex))
+ return s
+
+ @staticmethod
+ def compile_regex(regex):
try:
- _pattern = re.compile(s)
- except:
- _pattern = ''
- return _pattern
+ pattern = re.compile(regex)
+ except Exception as e:
+ error = _('The generated regular expression is incorrect: {}').format(str(e))
+ logger.error(error)
+ return False, error, None
+ return True, '', pattern
def match(self, data):
- found = self._pattern.search(data)
+ succeed, error, pattern = self.compile_regex(regex=self.pattern)
+ if not succeed:
+ return self.ACTION_UNKNOWN, ''
+
+ found = pattern.search(data)
if not found:
return self.ACTION_UNKNOWN, ''
diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py
index 337ccdf2f..5e0384d01 100644
--- a/apps/assets/models/user.py
+++ b/apps/assets/models/user.py
@@ -203,7 +203,6 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
- cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index 1bb5afe24..de95e3b80 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -69,6 +69,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
nodes_display = serializers.ListField(
child=serializers.CharField(), label=_('Nodes name'), required=False
)
+ labels_display = serializers.ListField(
+ child=serializers.CharField(), label=_('Labels name'), required=False, read_only=True
+ )
"""
资产的数据结构
@@ -91,7 +94,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
]
fields_m2m = [
- 'nodes', 'nodes_display', 'labels',
+ 'nodes', 'nodes_display', 'labels', 'labels_display',
]
read_only_fields = [
'connectivity', 'date_verified', 'cpu_info', 'hardware_info',
diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py
index 2f491aa21..3806dc1b1 100644
--- a/apps/assets/serializers/cmd_filter.py
+++ b/apps/assets/serializers/cmd_filter.py
@@ -3,6 +3,7 @@
import re
from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
from ..models import CommandFilter, CommandFilterRule
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.utils import tmp_to_root_org
@@ -22,16 +23,14 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
'comment', 'created_by',
]
fields_fk = ['rules']
- fields_m2m = ['system_users']
+ fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
fields = fields_small + fields_fk + fields_m2m
extra_kwargs = {
- 'rules': {'read_only': True},
- 'system_users': {'required': False},
+ 'rules': {'read_only': True}
}
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
- invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')
@@ -39,13 +38,13 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
model = CommandFilterRule
fields_mini = ['id']
fields_small = fields_mini + [
- 'type', 'type_display', 'content', 'priority',
+ 'type', 'type_display', 'content', 'pattern', 'priority',
'action', 'action_display', 'reviewers',
'date_created', 'date_updated',
'comment', 'created_by',
]
fields_fk = ['filter']
- fields = '__all__'
+ fields = fields_small + fields_fk
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -61,15 +60,16 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
choices.pop(CommandFilterRule.ActionChoices.confirm, None)
action._choices = choices
- # def validate_content(self, content):
- # tp = self.initial_data.get("type")
- # if tp == CommandFilterRule.TYPE_REGEX:
- # return content
- # if self.invalid_pattern.search(content):
- # invalid_char = self.invalid_pattern.pattern.replace('\\', '')
- # msg = _("Content should not be contain: {}").format(invalid_char)
- # raise serializers.ValidationError(msg)
- # return content
+ def validate_content(self, content):
+ tp = self.initial_data.get("type")
+ if tp == CommandFilterRule.TYPE_COMMAND:
+ regex = CommandFilterRule.construct_command_regex(content)
+ else:
+ regex = content
+ succeed, error, pattern = CommandFilterRule.compile_regex(regex)
+ if not succeed:
+ raise serializers.ValidationError(error)
+ return content
class CommandConfirmSerializer(serializers.Serializer):
diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py
index b86740f8c..9a085fb27 100644
--- a/apps/assets/serializers/system_user.py
+++ b/apps/assets/serializers/system_user.py
@@ -48,8 +48,10 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
extra_kwargs = {
'password': {
"write_only": True,
+ 'trim_whitespace': False,
"validators": [validate_password_contains_left_double_curly_bracket]
},
+ 'cmd_filters': {"required": False},
'public_key': {"write_only": True},
'private_key': {"write_only": True},
'token': {"write_only": True},
diff --git a/apps/assets/signals_handler/node_assets_mapping.py b/apps/assets/signals_handler/node_assets_mapping.py
index 30eb9ed0b..5c2439005 100644
--- a/apps/assets/signals_handler/node_assets_mapping.py
+++ b/apps/assets/signals_handler/node_assets_mapping.py
@@ -10,7 +10,6 @@ from django.dispatch import receiver
from django.utils.functional import LazyObject
from common.signals import django_ready
-from common.db.utils import close_old_connections
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from assets.models import Asset, Node
@@ -78,30 +77,17 @@ def on_node_asset_change(sender, instance, **kwargs):
def subscribe_node_assets_mapping_expire(sender, **kwargs):
logger.debug("Start subscribe for expire node assets id mapping from memory")
+ def handle_node_relation_change(org_id):
+ root_org_id = Organization.ROOT_ID
+ Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
+ Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
+ logger.debug(
+ "Expire node assets id mapping from memory of org={}, pid={}"
+ "".format(str(org_id), os.getpid())
+ )
+
def keep_subscribe_node_assets_relation():
- while True:
- try:
- subscribe = node_assets_mapping_for_memory_pub_sub.subscribe()
- msgs = subscribe.listen()
- # 开始之前关闭连接,因为server端可能关闭了连接,而 client 还在 CONN_MAX_AGE 中
- close_old_connections()
- for message in msgs:
- if message["type"] != "message":
- continue
- org_id = message['data'].decode()
- root_org_id = Organization.ROOT_ID
- Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
- Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
- logger.debug(
- "Expire node assets id mapping from memory of org={}, pid={}"
- "".format(str(org_id), os.getpid())
- )
- except Exception as e:
- logger.exception(f'subscribe_node_assets_mapping_expire: {e}')
- Node.expire_all_orgs_node_all_asset_ids_mapping_from_memory()
- finally:
- # 请求结束,关闭连接
- close_old_connections()
+ node_assets_mapping_for_memory_pub_sub.keep_handle_msg(handle_node_relation_change)
t = threading.Thread(target=keep_subscribe_node_assets_relation)
t.daemon = True
diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py
index 0e3306a36..8d00e6543 100644
--- a/apps/assets/urls/api_urls.py
+++ b/apps/assets/urls/api_urls.py
@@ -48,6 +48,7 @@ urlpatterns = [
path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'),
path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
+ path('cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='cmd-filter-rules'),
path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index 4083edb90..9001e8650 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -2,9 +2,10 @@
#
import urllib.parse
import json
-import base64
from typing import Callable
import os
+import base64
+import ctypes
from django.conf import settings
from django.core.cache import cache
@@ -18,10 +19,12 @@ from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
+from applications.models import Application
from authentication.signals import post_auth_failed, post_auth_success
from common.utils import get_logger, random_string
from common.mixins.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
+from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
@@ -124,17 +127,40 @@ class ClientProtocolMixin:
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
- content = ''
- for k, v in options.items():
- content += f'{k}:{v}\n'
if asset:
name = asset.hostname
elif application:
name = application.name
+ application.get_rdp_remote_app_setting()
+
+ app = f'||jmservisor'
+ options['remoteapplicationmode:i'] = '1'
+ options['alternate shell:s'] = app
+ options['remoteapplicationprogram:s'] = app
+ options['remoteapplicationname:s'] = name
+ options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
else:
name = '*'
+
+ content = ''
+ for k, v in options.items():
+ content += f'{k}:{v}\n'
return name, content
+ def get_encrypt_cmdline(self, app: Application):
+
+ parameters = app.get_rdp_remote_app_setting()['parameters']
+ parameters = parameters.encode('ascii')
+
+ lib_path = get_file_by_arch('xpack/libs', 'librailencrypt.so')
+ lib = ctypes.CDLL(lib_path)
+ lib.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_int]
+ lib.encrypt.restype = ctypes.c_char_p
+
+ rst = lib.encrypt(parameters, len(parameters))
+ rst = rst.decode('ascii')
+ return rst
+
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
def get_rdp_file(self, request, *args, **kwargs):
if self.request.method == 'GET':
diff --git a/apps/authentication/api/token.py b/apps/authentication/api/token.py
index c46c4d5e2..df8c6eb3f 100644
--- a/apps/authentication/api/token.py
+++ b/apps/authentication/api/token.py
@@ -40,5 +40,7 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
return Response(e.as_data(), status=400)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
- except errors.PasswordTooSimple as e:
- return redirect(e.url)
+ except errors.MFAUnsetError:
+ return Response({'error': 'MFA unset, please set first'}, status=400)
+ except Exception as e:
+ return Response({"error": str(e)}, status=400)
diff --git a/apps/authentication/backends/saml2/__init__.py b/apps/authentication/backends/saml2/__init__.py
new file mode 100644
index 000000000..bbdbdb814
--- /dev/null
+++ b/apps/authentication/backends/saml2/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+#
+from .backends import *
diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py
new file mode 100644
index 000000000..0cacdf920
--- /dev/null
+++ b/apps/authentication/backends/saml2/backends.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+#
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+from django.db import transaction
+
+from common.utils import get_logger
+from authentication.errors import reason_choices, reason_user_invalid
+from .signals import (
+ saml2_user_authenticated, saml2_user_authentication_failed,
+ saml2_create_or_update_user
+)
+
+__all__ = ['SAML2Backend']
+
+logger = get_logger(__file__)
+
+
+class SAML2Backend(ModelBackend):
+ @staticmethod
+ def user_can_authenticate(user):
+ is_valid = getattr(user, 'is_valid', None)
+ return is_valid or is_valid is None
+
+ @transaction.atomic
+ def get_or_create_from_saml_data(self, request, **saml_user_data):
+ log_prompt = "Get or Create user [SAML2Backend]: {}"
+ logger.debug(log_prompt.format('start'))
+
+ user, created = get_user_model().objects.get_or_create(
+ username=saml_user_data['username'], defaults=saml_user_data
+ )
+ logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
+
+ logger.debug(log_prompt.format("Send signal => saml2 create or update user"))
+ saml2_create_or_update_user.send(
+ sender=self, request=request, user=user, created=created, attrs=saml_user_data
+ )
+ return user, created
+
+ def authenticate(self, request, saml_user_data=None, **kwargs):
+ log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
+ logger.debug(log_prompt.format('Start'))
+ if saml_user_data is None:
+ logger.debug(log_prompt.format('saml_user_data is missing'))
+ return None
+
+ username = saml_user_data.get('username')
+ if not username:
+ logger.debug(log_prompt.format('username is missing'))
+ return None
+
+ user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
+
+ if self.user_can_authenticate(user):
+ logger.debug(log_prompt.format('SAML2 user login success'))
+ saml2_user_authenticated.send(
+ sender=self, request=request, user=user, created=created
+ )
+ return user
+ else:
+ logger.debug(log_prompt.format('SAML2 user login failed'))
+ saml2_user_authentication_failed.send(
+ sender=self, request=request, username=username,
+ reason=reason_choices.get(reason_user_invalid)
+ )
+ return None
diff --git a/apps/authentication/backends/saml2/settings.py b/apps/authentication/backends/saml2/settings.py
new file mode 100644
index 000000000..1f5b91f9b
--- /dev/null
+++ b/apps/authentication/backends/saml2/settings.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+from onelogin.saml2.settings import OneLogin_Saml2_Settings
+
+
+class JmsSaml2Settings(OneLogin_Saml2_Settings):
+ def get_sp_key(self):
+ key = getattr(settings, 'SAML2_SP_KEY_CONTENT', '')
+ return key
+
+ def get_sp_cert(self):
+ cert = getattr(settings, 'SAML2_SP_CERT_CONTENT', '')
+ return cert
diff --git a/apps/authentication/backends/saml2/signals.py b/apps/authentication/backends/saml2/signals.py
new file mode 100644
index 000000000..42252f4d0
--- /dev/null
+++ b/apps/authentication/backends/saml2/signals.py
@@ -0,0 +1,6 @@
+from django.dispatch import Signal
+
+
+saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs'))
+saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request'))
+saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason'))
diff --git a/apps/authentication/backends/saml2/urls.py b/apps/authentication/backends/saml2/urls.py
new file mode 100644
index 000000000..d354fff58
--- /dev/null
+++ b/apps/authentication/backends/saml2/urls.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+#
+from django.urls import path
+
+from . import views
+
+
+urlpatterns = [
+ path('login/', views.Saml2AuthRequestView.as_view(), name='saml2-login'),
+ path('logout/', views.Saml2EndSessionView.as_view(), name='saml2-logout'),
+ path('callback/', views.Saml2AuthCallbackView.as_view(), name='saml2-callback'),
+ path('metadata/', views.Saml2AuthMetadataView.as_view(), name='saml2-metadata'),
+]
diff --git a/apps/authentication/backends/saml2/views.py b/apps/authentication/backends/saml2/views.py
new file mode 100644
index 000000000..26c88b5e2
--- /dev/null
+++ b/apps/authentication/backends/saml2/views.py
@@ -0,0 +1,269 @@
+import json
+import os
+
+from django.views import View
+from django.contrib import auth as auth
+from django.urls import reverse
+from django.conf import settings
+from django.views.decorators.csrf import csrf_exempt
+from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
+
+from onelogin.saml2.auth import OneLogin_Saml2_Auth
+from onelogin.saml2.errors import OneLogin_Saml2_Error
+from onelogin.saml2.idp_metadata_parser import (
+ OneLogin_Saml2_IdPMetadataParser as IdPMetadataParse,
+ dict_deep_merge
+)
+
+from .settings import JmsSaml2Settings
+
+from common.utils import get_logger
+
+logger = get_logger(__file__)
+
+
+class PrepareRequestMixin:
+ @staticmethod
+ def prepare_django_request(request):
+ result = {
+ 'https': 'on' if request.is_secure() else 'off',
+ 'http_host': request.META['HTTP_HOST'],
+ 'script_name': request.META['PATH_INFO'],
+ 'get_data': request.GET.copy(),
+ 'post_data': request.POST.copy()
+ }
+ return result
+
+ @staticmethod
+ def get_idp_settings():
+ idp_metadata_xml = settings.SAML2_IDP_METADATA_XML
+ idp_metadata_url = settings.SAML2_IDP_METADATA_URL
+ logger.debug('Start getting IDP configuration')
+
+ try:
+ xml_idp_settings = IdPMetadataParse.parse(idp_metadata_xml)
+ except Exception as err:
+ xml_idp_settings = None
+ logger.warning('Failed to get IDP metadata XML settings, error: %s', str(err))
+
+ try:
+ url_idp_settings = IdPMetadataParse.parse_remote(
+ idp_metadata_url, timeout=20
+ )
+ except Exception as err:
+ url_idp_settings = None
+ logger.warning('Failed to get IDP metadata URL settings, error: %s', str(err))
+
+ idp_settings = url_idp_settings or xml_idp_settings
+
+ if idp_settings is None:
+ msg = 'Unable to resolve IDP settings. '
+ tip = 'Please contact your administrator to check system settings,' \
+ 'or login using other methods.'
+ logger.error(msg)
+ raise OneLogin_Saml2_Error(msg + tip, OneLogin_Saml2_Error.SETTINGS_INVALID)
+
+ logger.debug('IDP settings obtained successfully')
+ return idp_settings
+
+ @staticmethod
+ def get_attribute_consuming_service():
+ attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
+ name_prefix = settings.SITE_URL
+ if attr_mapping and isinstance(attr_mapping, dict):
+ attr_list = [
+ {
+ "name": '{}/{}'.format(name_prefix, sp_key),
+ "friendlyName": idp_key, "isRequired": True
+ }
+ for idp_key, sp_key in attr_mapping.items()
+ ]
+ request_attribute_template = {
+ "attributeConsumingService": {
+ "isDefault": False,
+ "serviceName": "JumpServer",
+ "serviceDescription": "JumpServer",
+ "requestedAttributes": attr_list
+ }
+ }
+ return request_attribute_template
+ else:
+ return {}
+
+ @staticmethod
+ def get_advanced_settings():
+ other_settings = {}
+ other_settings_path = settings.SAML2_OTHER_SETTINGS_PATH
+ if os.path.exists(other_settings_path):
+ with open(other_settings_path, 'r') as json_data:
+ try:
+ other_settings = json.loads(json_data.read())
+ except Exception as error:
+ logger.error('Get other settings error: %s', error)
+
+ default = {
+ "organization": {
+ "en": {
+ "name": "JumpServer",
+ "displayname": "JumpServer",
+ "url": "https://jumpserver.org/"
+ }
+ }
+ }
+ default.update(other_settings)
+ return default
+
+ def get_sp_settings(self):
+ sp_host = settings.SITE_URL
+ attrs = self.get_attribute_consuming_service()
+ sp_settings = {
+ 'sp': {
+ 'entityId': f"{sp_host}{reverse('authentication:saml2:saml2-login')}",
+ 'assertionConsumerService': {
+ 'url': f"{sp_host}{reverse('authentication:saml2:saml2-callback')}",
+ },
+ 'singleLogoutService': {
+ 'url': f"{sp_host}{reverse('authentication:saml2:saml2-logout')}"
+ }
+ }
+ }
+ sp_settings['sp'].update(attrs)
+ advanced_settings = self.get_advanced_settings()
+ sp_settings.update(advanced_settings)
+ return sp_settings
+
+ def get_saml2_settings(self):
+ sp_settings = self.get_sp_settings()
+ idp_settings = self.get_idp_settings()
+ saml2_settings = dict_deep_merge(sp_settings, idp_settings)
+ return saml2_settings
+
+ def init_saml_auth(self, request):
+ request = self.prepare_django_request(request)
+ _settings = self.get_saml2_settings()
+ saml_instance = OneLogin_Saml2_Auth(
+ request, old_settings=_settings, custom_base_path=settings.SAML_FOLDER
+ )
+ return saml_instance
+
+ @staticmethod
+ def value_to_str(attr):
+ if isinstance(attr, str):
+ return attr
+ elif isinstance(attr, list) and len(attr) > 0:
+ return str(attr[0])
+
+ def get_attributes(self, saml_instance):
+ user_attrs = {}
+ real_key_index = len(settings.SITE_URL) + 1
+ attrs = saml_instance.get_attributes()
+
+ for attr, value in attrs.items():
+ attr = attr[real_key_index:]
+ user_attrs[attr] = self.value_to_str(value)
+ return user_attrs
+
+
+class Saml2AuthRequestView(View, PrepareRequestMixin):
+
+ def get(self, request):
+ log_prompt = "Process GET requests [SAML2AuthRequestView]: {}"
+ logger.debug(log_prompt.format('Start'))
+
+ try:
+ saml_instance = self.init_saml_auth(request)
+ except OneLogin_Saml2_Error as error:
+ logger.error(log_prompt.format('Init saml auth error: %s' % error))
+ return HttpResponse(error, status=412)
+
+ next_url = settings.AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT
+ url = saml_instance.login(return_to=next_url)
+ logger.debug(log_prompt.format('Redirect login url'))
+ return HttpResponseRedirect(url)
+
+
+class Saml2EndSessionView(View, PrepareRequestMixin):
+ http_method_names = ['get', 'post', ]
+
+ def get(self, request):
+ log_prompt = "Process GET requests [SAML2EndSessionView]: {}"
+ logger.debug(log_prompt.format('Start'))
+ return self.post(request)
+
+ def post(self, request):
+ log_prompt = "Process POST requests [SAML2EndSessionView]: {}"
+ logger.debug(log_prompt.format('Start'))
+
+ logout_url = settings.LOGOUT_REDIRECT_URL or '/'
+
+ if request.user.is_authenticated:
+ logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
+ auth.logout(request)
+
+ if settings.SAML2_LOGOUT_COMPLETELY:
+ saml_instance = self.init_saml_auth(request)
+ logger.debug(log_prompt.format('Log out IDP user session synchronously'))
+ return HttpResponseRedirect(saml_instance.logout())
+
+ logger.debug(log_prompt.format('Redirect logout url'))
+ return HttpResponseRedirect(logout_url)
+
+
+class Saml2AuthCallbackView(View, PrepareRequestMixin):
+
+ def post(self, request):
+ log_prompt = "Process POST requests [SAML2AuthCallbackView]: {}"
+ post_data = request.POST
+
+ try:
+ saml_instance = self.init_saml_auth(request)
+ except OneLogin_Saml2_Error as error:
+ logger.error(log_prompt.format('Init saml auth error: %s' % error))
+ return HttpResponse(error, status=412)
+
+ request_id = None
+ if 'AuthNRequestID' in request.session:
+ request_id = request.session['AuthNRequestID']
+
+ logger.debug(log_prompt.format('Process saml response'))
+ saml_instance.process_response(request_id=request_id)
+ errors = saml_instance.get_errors()
+
+ if not errors:
+ if 'AuthNRequestID' in request.session:
+ del request.session['AuthNRequestID']
+
+ logger.debug(log_prompt.format('Process authenticate'))
+ saml_user_data = self.get_attributes(saml_instance)
+ user = auth.authenticate(request=request, saml_user_data=saml_user_data)
+ if user and user.is_valid:
+ logger.debug(log_prompt.format('Login: {}'.format(user)))
+ auth.login(self.request, user)
+
+ logger.debug(log_prompt.format('Redirect'))
+ next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
+ return HttpResponseRedirect(next_url)
+ logger.error(log_prompt.format('Saml response has error: %s' % str(errors)))
+ return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI)
+
+ @csrf_exempt
+ def dispatch(self, *args, **kwargs):
+ return super().dispatch(*args, **kwargs)
+
+
+class Saml2AuthMetadataView(View, PrepareRequestMixin):
+
+ def get(self, _):
+ saml_settings = self.get_sp_settings()
+ saml_settings = JmsSaml2Settings(
+ settings=saml_settings, sp_validation_only=True,
+ custom_base_path=settings.SAML_FOLDER
+ )
+ metadata = saml_settings.get_sp_metadata()
+ errors = saml_settings.validate_metadata(metadata)
+
+ if len(errors) == 0:
+ resp = HttpResponse(content=metadata, content_type='text/xml')
+ else:
+ resp = HttpResponseServerError(content=', '.join(errors))
+ return resp
diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index abd78d9eb..9a75b4691 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -51,10 +51,14 @@ invalid_login_msg = _(
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
-block_login_msg = _(
+block_user_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
+block_ip_login_msg = _(
+ "The ip has been locked "
+ "(please contact admin to unlock it or try again after {} minutes)"
+)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
@@ -118,7 +122,7 @@ class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
- self.msg = _("IP is not allowed")
+ self.msg = block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
@@ -133,7 +137,7 @@ class CredentialError(
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder < 1:
- self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
+ self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
return
default_msg = invalid_login_msg.format(
@@ -184,7 +188,7 @@ class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
def __init__(self, username, ip):
- self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
+ self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip)
diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py
index d8533536d..16ca659c0 100644
--- a/apps/authentication/forms.py
+++ b/apps/authentication/forms.py
@@ -43,8 +43,8 @@ class UserLoginForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form):
- code = forms.CharField(label=_('MFA Code'), max_length=6, required=False)
- mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
+ code = forms.CharField(label=_('MFA Code'), max_length=128, required=False)
+ mfa_type = forms.CharField(label=_('MFA type'), max_length=128)
class CustomCaptchaTextInput(CaptchaTextInput):
@@ -57,7 +57,7 @@ class CaptchaMixin(forms.Form):
class ChallengeMixin(forms.Form):
challenge = forms.CharField(
- label=_('MFA code'), max_length=6, required=False,
+ label=_('MFA code'), max_length=128, required=False,
widget=forms.TextInput(attrs={
'placeholder': _("Dynamic code"),
'style': 'width: 50%'
diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py
index 9a5e4e793..9481c3ff6 100644
--- a/apps/authentication/middleware.py
+++ b/apps/authentication/middleware.py
@@ -1,14 +1,36 @@
-from django.shortcuts import redirect
+from django.shortcuts import redirect, reverse
+from django.http import HttpResponse
class MFAMiddleware:
+ """
+ 这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了,
+ 所以只能在 Middleware 中控制
+ """
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
- if request.path.find('/auth/login/otp/') > -1:
+ # 没有校验
+ if not request.session.get('auth_mfa_required'):
return response
- if request.session.get('auth_mfa_required'):
- return redirect('authentication:login-mfa')
- return response
+ # 没有认证过,证明不是从 第三方 来的
+ if request.user.is_anonymous:
+ return response
+
+ # 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
+ white_urls = [
+ 'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
+ '/profile/otp', '/logout/',
+ ]
+ for url in white_urls:
+ if request.path.find(url) > -1:
+ return response
+
+ # 因为使用 CAS/OIDC 登录的,不小心去了别的页面就回不来了
+ if request.path.find('users/profile') > -1:
+ return HttpResponse('', status=401)
+
+ url = reverse('authentication:login-mfa') + '?_=middleware'
+ return redirect(url)
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 7491b3b9e..2167ab198 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -257,7 +257,8 @@ class MFAMixin:
def _check_login_page_mfa_if_need(self, user):
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
return
- self._check_if_no_active_mfa(user)
+ if not user.active_mfa_backends:
+ return
request = self.request
data = request.data if hasattr(request, 'data') else request.POST
@@ -274,10 +275,8 @@ class MFAMixin:
if not user.mfa_enabled:
return
- self._check_if_no_active_mfa(user)
-
- active_mfa_mapper = user.active_mfa_backends_mapper
- raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
+ active_mfa_names = user.active_mfa_backends_mapper.keys()
+ raise errors.MFARequiredError(mfa_types=tuple(active_mfa_names))
def mark_mfa_ok(self, mfa_type):
self.request.session['auth_mfa'] = 1
@@ -417,12 +416,10 @@ class AuthACLMixin:
self.request.session["auth_confirm"] = "1"
return
elif ticket.state_reject:
- self.clean_mfa_mark()
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
elif ticket.state_close:
- self.clean_mfa_mark()
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py
index d895c8498..5cdd07984 100644
--- a/apps/authentication/signals_handlers.py
+++ b/apps/authentication/signals_handlers.py
@@ -8,6 +8,9 @@ from django_cas_ng.signals import cas_user_authenticated
from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success
+from authentication.backends.saml2.signals import (
+ saml2_user_authenticated, saml2_user_authentication_failed
+)
from .signals import post_auth_success, post_auth_failed
@@ -43,3 +46,15 @@ def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
def on_cas_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
post_auth_success.send(sender, user=user, request=request)
+
+
+@receiver(saml2_user_authenticated)
+def on_saml2_user_login_success(sender, request, user, **kwargs):
+ request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
+ post_auth_success.send(sender, user=user, request=request)
+
+
+@receiver(saml2_user_authentication_failed)
+def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
+ request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
+ post_auth_failed.send(sender, username=username, request=request, reason=reason)
diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py
index a976b624f..53944f759 100644
--- a/apps/authentication/urls/view_urls.py
+++ b/apps/authentication/urls/view_urls.py
@@ -56,5 +56,6 @@ urlpatterns = [
# openid
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')),
+ path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
path('captcha/', include('captcha.urls')),
]
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index f8c760b43..ecccf2375 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -48,7 +48,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
return None
next_url = request.GET.get('next') or '/'
auth_type = ''
-
if settings.AUTH_OPENID:
auth_type = 'OIDC'
openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
@@ -62,7 +61,13 @@ class UserLoginView(mixins.AuthMixin, FormView):
else:
cas_auth_url = None
- if not any([openid_auth_url, cas_auth_url]):
+ if settings.AUTH_SAML2:
+ auth_type = 'saml2'
+ saml2_auth_url = reverse(settings.SAML2_LOGIN_URL_NAME) + f'?next={next_url}'
+ else:
+ saml2_auth_url = None
+
+ if not any([openid_auth_url, cas_auth_url, saml2_auth_url]):
return None
login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
@@ -72,8 +77,10 @@ class UserLoginView(mixins.AuthMixin, FormView):
auth_url = cas_auth_url
elif login_redirect in ['openid', 'oidc'] and openid_auth_url:
auth_url = openid_auth_url
+ elif login_redirect in ['saml2'] and saml2_auth_url:
+ auth_url = saml2_auth_url
else:
- auth_url = openid_auth_url or cas_auth_url
+ auth_url = openid_auth_url or cas_auth_url or saml2_auth_url
if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED:
redirect_url = auth_url
@@ -166,6 +173,12 @@ class UserLoginView(mixins.AuthMixin, FormView):
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png')
},
+ {
+ 'name': 'SAML2',
+ 'enabled': settings.AUTH_SAML2,
+ 'url': reverse('authentication:saml2:saml2-login'),
+ 'logo': static('img/login_cas_logo.png')
+ },
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
@@ -292,6 +305,8 @@ class UserLogoutView(TemplateView):
return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME
elif 'CAS' in backend:
return settings.CAS_LOGOUT_URL_NAME
+ elif 'saml2' in backend:
+ return settings.SAML2_LOGOUT_URL_NAME
return None
def get(self, request, *args, **kwargs):
diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py
index ec51ed63c..fd8b80e32 100644
--- a/apps/authentication/views/mfa.py
+++ b/apps/authentication/views/mfa.py
@@ -3,6 +3,7 @@
from __future__ import unicode_literals
from django.views.generic.edit import FormView
+from django.shortcuts import redirect
from common.utils import get_logger
from .. import forms, errors, mixins
@@ -19,9 +20,15 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
def get(self, *args, **kwargs):
try:
- self.get_user_from_session()
+ user = self.get_user_from_session()
except errors.SessionEmptyError:
- return redirect_to_guard_view()
+ return redirect_to_guard_view('session_empty')
+
+ try:
+ self._check_if_no_active_mfa(user)
+ except errors.MFAUnsetError as e:
+ return redirect(e.url + '?_=login_mfa')
+
return super().get(*args, **kwargs)
def form_valid(self, form):
@@ -30,17 +37,17 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
try:
self._do_check_user_mfa(code, mfa_type)
- return redirect_to_guard_view()
+ return redirect_to_guard_view('mfa_ok')
except (errors.MFAFailedError, errors.BlockMFAError) as e:
form.add_error('code', e.msg)
return super().form_invalid(form)
except errors.SessionEmptyError:
- return redirect_to_guard_view()
+ return redirect_to_guard_view('session_empty')
except Exception as e:
logger.error(e)
import traceback
traceback.print_exc()
- return redirect_to_guard_view()
+ return redirect_to_guard_view('unexpect')
def get_context_data(self, **kwargs):
user = self.get_user_from_session()
diff --git a/apps/authentication/views/utils.py b/apps/authentication/views/utils.py
index 182d7390b..63a1d76c6 100644
--- a/apps/authentication/views/utils.py
+++ b/apps/authentication/views/utils.py
@@ -3,6 +3,6 @@
from django.shortcuts import reverse, redirect
-def redirect_to_guard_view():
- continue_url = reverse('authentication:login-guard')
+def redirect_to_guard_view(comment=''):
+ continue_url = reverse('authentication:login-guard') + '?_=' + comment
return redirect(continue_url)
diff --git a/apps/common/db/utils.py b/apps/common/db/utils.py
index 9d6123884..eb6328a9f 100644
--- a/apps/common/db/utils.py
+++ b/apps/common/db/utils.py
@@ -1,6 +1,9 @@
-from common.utils import get_logger
+from contextlib import contextmanager
+
from django.db import connections
+from common.utils import get_logger
+
logger = get_logger(__file__)
@@ -44,3 +47,10 @@ def get_objects(model, pks):
def close_old_connections():
for conn in connections.all():
conn.close_if_unusable_or_obsolete()
+
+
+@contextmanager
+def safe_db_connection():
+ close_old_connections()
+ yield
+ close_old_connections()
diff --git a/apps/common/sdk/sms/utils.py b/apps/common/sdk/sms/utils.py
index a0285df86..7474acc55 100644
--- a/apps/common/sdk/sms/utils.py
+++ b/apps/common/sdk/sms/utils.py
@@ -37,12 +37,17 @@ class SendAndVerifySMSUtil:
self.code = ''
self.timeout = timeout or self.TIMEOUT
self.key_suffix = key_suffix or str(phone)
- self.key = self.KEY_TMPL.format(key_suffix)
+ self.key = self.KEY_TMPL.format(self.key_suffix)
def gen_and_send(self):
"""
生成,保存,发送
"""
+ ttl = self.ttl()
+ if ttl > 0:
+ logger.error('Send sms too frequently, delay {}'.format(ttl))
+ raise CodeSendTooFrequently(ttl)
+
try:
code = self.generate()
self.send(code)
@@ -62,10 +67,6 @@ class SendAndVerifySMSUtil:
"""
发送信息的方法,如果有错误直接抛出 api 异常
"""
- ttl = self.ttl()
- if ttl > 0:
- logger.error('Send sms too frequently, delay {}'.format(ttl))
- raise CodeSendTooFrequently(ttl)
sms = SMS()
sms.send_verify_code(self.phone, code)
cache.set(self.key, self.code, self.timeout)
diff --git a/apps/common/tasks.py b/apps/common/tasks.py
index 715be5103..b9c7caf07 100644
--- a/apps/common/tasks.py
+++ b/apps/common/tasks.py
@@ -1,4 +1,6 @@
-from django.core.mail import send_mail
+import os
+
+from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings
from celery import shared_task
@@ -24,11 +26,31 @@ def send_mail_async(*args, **kwargs):
if len(args) == 3:
args = list(args)
args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0]
- email_from = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
- args.insert(2, email_from)
+ from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
+ args.insert(2, from_email)
args = tuple(args)
try:
return send_mail(*args, **kwargs)
except Exception as e:
logger.error("Sending mail error: {}".format(e))
+
+
+@shared_task
+def send_mail_attachment_async(subject, message, recipient_list, attachment_list=None):
+ if attachment_list is None:
+ attachment_list = []
+ from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
+ email = EmailMultiAlternatives(
+ subject=subject,
+ body=message,
+ from_email=from_email,
+ to=recipient_list
+ )
+ for attachment in attachment_list:
+ email.attach_file(attachment)
+ os.remove(attachment)
+ try:
+ return email.send()
+ except Exception as e:
+ logger.error("Sending mail attachment error: {}".format(e))
diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py
index 7c449b0f0..fb50009b2 100644
--- a/apps/common/utils/common.py
+++ b/apps/common/utils/common.py
@@ -10,7 +10,10 @@ from functools import wraps
import time
import ipaddress
import psutil
-from typing import Iterable
+import platform
+import os
+
+from django.conf import settings
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
ipip_db = None
@@ -252,7 +255,40 @@ def get_cpu_load():
return float(single_cpu_load_1)
+def get_docker_mem_usage_if_limit():
+ try:
+ with open('/sys/fs/cgroup/memory/memory.limit_in_bytes') as f:
+ limit_in_bytes = int(f.readline())
+ total = psutil.virtual_memory().total
+ if limit_in_bytes >= total:
+ raise ValueError('Not limit')
+
+ with open('/sys/fs/cgroup/memory/memory.usage_in_bytes') as f:
+ usage_in_bytes = int(f.readline())
+
+ with open('/sys/fs/cgroup/memory/memory.stat') as f:
+ inactive_file = 0
+ for line in f:
+ if line.startswith('total_inactive_file'):
+ name, inactive_file = line.split()
+ break
+
+ if line.startswith('inactive_file'):
+ name, inactive_file = line.split()
+ continue
+
+ inactive_file = int(inactive_file)
+ return ((usage_in_bytes - inactive_file) / limit_in_bytes) * 100
+
+ except Exception as e:
+ logger.debug(f'Get memory usage by docker limit: {e}')
+ return None
+
+
def get_memory_usage():
+ usage = get_docker_mem_usage_if_limit()
+ if usage is not None:
+ return usage
return psutil.virtual_memory().percent
@@ -293,3 +329,13 @@ def unique(objects, key=None):
if v not in seen:
seen[v] = obj
return list(seen.values())
+
+
+def get_file_by_arch(dir, filename):
+ platform_name = platform.system()
+ arch = platform.machine()
+
+ file_path = os.path.join(
+ settings.BASE_DIR, dir, platform_name, arch, filename
+ )
+ return file_path
diff --git a/apps/common/utils/connection.py b/apps/common/utils/connection.py
index 9bdf39628..b7d1f165a 100644
--- a/apps/common/utils/connection.py
+++ b/apps/common/utils/connection.py
@@ -1,6 +1,13 @@
+import json
+
import redis
from django.conf import settings
+from common.db.utils import safe_db_connection
+from common.utils import get_logger
+
+logger = get_logger(__name__)
+
def get_redis_client(db):
rc = redis.StrictRedis(
@@ -23,5 +30,38 @@ class RedisPubSub:
return ps
def publish(self, data):
- self.redis.publish(self.ch, data)
+ data_json = json.dumps(data)
+ self.redis.publish(self.ch, data_json)
return True
+
+ def keep_handle_msg(self, handle):
+ """
+ handle arg is the pub published
+
+ :param handle: lambda item: do_something
+ :return:
+ """
+ sub = self.subscribe()
+ msgs = sub.listen()
+
+ try:
+ for msg in msgs:
+ if msg["type"] != "message":
+ continue
+ try:
+ item_json = msg['data'].decode()
+ item = json.loads(item_json)
+
+ with safe_db_connection():
+ handle(item)
+ except Exception as e:
+ logger.error('Subscribe handler handle msg error: ', e)
+
+ except Exception as e:
+ logger.error('Consume msg error: ', e)
+
+ try:
+ sub.close()
+ except Exception as e:
+ logger.error("Redis observer close error: ", e)
+
diff --git a/apps/common/utils/file.py b/apps/common/utils/file.py
new file mode 100644
index 000000000..cb883ed55
--- /dev/null
+++ b/apps/common/utils/file.py
@@ -0,0 +1,19 @@
+import os
+import csv
+import pyzipper
+
+
+def create_csv_file(filename, headers, rows, ):
+ with open(filename, 'w', encoding='utf-8-sig')as f:
+ w = csv.writer(f)
+ w.writerow(headers)
+ w.writerows(rows)
+
+
+def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filename):
+ with pyzipper.AESZipFile(
+ filename, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES
+ ) as zf:
+ zf.setpassword(secret_password)
+ with open(encrypted_filename, 'rb') as f:
+ zf.writestr(os.path.basename(encrypted_filename), f.read())
diff --git a/apps/common/utils/lock.py b/apps/common/utils/lock.py
index 41b6376cf..c9e4e9734 100644
--- a/apps/common/utils/lock.py
+++ b/apps/common/utils/lock.py
@@ -1,7 +1,10 @@
from functools import wraps
import threading
-from redis_lock import Lock as RedisLock, NotAcquired
+from redis_lock import (
+ Lock as RedisLock, NotAcquired, UNLOCK_SCRIPT,
+ EXTEND_SCRIPT, RESET_SCRIPT, RESET_ALL_SCRIPT
+)
from redis import Redis
from django.db import transaction
@@ -49,7 +52,8 @@ class DistributedLock(RedisLock):
else:
auto_renewal = False
- super().__init__(redis_client=redis, name=name, expire=expire, auto_renewal=auto_renewal)
+ super().__init__(redis_client=redis, name='{' + name + '}', expire=expire, auto_renewal=auto_renewal)
+ self.register_scripts(redis)
self._release_on_transaction_commit = release_on_transaction_commit
self._release_raise_exc = release_raise_exc
self._reentrant = reentrant
@@ -71,8 +75,16 @@ class DistributedLock(RedisLock):
# 要创建一个新的锁对象
with self.__class__(**self.kwargs_copy):
return func(*args, **kwds)
+
return inner
+ @classmethod
+ def register_scripts(cls, redis_client):
+ cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
+ cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
+ cls.reset_script = redis_client.register_script(RESET_SCRIPT)
+ cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
+
def locked_by_me(self):
if self.locked():
if self.get_owner_id() == self.id:
@@ -92,8 +104,7 @@ class DistributedLock(RedisLock):
if self._reentrant:
if self.locked_by_current_thread():
self._acquired_reentrant_lock = True
- logger.debug(
- f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}')
+ logger.debug(f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}')
return True
logger.debug(f'Attempt acquire reentrant-lock: lock_id={self.id} lock={self.name} thread={self._thread_id}')
@@ -102,7 +113,8 @@ class DistributedLock(RedisLock):
logger.debug(f'Acquired reentrant-lock ok: lock_id={self.id} lock={self.name} thread={self._thread_id}')
setattr(thread_local, self.name, self.id)
else:
- logger.debug(f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id}')
+ logger.debug(
+ f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id}')
return acquired
else:
logger.debug(f'Attempt acquire lock: lock_id={self.id} lock={self.name} thread={self._thread_id}')
@@ -174,11 +186,13 @@ class DistributedLock(RedisLock):
else:
_release = self._release_on_reentrant_locked_by_brother
else:
- self._raise_exc_with_log(f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name} thread={self._thread_id}')
+ self._raise_exc_with_log(
+ f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name} thread={self._thread_id}')
# 处理是否在事务提交时才释放锁
if self._release_on_transaction_commit:
- logger.debug(f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name} thread={self._thread_id}')
+ logger.debug(
+ f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name} thread={self._thread_id}')
transaction.on_commit(_release)
else:
_release()
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 9d95609d2..9bffc38fa 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -229,6 +229,19 @@ class Config(dict):
'AUTH_SSO': False,
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
+ # SAML2 认证
+ 'AUTH_SAML2': False,
+ 'SAML2_LOGOUT_COMPLETELY': True,
+ 'AUTH_SAML2_ALWAYS_UPDATE_USER': True,
+ 'SAML2_RENAME_ATTRIBUTES': {'uid': 'username', 'email': 'email'},
+ 'SAML2_OTHER_SETTINGS_PATH': '',
+ 'SAML2_IDP_METADATA_URL': '',
+ 'SAML2_IDP_METADATA_XML': '',
+ 'SAML2_SP_KEY_CONTENT': '',
+ 'SAML2_SP_CERT_CONTENT': '',
+ 'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
+ 'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
+
# 企业微信
'AUTH_WECOM': False,
'WECOM_CORPID': '',
@@ -246,7 +259,7 @@ class Config(dict):
'FEISHU_APP_ID': '',
'FEISHU_APP_SECRET': '',
- 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS
+ 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2
'LOGIN_REDIRECT_MSG_ENABLED': True,
'SMS_ENABLED': False,
@@ -291,9 +304,6 @@ class Config(dict):
'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
'SECURITY_VIEW_AUTH_NEED_MFA': True,
- 'SECURITY_LOGIN_LIMIT_COUNT': 7,
- 'SECURITY_LOGIN_IP_BLACK_LIST': [],
- 'SECURITY_LOGIN_LIMIT_TIME': 30,
'SECURITY_MAX_IDLE_TIME': 30,
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
'SECURITY_PASSWORD_MIN_LENGTH': 6,
@@ -318,6 +328,14 @@ class Config(dict):
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
'ONLY_ALLOW_EXIST_USER_AUTH': False,
'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
+ # 用户登录限制的规则
+ 'SECURITY_LOGIN_LIMIT_COUNT': 7,
+ 'SECURITY_LOGIN_LIMIT_TIME': 30,
+ # 登录IP限制的规则
+ 'SECURITY_LOGIN_IP_BLACK_LIST': [],
+ 'SECURITY_LOGIN_IP_WHITE_LIST': [],
+ 'SECURITY_LOGIN_IP_LIMIT_COUNT': 99999,
+ 'SECURITY_LOGIN_IP_LIMIT_TIME': 30,
# 启动前
'HTTP_BIND_HOST': '0.0.0.0',
diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py
index a43307ab1..42ba9958a 100644
--- a/apps/jumpserver/settings/auth.py
+++ b/apps/jumpserver/settings/auth.py
@@ -4,7 +4,7 @@ import os
import ldap
from django.utils.translation import ugettext_lazy as _
-from ..const import CONFIG, PROJECT_DIR
+from ..const import CONFIG, PROJECT_DIR, BASE_DIR
# OTP settings
OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME
@@ -122,7 +122,16 @@ AUTH_FEISHU = CONFIG.AUTH_FEISHU
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
-
+# Saml2 auth
+AUTH_SAML2 = CONFIG.AUTH_SAML2
+AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT
+AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI = CONFIG.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI
+AUTH_SAML2_ALWAYS_UPDATE_USER = CONFIG.AUTH_SAML2_ALWAYS_UPDATE_USER
+SAML2_LOGOUT_COMPLETELY = CONFIG.SAML2_LOGOUT_COMPLETELY
+SAML2_RENAME_ATTRIBUTES = CONFIG.SAML2_RENAME_ATTRIBUTES
+SAML2_OTHER_SETTINGS_PATH = CONFIG.SAML2_OTHER_SETTINGS_PATH
+SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
+SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
# Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
@@ -141,6 +150,7 @@ AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
AUTH_BACKEND_FEISHU = 'authentication.backends.api.FeiShuAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
+AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
AUTHENTICATION_BACKENDS = [
@@ -156,7 +166,11 @@ if AUTH_OPENID:
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_CODE)
if AUTH_RADIUS:
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
+if AUTH_SAML2:
+ AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_SAML2)
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
+
+SAML_FOLDER = os.path.join(BASE_DIR, 'authentication', 'backends', 'saml2')
diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index 0673cbc1d..a95571194 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -32,11 +32,8 @@ TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
# Security settings
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
-SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
-SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
-SECURITY_LOGIN_IP_BLACK_LIST = CONFIG.SECURITY_LOGIN_IP_BLACK_LIST
-SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
+SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH # Unit: bit
@@ -63,6 +60,14 @@ SECURITY_INSECURE_COMMAND = CONFIG.SECURITY_INSECURE_COMMAND
SECURITY_INSECURE_COMMAND_LEVEL = CONFIG.SECURITY_INSECURE_COMMAND_LEVEL
SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = CONFIG.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER
SECURITY_CHECK_DIFFERENT_CITY_LOGIN = CONFIG.SECURITY_CHECK_DIFFERENT_CITY_LOGIN
+# 用户登录限制的规则
+SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
+SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
+# 登录IP限制的规则
+SECURITY_LOGIN_IP_BLACK_LIST = CONFIG.SECURITY_LOGIN_IP_BLACK_LIST
+SECURITY_LOGIN_IP_WHITE_LIST = CONFIG.SECURITY_LOGIN_IP_WHITE_LIST
+SECURITY_LOGIN_IP_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_IP_LIMIT_COUNT
+SECURITY_LOGIN_IP_LIMIT_TIME = CONFIG.SECURITY_LOGIN_IP_LIMIT_TIME # Unit: minute
# Terminal other setting
TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH
diff --git a/apps/jumpserver/views/swagger.py b/apps/jumpserver/views/swagger.py
index b42d283b6..5a18b30f1 100644
--- a/apps/jumpserver/views/swagger.py
+++ b/apps/jumpserver/views/swagger.py
@@ -55,7 +55,7 @@ api_info = openapi.Info(
description="JumpServer Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
- license=openapi.License(name="GPLv2 License"),
+ license=openapi.License(name="GPLv3 License"),
)
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 2b95a8070..e9a002a25 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4fea2cdf5a5477757cb95ff36016ed754fd65f839c12adbac9247ebdcca138ef
-size 93440
+oid sha256:9a8f5840c041b5f3188621ec731fa1b4a5da20730ea6394cf5e2b5c9c241a00e
+size 94712
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 012f71479..a592d4b78 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/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: 2021-11-17 16:39+0800\n"
+"POT-Creation-Date: 2021-12-09 20:32+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler \n"
"Language-Team: JumpServer team\n"
@@ -20,12 +20,12 @@ msgstr ""
#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47
#: applications/models/application.py:166 assets/models/asset.py:139
#: assets/models/base.py:175 assets/models/cluster.py:18
-#: assets/models/cmd_filter.py:21 assets/models/domain.py:24
+#: assets/models/cmd_filter.py:23 assets/models/domain.py:24
#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24
#: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29
#: settings/serializers/sms.py:6 terminal/models/storage.py:23
#: terminal/models/task.py:16 terminal/models/terminal.py:100
-#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:547
+#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:549
#: users/templates/users/_select_user_modal.html:13
#: users/templates/users/user_asset_permission.html:37
#: users/templates/users/user_asset_permission.html:154
@@ -34,13 +34,13 @@ msgstr ""
msgid "Name"
msgstr "名称"
-#: acls/models/base.py:27 assets/models/cmd_filter.py:54
-#: assets/models/user.py:199
+#: acls/models/base.py:27 assets/models/cmd_filter.py:77
+#: assets/models/user.py:200
msgid "Priority"
msgstr "优先级"
-#: acls/models/base.py:28 assets/models/cmd_filter.py:54
-#: assets/models/user.py:199
+#: acls/models/base.py:28 assets/models/cmd_filter.py:77
+#: assets/models/user.py:200
msgid "1-100, the lower the value will be match first"
msgstr "优先级可选范围为 1-100 (数值越小越优先)"
@@ -54,13 +54,13 @@ msgstr "激活中"
#: acls/models/base.py:32 applications/models/application.py:179
#: assets/models/asset.py:144 assets/models/asset.py:232
#: assets/models/base.py:180 assets/models/cluster.py:29
-#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64
+#: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:87
#: assets/models/domain.py:25 assets/models/domain.py:65
#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37
#: orgs/models.py:27 perms/models/base.py:53 settings/models.py:34
#: terminal/models/storage.py:26 terminal/models/terminal.py:114
#: tickets/models/ticket.py:71 users/models/group.py:16
-#: users/models/user.py:580 xpack/plugins/change_auth_plan/models/base.py:41
+#: users/models/user.py:585 xpack/plugins/change_auth_plan/models/base.py:41
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113
#: xpack/plugins/gathered_user/models.py:26
msgid "Comment"
@@ -70,7 +70,7 @@ msgstr "备注"
msgid "Reject"
msgstr "拒绝"
-#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:48
+#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:71
msgid "Allow"
msgstr "允许"
@@ -80,14 +80,15 @@ msgid "Login confirm"
msgstr "登录复核"
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
-#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56
-#: audits/models.py:74 audits/serializers.py:94 authentication/models.py:47
-#: orgs/models.py:19 orgs/models.py:433 perms/models/base.py:45
-#: templates/index.html:78 terminal/backends/command/models.py:18
-#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
+#: assets/models/cmd_filter.py:26 assets/models/label.py:15 audits/models.py:36
+#: audits/models.py:56 audits/models.py:74 audits/serializers.py:94
+#: authentication/models.py:47 orgs/models.py:19 orgs/models.py:433
+#: perms/models/base.py:45 templates/index.html:78
+#: terminal/backends/command/models.py:18
+#: terminal/backends/command/serializers.py:12 terminal/models/session.py:39
#: terminal/notifications.py:90 terminal/notifications.py:138
#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:169
-#: users/models/user.py:751 users/models/user.py:777
+#: users/models/user.py:756 users/models/user.py:782
#: users/serializers/group.py:19
#: users/templates/users/user_asset_permission.html:38
#: users/templates/users/user_asset_permission.html:64
@@ -102,7 +103,7 @@ msgstr "规则"
#: acls/models/login_acl.py:31 acls/models/login_asset_acl.py:26
#: acls/serializers/login_acl.py:17 acls/serializers/login_asset_acl.py:75
-#: assets/models/cmd_filter.py:57 audits/models.py:57
+#: assets/models/cmd_filter.py:80 audits/models.py:57
#: authentication/templates/authentication/_access_key_modal.html:34
#: users/templates/users/_granted_assets.html:29
#: users/templates/users/user_asset_permission.html:44
@@ -112,7 +113,7 @@ msgid "Action"
msgstr "动作"
#: acls/models/login_acl.py:35 acls/models/login_asset_acl.py:32
-#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:62
+#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:85
msgid "Reviewers"
msgstr "审批人"
@@ -128,15 +129,16 @@ msgstr "系统用户"
#: acls/models/login_asset_acl.py:22
#: applications/serializers/attrs/application_category/remote_app.py:37
-#: assets/models/asset.py:350 assets/models/authbook.py:18
-#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:258
-#: audits/models.py:38 perms/models/asset_permission.py:99
-#: templates/index.html:82 terminal/backends/command/models.py:19
-#: terminal/backends/command/serializers.py:13 terminal/models/session.py:40
+#: assets/models/asset.py:356 assets/models/authbook.py:18
+#: assets/models/cmd_filter.py:34 assets/models/gathered_user.py:14
+#: assets/serializers/system_user.py:260 audits/models.py:38
+#: perms/models/asset_permission.py:99 templates/index.html:82
+#: terminal/backends/command/models.py:19
+#: terminal/backends/command/serializers.py:13 terminal/models/session.py:41
#: terminal/notifications.py:89
#: users/templates/users/user_asset_permission.html:40
#: users/templates/users/user_asset_permission.html:70
-#: xpack/plugins/change_auth_plan/models/asset.py:195
+#: xpack/plugins/change_auth_plan/models/asset.py:200
#: xpack/plugins/cloud/models.py:217
msgid "Asset"
msgstr "资产"
@@ -162,12 +164,12 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
#: assets/models/base.py:176 assets/models/gathered_user.py:15
#: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17
#: authentication/templates/authentication/_msg_different_city.html:9
-#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:545
+#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:547
#: users/templates/users/_msg_user_created.html:12
#: users/templates/users/_select_user_modal.html:14
#: xpack/plugins/change_auth_plan/models/asset.py:35
-#: xpack/plugins/change_auth_plan/models/asset.py:191
-#: xpack/plugins/cloud/serializers/account_attrs.py:62
+#: xpack/plugins/change_auth_plan/models/asset.py:196
+#: xpack/plugins/cloud/serializers/account_attrs.py:22
msgid "Username"
msgstr "用户名"
@@ -180,7 +182,7 @@ msgstr ""
"格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, "
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
-#: acls/serializers/login_asset_acl.py:31 acls/serializers/rules/rules.py:32
+#: acls/serializers/login_asset_acl.py:31 acls/serializers/rules/rules.py:33
#: applications/serializers/attrs/application_type/mysql_workbench.py:18
#: assets/models/asset.py:211 assets/models/domain.py:61
#: assets/serializers/account.py:12
@@ -207,7 +209,7 @@ msgid ""
msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}"
#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:214
-#: assets/models/domain.py:63 assets/models/user.py:200
+#: assets/models/domain.py:63 assets/models/user.py:201
#: terminal/serializers/session.py:30 terminal/serializers/storage.py:69
msgid "Protocol"
msgstr "协议"
@@ -225,12 +227,12 @@ msgstr "组织 `{}` 不存在"
msgid "None of the reviewers belong to Organization `{}`"
msgstr "所有复核人都不属于组织 `{}`"
-#: acls/serializers/rules/rules.py:20 settings/serializers/security.py:35
+#: acls/serializers/rules/rules.py:20
#: xpack/plugins/cloud/serializers/task.py:23
msgid "IP address invalid: `{}`"
msgstr "IP 地址无效: `{}`"
-#: acls/serializers/rules/rules.py:26
+#: acls/serializers/rules/rules.py:25
msgid ""
"Format for comma-delimited string, with * indicating a match all. Such as: "
"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:"
@@ -239,7 +241,7 @@ msgstr ""
"格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, "
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
-#: acls/serializers/rules/rules.py:34
+#: acls/serializers/rules/rules.py:35
msgid "Time Period"
msgstr "时段"
@@ -258,16 +260,16 @@ msgstr "数据库"
msgid "Remote app"
msgstr "远程应用"
-#: applications/const.py:28
+#: applications/const.py:29
msgid "Custom"
msgstr "自定义"
#: applications/models/account.py:11 assets/models/authbook.py:19
-#: assets/models/user.py:291 audits/models.py:39
+#: assets/models/cmd_filter.py:38 assets/models/user.py:291 audits/models.py:39
#: perms/models/application_permission.py:32
#: perms/models/asset_permission.py:101 templates/_nav.html:45
#: terminal/backends/command/models.py:20
-#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42
+#: terminal/backends/command/serializers.py:14 terminal/models/session.py:43
#: users/templates/users/_granted_assets.html:27
#: users/templates/users/user_asset_permission.html:42
#: users/templates/users/user_asset_permission.html:76
@@ -275,7 +277,7 @@ msgstr "自定义"
#: users/templates/users/user_database_app_permission.html:40
#: users/templates/users/user_database_app_permission.html:67
#: xpack/plugins/change_auth_plan/models/app.py:36
-#: xpack/plugins/change_auth_plan/models/app.py:142
+#: xpack/plugins/change_auth_plan/models/app.py:147
msgid "System user"
msgstr "系统用户"
@@ -285,7 +287,7 @@ msgid "Version"
msgstr "版本"
#: applications/models/account.py:18 xpack/plugins/cloud/models.py:82
-#: xpack/plugins/cloud/serializers/task.py:65
+#: xpack/plugins/cloud/serializers/task.py:66
msgid "Account"
msgstr "账户"
@@ -297,20 +299,20 @@ msgstr "应用管理"
#: applications/serializers/application.py:88 assets/models/label.py:21
#: perms/models/application_permission.py:20
#: perms/serializers/application/user_permission.py:33
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:24
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:22
#: xpack/plugins/change_auth_plan/models/app.py:25
msgid "Category"
msgstr "类别"
#: applications/models/application.py:171
-#: applications/serializers/application.py:90 assets/models/cmd_filter.py:53
-#: assets/models/user.py:198 perms/models/application_permission.py:23
+#: applications/serializers/application.py:90 assets/models/cmd_filter.py:76
+#: assets/models/user.py:199 perms/models/application_permission.py:23
#: perms/serializers/application/user_permission.py:34
#: terminal/models/storage.py:55 terminal/models/storage.py:116
#: tickets/models/flow.py:51 tickets/models/ticket.py:48
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:31
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:29
#: xpack/plugins/change_auth_plan/models/app.py:28
-#: xpack/plugins/change_auth_plan/models/app.py:148
+#: xpack/plugins/change_auth_plan/models/app.py:153
msgid "Type"
msgstr "类型"
@@ -323,7 +325,7 @@ msgstr "网域"
msgid "Attrs"
msgstr ""
-#: applications/models/application.py:183
+#: applications/models/application.py:183 assets/models/cmd_filter.py:41
#: perms/models/application_permission.py:27 users/models/user.py:170
msgid "Application"
msgstr "应用程序"
@@ -331,7 +333,7 @@ msgstr "应用程序"
#: applications/serializers/application.py:59
#: applications/serializers/application.py:89 assets/serializers/label.py:13
#: perms/serializers/application/permission.py:16
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:28
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:26
msgid "Category display"
msgstr "类别名称"
@@ -339,7 +341,7 @@ msgstr "类别名称"
#: applications/serializers/application.py:91
#: assets/serializers/system_user.py:27 audits/serializers.py:29
#: perms/serializers/application/permission.py:17
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:35
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:33
#: tickets/serializers/ticket/ticket.py:22
#: tickets/serializers/ticket/ticket.py:168
msgid "Type display"
@@ -357,7 +359,7 @@ msgstr "集群"
#: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:146 settings/serializers/auth/radius.py:14
-#: xpack/plugins/cloud/serializers/account_attrs.py:60
+#: xpack/plugins/cloud/serializers/account_attrs.py:68
msgid "Host"
msgstr "主机"
@@ -366,9 +368,10 @@ msgstr "主机"
#: applications/serializers/attrs/application_type/mysql_workbench.py:22
#: applications/serializers/attrs/application_type/oracle.py:11
#: applications/serializers/attrs/application_type/pgsql.py:11
+#: applications/serializers/attrs/application_type/sqlserver.py:11
#: assets/models/asset.py:215 assets/models/domain.py:62
#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:61
+#: xpack/plugins/cloud/serializers/account_attrs.py:69
msgid "Port"
msgstr "端口"
@@ -380,12 +383,12 @@ msgid "Application path"
msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:45
-#: assets/serializers/system_user.py:157
+#: assets/serializers/system_user.py:159
#: xpack/plugins/change_auth_plan/serializers/asset.py:65
#: xpack/plugins/change_auth_plan/serializers/asset.py:68
#: xpack/plugins/change_auth_plan/serializers/asset.py:71
#: xpack/plugins/change_auth_plan/serializers/asset.py:87
-#: xpack/plugins/cloud/serializers/account_attrs.py:44
+#: xpack/plugins/cloud/serializers/account_attrs.py:52
msgid "This field is required."
msgstr "该字段是必填项。"
@@ -403,13 +406,12 @@ msgstr "目标URL"
#: authentication/templates/authentication/login.html:151
#: settings/serializers/auth/ldap.py:44 users/forms/profile.py:21
#: users/templates/users/_msg_user_created.html:13
-#: users/templates/users/user_otp_check_password.html:15
#: users/templates/users/user_password_update.html:43
#: users/templates/users/user_password_verify.html:18
#: xpack/plugins/change_auth_plan/models/base.py:39
-#: xpack/plugins/change_auth_plan/models/base.py:114
-#: xpack/plugins/change_auth_plan/models/base.py:182
-#: xpack/plugins/cloud/serializers/account_attrs.py:64
+#: xpack/plugins/change_auth_plan/models/base.py:118
+#: xpack/plugins/change_auth_plan/models/base.py:193
+#: xpack/plugins/cloud/serializers/account_attrs.py:24
msgid "Password"
msgstr "密码"
@@ -425,15 +427,15 @@ msgstr "目标URL"
msgid "Number required"
msgstr "需要为数字"
-#: assets/api/node.py:65
+#: assets/api/node.py:66
msgid "You can't update the root node name"
msgstr "不能修改根节点名称"
-#: assets/api/node.py:72
+#: assets/api/node.py:73
msgid "You can't delete the root node ({})"
msgstr "不能删除根节点 ({})"
-#: assets/api/node.py:75
+#: assets/api/node.py:76
msgid "Deletion failed and the node contains assets"
msgstr "删除失败,节点包含资产"
@@ -445,7 +447,7 @@ msgstr "基础"
msgid "Charset"
msgstr "编码"
-#: assets/models/asset.py:142 assets/serializers/asset.py:175
+#: assets/models/asset.py:142 assets/serializers/asset.py:178
#: tickets/models/ticket.py:50
msgid "Meta"
msgstr "元数据"
@@ -521,20 +523,20 @@ msgstr "主机名原始"
msgid "Protocols"
msgstr "协议组"
-#: assets/models/asset.py:219 assets/models/user.py:190
+#: assets/models/asset.py:219 assets/models/user.py:191
#: perms/models/asset_permission.py:100
#: xpack/plugins/change_auth_plan/models/asset.py:44
#: xpack/plugins/gathered_user/models.py:24
msgid "Nodes"
msgstr "节点"
-#: assets/models/asset.py:220 assets/models/cmd_filter.py:22
+#: assets/models/asset.py:220 assets/models/cmd_filter.py:43
#: assets/models/domain.py:66 assets/models/label.py:22
msgid "Is active"
msgstr "激活"
#: assets/models/asset.py:223 assets/models/cluster.py:19
-#: assets/models/user.py:187 assets/models/user.py:340 templates/_nav.html:44
+#: assets/models/user.py:188 assets/models/user.py:340 templates/_nav.html:44
msgid "Admin user"
msgstr "特权用户"
@@ -551,10 +553,10 @@ msgid "Labels"
msgstr "标签管理"
#: assets/models/asset.py:230 assets/models/base.py:183
-#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
-#: assets/models/cmd_filter.py:67 assets/models/group.py:21
+#: assets/models/cluster.py:28 assets/models/cmd_filter.py:48
+#: assets/models/cmd_filter.py:90 assets/models/group.py:21
#: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25
-#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:588
+#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:593
#: users/serializers/group.py:33
#: xpack/plugins/change_auth_plan/models/base.py:45
#: xpack/plugins/cloud/models.py:119 xpack/plugins/gathered_user/models.py:30
@@ -567,7 +569,7 @@ msgstr "创建者"
#: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50
#: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26
#: orgs/models.py:435 perms/models/base.py:52 users/models/group.py:18
-#: users/models/user.py:778 xpack/plugins/cloud/models.py:122
+#: users/models/user.py:783 xpack/plugins/cloud/models.py:122
msgid "Date created"
msgstr "创建日期"
@@ -584,7 +586,7 @@ msgid "Ok"
msgstr "成功"
#: assets/models/base.py:32 audits/models.py:102
-#: xpack/plugins/cloud/const.py:28
+#: xpack/plugins/cloud/const.py:29
msgid "Failed"
msgstr "失败"
@@ -597,14 +599,14 @@ msgid "Date verified"
msgstr "校验日期"
#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models/asset.py:54
-#: xpack/plugins/change_auth_plan/models/asset.py:126
-#: xpack/plugins/change_auth_plan/models/asset.py:202
+#: xpack/plugins/change_auth_plan/models/asset.py:131
+#: xpack/plugins/change_auth_plan/models/asset.py:207
msgid "SSH private key"
msgstr "SSH密钥"
#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models/asset.py:57
-#: xpack/plugins/change_auth_plan/models/asset.py:122
-#: xpack/plugins/change_auth_plan/models/asset.py:198
+#: xpack/plugins/change_auth_plan/models/asset.py:127
+#: xpack/plugins/change_auth_plan/models/asset.py:203
msgid "SSH public key"
msgstr "SSH公钥"
@@ -622,7 +624,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
-#: assets/models/cluster.py:22 users/models/user.py:566
+#: assets/models/cluster.py:22 users/models/user.py:568
msgid "Phone"
msgstr "手机"
@@ -648,7 +650,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
-#: users/models/user.py:763
+#: users/models/user.py:768
msgid "System"
msgstr "系统"
@@ -656,47 +658,61 @@ msgstr "系统"
msgid "Default Cluster"
msgstr "默认Cluster"
-#: assets/models/cmd_filter.py:33 assets/models/user.py:205
+#: assets/models/cmd_filter.py:30 perms/models/base.py:47
+#: templates/_nav.html:21 users/models/group.py:31 users/models/user.py:555
+#: users/templates/users/_select_user_modal.html:16
+#: users/templates/users/user_asset_permission.html:39
+#: users/templates/users/user_asset_permission.html:67
+#: users/templates/users/user_database_app_permission.html:38
+#: users/templates/users/user_database_app_permission.html:61
+msgid "User group"
+msgstr "用户组"
+
+#: assets/models/cmd_filter.py:56
msgid "Command filter"
msgstr "命令过滤器"
-#: assets/models/cmd_filter.py:40
+#: assets/models/cmd_filter.py:63
msgid "Regex"
msgstr "正则表达式"
-#: assets/models/cmd_filter.py:41 ops/models/command.py:25
-#: terminal/backends/command/serializers.py:15 terminal/models/session.py:49
+#: assets/models/cmd_filter.py:64 ops/models/command.py:25
+#: terminal/backends/command/serializers.py:15 terminal/models/session.py:50
#: terminal/templates/terminal/_msg_command_alert.html:12
#: terminal/templates/terminal/_msg_command_execute_alert.html:10
msgid "Command"
msgstr "命令"
-#: assets/models/cmd_filter.py:47
+#: assets/models/cmd_filter.py:70
msgid "Deny"
msgstr "拒绝"
-#: assets/models/cmd_filter.py:49
+#: assets/models/cmd_filter.py:72
msgid "Reconfirm"
msgstr "复核"
-#: assets/models/cmd_filter.py:52
+#: assets/models/cmd_filter.py:75
msgid "Filter"
msgstr "过滤器"
-#: assets/models/cmd_filter.py:56 settings/serializers/basic.py:10
+#: assets/models/cmd_filter.py:79 settings/serializers/basic.py:10
#: xpack/plugins/license/models.py:29
msgid "Content"
msgstr "内容"
-#: assets/models/cmd_filter.py:56
+#: assets/models/cmd_filter.py:79
msgid "One line one command"
msgstr "每行一个命令"
-#: assets/models/cmd_filter.py:71
+#: assets/models/cmd_filter.py:94
msgid "Command filter rule"
msgstr "命令过滤规则"
-#: assets/models/cmd_filter.py:111 tickets/const.py:13
+#: assets/models/cmd_filter.py:132
+msgid "The generated regular expression is incorrect: {}"
+msgstr "生成的正则表达式有误"
+
+#: assets/models/cmd_filter.py:158 tickets/const.py:13
msgid "Command confirm"
msgstr "命令复核"
@@ -765,59 +781,59 @@ msgstr "全称"
msgid "Parent key"
msgstr "ssh私钥"
-#: assets/models/node.py:559 assets/serializers/system_user.py:257
+#: assets/models/node.py:559 assets/serializers/system_user.py:259
#: users/templates/users/user_asset_permission.html:41
#: users/templates/users/user_asset_permission.html:73
#: users/templates/users/user_asset_permission.html:158
-#: xpack/plugins/cloud/models.py:93 xpack/plugins/cloud/serializers/task.py:68
+#: xpack/plugins/cloud/models.py:93 xpack/plugins/cloud/serializers/task.py:69
msgid "Node"
msgstr "节点"
-#: assets/models/user.py:181
+#: assets/models/user.py:182
msgid "Automatic managed"
msgstr "托管密码"
-#: assets/models/user.py:182
+#: assets/models/user.py:183
msgid "Manually input"
msgstr "手动输入"
-#: assets/models/user.py:186
+#: assets/models/user.py:187
msgid "Common user"
msgstr "普通用户"
-#: assets/models/user.py:189
+#: assets/models/user.py:190
msgid "Username same with user"
msgstr "用户名与用户相同"
-#: assets/models/user.py:192 assets/serializers/domain.py:29
+#: assets/models/user.py:193 assets/serializers/domain.py:29
#: templates/_nav.html:39
#: terminal/templates/terminal/_msg_command_execute_alert.html:16
#: xpack/plugins/change_auth_plan/models/asset.py:40
msgid "Assets"
msgstr "资产"
-#: assets/models/user.py:196 templates/_nav.html:17
+#: assets/models/user.py:197 templates/_nav.html:17
#: users/views/profile/pubkey.py:37
msgid "Users"
msgstr "用户管理"
-#: assets/models/user.py:197
+#: assets/models/user.py:198
msgid "User groups"
msgstr "用户组"
-#: assets/models/user.py:201
+#: assets/models/user.py:202
msgid "Auto push"
msgstr "自动推送"
-#: assets/models/user.py:202
+#: assets/models/user.py:203
msgid "Sudo"
msgstr "Sudo"
-#: assets/models/user.py:203
+#: assets/models/user.py:204
msgid "Shell"
msgstr "Shell"
-#: assets/models/user.py:204
+#: assets/models/user.py:205
msgid "Login mode"
msgstr "认证方式"
@@ -870,15 +886,19 @@ msgstr "网域名称"
msgid "Nodes name"
msgstr "节点名称"
-#: assets/serializers/asset.py:104
+#: assets/serializers/asset.py:73
+msgid "Labels name"
+msgstr "标签名称"
+
+#: assets/serializers/asset.py:107
msgid "Hardware info"
msgstr "硬件信息"
-#: assets/serializers/asset.py:105
+#: assets/serializers/asset.py:108
msgid "Admin user display"
msgstr "特权用户名称"
-#: assets/serializers/asset.py:106
+#: assets/serializers/asset.py:109
msgid "CPU info"
msgstr "CPU信息"
@@ -887,7 +907,7 @@ msgid "private key invalid"
msgstr "密钥不合法"
#: assets/serializers/domain.py:13 assets/serializers/label.py:12
-#: assets/serializers/system_user.py:57
+#: assets/serializers/system_user.py:59
#: perms/serializers/asset/permission.py:72
msgid "Assets amount"
msgstr "资产数量"
@@ -921,68 +941,68 @@ msgstr "密钥指纹"
msgid "Apps amount"
msgstr "应用数量"
-#: assets/serializers/system_user.py:56
+#: assets/serializers/system_user.py:58
#: perms/serializers/asset/permission.py:73
msgid "Nodes amount"
msgstr "节点数量"
-#: assets/serializers/system_user.py:58 assets/serializers/system_user.py:259
+#: assets/serializers/system_user.py:60 assets/serializers/system_user.py:261
msgid "Login mode display"
msgstr "认证方式名称"
-#: assets/serializers/system_user.py:60
+#: assets/serializers/system_user.py:62
msgid "Ad domain"
msgstr "Ad 网域"
-#: assets/serializers/system_user.py:61
+#: assets/serializers/system_user.py:63
msgid "Is asset protocol"
msgstr "资产协议"
-#: assets/serializers/system_user.py:62
+#: assets/serializers/system_user.py:64
msgid "Only ssh and automatic login system users are supported"
msgstr "仅支持ssh协议和自动登录的系统用户"
-#: assets/serializers/system_user.py:102
+#: assets/serializers/system_user.py:104
msgid "Username same with user with protocol {} only allow 1"
msgstr "用户名和用户相同的一种协议只允许存在一个"
-#: assets/serializers/system_user.py:112 common/validators.py:14
+#: assets/serializers/system_user.py:114 common/validators.py:14
msgid "Special char not allowed"
msgstr "不能包含特殊字符"
-#: assets/serializers/system_user.py:121
+#: assets/serializers/system_user.py:123
msgid "* Automatic login mode must fill in the username."
msgstr "自动登录模式,必须填写用户名"
-#: assets/serializers/system_user.py:136
+#: assets/serializers/system_user.py:138
msgid "Path should starts with /"
msgstr "路径应该以 / 开头"
-#: assets/serializers/system_user.py:148
+#: assets/serializers/system_user.py:150
msgid "Password or private key required"
msgstr "密码或密钥密码需要一个"
-#: assets/serializers/system_user.py:162
+#: assets/serializers/system_user.py:164
msgid "Only ssh protocol system users are allowed"
msgstr "仅允许ssh协议的系统用户"
-#: assets/serializers/system_user.py:166
+#: assets/serializers/system_user.py:168
msgid "The protocol must be consistent with the current user: {}"
msgstr "协议必须和当前用户保持一致: {}"
-#: assets/serializers/system_user.py:170
+#: assets/serializers/system_user.py:172
msgid "Only system users with automatic login are allowed"
msgstr "仅允许自动登录的系统用户"
-#: assets/serializers/system_user.py:275
+#: assets/serializers/system_user.py:277
msgid "System user name"
msgstr "系统用户名称"
-#: assets/serializers/system_user.py:276 orgs/mixins/serializers.py:26
+#: assets/serializers/system_user.py:278 orgs/mixins/serializers.py:26
msgid "Org name"
msgstr "组织名称"
-#: assets/serializers/system_user.py:285
+#: assets/serializers/system_user.py:287
msgid "Asset hostname"
msgstr "资产主机名"
@@ -1136,7 +1156,7 @@ msgid "Symlink"
msgstr "建立软链接"
#: audits/models.py:37 audits/models.py:60 audits/models.py:76
-#: terminal/models/session.py:45 terminal/models/sharing.py:76
+#: terminal/models/session.py:46 terminal/models/sharing.py:76
msgid "Remote addr"
msgstr "远端地址"
@@ -1153,11 +1173,11 @@ msgid "Success"
msgstr "成功"
#: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:49
-#: terminal/models/session.py:52
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:57
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:47
-#: xpack/plugins/change_auth_plan/models/base.py:105
-#: xpack/plugins/change_auth_plan/models/base.py:189
+#: terminal/models/session.py:53
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57
+#: xpack/plugins/change_auth_plan/models/base.py:109
+#: xpack/plugins/change_auth_plan/models/base.py:200
#: xpack/plugins/gathered_user/models.py:76
msgid "Date start"
msgstr "开始日期"
@@ -1223,13 +1243,13 @@ msgstr "用户代理"
#: audits/models.py:110
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:64 users/models/user.py:569
-#: users/serializers/profile.py:102
+#: users/forms/profile.py:64 users/models/user.py:571
+#: users/serializers/profile.py:123
msgid "MFA"
msgstr "MFA"
#: audits/models.py:111 terminal/models/sharing.py:88
-#: xpack/plugins/change_auth_plan/models/base.py:187
+#: xpack/plugins/change_auth_plan/models/base.py:198
#: xpack/plugins/cloud/models.py:176
msgid "Reason"
msgstr "原因"
@@ -1265,6 +1285,7 @@ msgstr "原因描述"
#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248
#: terminal/serializers/session.py:35
+#: xpack/plugins/change_auth_plan/models/base.py:199
msgid "Is success"
msgstr "是否成功"
@@ -1305,13 +1326,13 @@ msgstr ""
msgid "Auth Token"
msgstr "认证令牌"
-#: audits/signals_handler.py:68 authentication/views/login.py:170
-#: notifications/backends/__init__.py:11 users/models/user.py:602
+#: audits/signals_handler.py:68 authentication/views/login.py:183
+#: notifications/backends/__init__.py:11 users/models/user.py:607
msgid "WeCom"
msgstr "企业微信"
-#: audits/signals_handler.py:69 authentication/views/login.py:176
-#: notifications/backends/__init__.py:12 users/models/user.py:603
+#: audits/signals_handler.py:69 authentication/views/login.py:189
+#: notifications/backends/__init__.py:12 users/models/user.py:608
msgid "DingTalk"
msgstr "钉钉"
@@ -1498,7 +1519,7 @@ msgstr "{ApplicationPermission} 添加 {SystemUser}"
msgid "{ApplicationPermission} REMOVE {SystemUser}"
msgstr "{ApplicationPermission} 移除 {SystemUser}"
-#: authentication/api/connection_token.py:259
+#: authentication/api/connection_token.py:285
msgid "Invalid token"
msgstr "无效的令牌"
@@ -1619,13 +1640,19 @@ msgstr ""
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
"被临时 锁定 {block_time} 分钟)"
-#: authentication/errors.py:55 authentication/errors.py:59
+#: authentication/errors.py:55 authentication/errors.py:63
msgid ""
"The account has been locked (please contact admin to unlock it or try again "
"after {} minutes)"
-msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
+msgstr "账号已被锁定(请联系管理员解锁或{}分钟后重试)"
-#: authentication/errors.py:63
+#: authentication/errors.py:59
+msgid ""
+"The ip has been locked (please contact admin to unlock it or try again after "
+"{} minutes)"
+msgstr "IP 已被锁定(请联系管理员解锁或{}分钟后重试)"
+
+#: authentication/errors.py:67
#, python-brace-format
msgid ""
"{error}, You can also try {times_try} times (The account will be temporarily "
@@ -1633,63 +1660,63 @@ msgid ""
msgstr ""
"{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
-#: authentication/errors.py:67
+#: authentication/errors.py:71
msgid "MFA required"
msgstr "需要 MFA 认证"
-#: authentication/errors.py:68
+#: authentication/errors.py:72
msgid "MFA not set, please set it first"
msgstr "MFA 没有设置,请先完成设置"
-#: authentication/errors.py:69
+#: authentication/errors.py:73
msgid "Login confirm required"
msgstr "需要登录复核"
-#: authentication/errors.py:70
+#: authentication/errors.py:74
msgid "Wait login confirm ticket for accept"
msgstr "等待登录复核处理"
-#: authentication/errors.py:71
+#: authentication/errors.py:75
msgid "Login confirm ticket was {}"
msgstr "登录复核 {}"
-#: authentication/errors.py:184 authentication/errors.py:248
+#: authentication/errors.py:255
msgid "IP is not allowed"
msgstr "来源 IP 不被允许登录"
-#: authentication/errors.py:255
+#: authentication/errors.py:262
msgid "Time Period is not allowed"
msgstr "该 时间段 不被允许登录"
-#: authentication/errors.py:288
+#: authentication/errors.py:295
msgid "SSO auth closed"
msgstr "SSO 认证关闭了"
-#: authentication/errors.py:293 authentication/mixins.py:360
+#: authentication/errors.py:300 authentication/mixins.py:359
msgid "Your password is too simple, please change it for security"
msgstr "你的密码过于简单,为了安全,请修改"
-#: authentication/errors.py:302 authentication/mixins.py:367
+#: authentication/errors.py:309 authentication/mixins.py:366
msgid "You should to change your password before login"
msgstr "登录完成前,请先修改密码"
-#: authentication/errors.py:311 authentication/mixins.py:374
+#: authentication/errors.py:318 authentication/mixins.py:373
msgid "Your password has expired, please reset before logging in"
msgstr "您的密码已过期,先修改再登录"
-#: authentication/errors.py:345
+#: authentication/errors.py:352
msgid "Your password is invalid"
msgstr "您的密码无效"
-#: authentication/errors.py:350
+#: authentication/errors.py:357
msgid "Please enter MFA code"
msgstr "请输入 MFA 验证码"
-#: authentication/errors.py:355
+#: authentication/errors.py:362
msgid "Please enter SMS code"
msgstr "请输入短信验证码"
-#: authentication/errors.py:360 users/exceptions.py:15
+#: authentication/errors.py:367 users/exceptions.py:15
msgid "Phone not set"
msgstr "手机号没有设置"
@@ -1723,7 +1750,7 @@ msgstr "虚拟 MFA 验证码错误,或者服务器端时间不对"
#: authentication/mfa/otp.py:12
msgid "OTP"
-msgstr "MFA"
+msgstr "虚拟 MFA"
#: authentication/mfa/otp.py:13
msgid "OTP verification code"
@@ -1765,11 +1792,11 @@ msgstr "设置手机号码启用"
msgid "Clear phone number to disable"
msgstr "清空手机号码禁用"
-#: authentication/mixins.py:315
+#: authentication/mixins.py:314
msgid "The MFA type ({}) is not enabled"
msgstr "该 MFA ({}) 方式没有启用"
-#: authentication/mixins.py:350
+#: authentication/mixins.py:349
msgid "Please change your password"
msgstr "请修改密码"
@@ -1818,14 +1845,14 @@ msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
-#: settings/serializers/security.py:42 users/models/user.py:458
-#: users/serializers/profile.py:99 users/templates/users/mfa_setting.html:60
+#: settings/serializers/security.py:39 users/models/user.py:458
+#: users/serializers/profile.py:120 users/templates/users/mfa_setting.html:60
#: users/templates/users/user_verify_mfa.html:36
msgid "Disable"
msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67
-#: users/models/user.py:459 users/serializers/profile.py:100
+#: users/models/user.py:459 users/serializers/profile.py:121
#: users/templates/users/mfa_setting.html:26
#: users/templates/users/mfa_setting.html:67
msgid "Enable"
@@ -1872,7 +1899,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:269
+#: jumpserver/conf.py:282
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: users/templates/users/_msg_account_expire_reminder.html:4
@@ -1970,7 +1997,7 @@ msgid "MFA Auth"
msgstr "MFA 多因子认证"
#: authentication/templates/authentication/login_mfa.html:19
-#: users/templates/users/user_otp_check_password.html:18
+#: users/templates/users/user_otp_check_password.html:12
#: users/templates/users/user_otp_enable_bind.html:24
#: users/templates/users/user_otp_enable_install_app.html:29
#: users/templates/users/user_verify_mfa.html:30
@@ -2080,24 +2107,24 @@ msgstr "没有绑定飞书"
msgid "Please login with a password and then bind the FeiShu"
msgstr "请使用密码登录,然后绑定飞书"
-#: authentication/views/login.py:82
+#: authentication/views/login.py:89
msgid "Redirecting"
msgstr "跳转中"
-#: authentication/views/login.py:83
+#: authentication/views/login.py:90
msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证"
-#: authentication/views/login.py:109
+#: authentication/views/login.py:116
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
-#: authentication/views/login.py:182 notifications/backends/__init__.py:14
-#: users/models/user.py:604
+#: authentication/views/login.py:195 notifications/backends/__init__.py:14
+#: users/models/user.py:609
msgid "FeiShu"
msgstr "飞书"
-#: authentication/views/login.py:271
+#: authentication/views/login.py:284
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -2105,15 +2132,15 @@ msgstr ""
"等待 {} 确认, 你也可以复制链接发给他/她
\n"
" 不要关闭本页面"
-#: authentication/views/login.py:276
+#: authentication/views/login.py:289
msgid "No ticket found"
msgstr "没有发现工单"
-#: authentication/views/login.py:308
+#: authentication/views/login.py:323
msgid "Logout success"
msgstr "退出登录成功"
-#: authentication/views/login.py:309
+#: authentication/views/login.py:324
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@@ -2312,11 +2339,11 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
-#: jumpserver/conf.py:268
+#: jumpserver/conf.py:281
msgid "Create account successfully"
msgstr "创建账户成功"
-#: jumpserver/conf.py:270
+#: jumpserver/conf.py:283
msgid "Your account has been created successfully"
msgstr "你的账户已创建成功"
@@ -2352,7 +2379,7 @@ msgstr ""
"div>"
#: notifications/backends/__init__.py:10 users/forms/profile.py:101
-#: users/models/user.py:549
+#: users/models/user.py:551
msgid "Email"
msgstr "邮件"
@@ -2361,8 +2388,8 @@ msgid "Site message"
msgstr "站内信"
#: notifications/notifications.py:172 ops/models/adhoc.py:246
-#: xpack/plugins/change_auth_plan/models/base.py:108
-#: xpack/plugins/change_auth_plan/models/base.py:190
+#: xpack/plugins/change_auth_plan/models/base.py:112
+#: xpack/plugins/change_auth_plan/models/base.py:201
#: xpack/plugins/gathered_user/models.py:79
msgid "Time"
msgstr "时间"
@@ -2567,7 +2594,7 @@ msgstr "组织审计员"
msgid "GLOBAL"
msgstr "全局组织"
-#: orgs/models.py:434 users/models/user.py:557 users/serializers/user.py:37
+#: orgs/models.py:434 users/models/user.py:559 users/serializers/user.py:37
#: users/templates/users/_select_user_modal.html:15
msgid "Role"
msgstr "角色"
@@ -2627,19 +2654,10 @@ msgstr "未分组"
msgid "Favorite"
msgstr "收藏夹"
-#: perms/models/base.py:47 templates/_nav.html:21 users/models/group.py:31
-#: users/models/user.py:553 users/templates/users/_select_user_modal.html:16
-#: users/templates/users/user_asset_permission.html:39
-#: users/templates/users/user_asset_permission.html:67
-#: users/templates/users/user_database_app_permission.html:38
-#: users/templates/users/user_database_app_permission.html:61
-msgid "User group"
-msgstr "用户组"
-
#: perms/models/base.py:50
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:60
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:50
-#: users/models/user.py:585
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:58
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:60
+#: users/models/user.py:590
msgid "Date expired"
msgstr "失效日期"
@@ -2785,7 +2803,7 @@ msgstr "获取 LDAP 用户为 None"
msgid "Imported {} users successfully (Organization: {})"
msgstr "成功导入 {} 个用户 ( 组织: {} )"
-#: settings/models.py:191 users/templates/users/reset_password.html:29
+#: settings/models.py:196 users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "设置"
@@ -2817,11 +2835,15 @@ msgstr "企业微信 认证"
msgid "SSO Auth"
msgstr "SSO Token 认证"
-#: settings/serializers/auth/base.py:19 settings/serializers/basic.py:36
+#: settings/serializers/auth/base.py:17
+msgid "SAML2 Auth"
+msgstr "SAML2 认证"
+
+#: settings/serializers/auth/base.py:20 settings/serializers/basic.py:36
msgid "Forgot password url"
msgstr "忘记密码 URL"
-#: settings/serializers/auth/base.py:25
+#: settings/serializers/auth/base.py:26
msgid "Enable login redirect msg"
msgstr "启用登录跳转提示"
@@ -2837,7 +2859,7 @@ msgstr "服务端地址"
msgid "Proxy server url"
msgstr "代理服务地址"
-#: settings/serializers/auth/cas.py:14
+#: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:29
msgid "Logout completely"
msgstr "同步注销"
@@ -2849,7 +2871,7 @@ msgstr "用户名属性"
msgid "Enable attributes map"
msgstr "启用属性映射"
-#: settings/serializers/auth/cas.py:18
+#: settings/serializers/auth/cas.py:18 settings/serializers/auth/saml2.py:28
msgid "Rename attr"
msgstr "映射属性"
@@ -2907,7 +2929,7 @@ msgstr ""
"email 是jumpserver的用户需要属性"
#: settings/serializers/auth/ldap.py:58
-#: xpack/plugins/cloud/serializers/task.py:69
+#: xpack/plugins/cloud/serializers/task.py:70
#: xpack/plugins/gathered_user/serializers.py:20
msgid "Periodic display"
msgstr "定时执行"
@@ -2933,7 +2955,7 @@ msgid "Client Id"
msgstr "客户端 ID"
#: settings/serializers/auth/oidc.py:18
-#: xpack/plugins/cloud/serializers/account_attrs.py:26
+#: xpack/plugins/cloud/serializers/account_attrs.py:34
msgid "Client Secret"
msgstr "客户端密钥"
@@ -3009,7 +3031,7 @@ msgstr "使用状态"
msgid "Use nonce"
msgstr "临时使用"
-#: settings/serializers/auth/oidc.py:76
+#: settings/serializers/auth/oidc.py:76 settings/serializers/auth/saml2.py:30
msgid "Always update user"
msgstr "总是更新用户信息"
@@ -3021,6 +3043,26 @@ msgstr "启用 Radius 认证"
msgid "OTP in Radius"
msgstr "使用 Radius OTP"
+#: settings/serializers/auth/saml2.py:12
+msgid "Enable SAML2 Auth"
+msgstr "启用 SAML2 认证"
+
+#: settings/serializers/auth/saml2.py:15
+msgid "IDP Metadata URL"
+msgstr ""
+
+#: settings/serializers/auth/saml2.py:18
+msgid "IDP Metadata XML"
+msgstr ""
+
+#: settings/serializers/auth/saml2.py:22
+msgid "SP Private Key"
+msgstr ""
+
+#: settings/serializers/auth/saml2.py:26
+msgid "SP Public Cert"
+msgstr ""
+
#: settings/serializers/auth/sms.py:10
msgid "Enable SMS"
msgstr "启用 SMS"
@@ -3326,49 +3368,53 @@ msgstr "必须包含数字"
msgid "Must contain special"
msgstr "必须包含特殊字符"
-#: settings/serializers/security.py:43
-msgid "All users"
-msgstr "所有用户"
-
-#: settings/serializers/security.py:44
-msgid "Only admin users"
-msgstr "仅管理员"
-
-#: settings/serializers/security.py:46
-msgid "Global MFA auth"
-msgstr "全局启用 MFA 认证"
-
-#: settings/serializers/security.py:50
-msgid "Limit the number of login failures"
-msgstr "限制登录失败次数"
-
-#: settings/serializers/security.py:54
-msgid "Block logon interval"
-msgstr "禁止登录时间间隔"
-
-#: settings/serializers/security.py:56
+#: settings/serializers/security.py:31
msgid ""
"Unit: minute, If the user has failed to log in for a limited number of "
"times, no login is allowed during this time interval."
msgstr "单位:分, 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
-#: settings/serializers/security.py:61
-msgid "Login IP Black List"
-msgstr "登录 IP 黑名单"
+#: settings/serializers/security.py:40
+msgid "All users"
+msgstr "所有用户"
+
+#: settings/serializers/security.py:41
+msgid "Only admin users"
+msgstr "仅管理员"
+
+#: settings/serializers/security.py:43
+msgid "Global MFA auth"
+msgstr "全局启用 MFA 认证"
+
+#: settings/serializers/security.py:47
+msgid "Limit the number of user login failures"
+msgstr "限制用户登录失败次数"
+
+#: settings/serializers/security.py:51
+msgid "Block user login interval"
+msgstr "禁止用户登录时间间隔"
+
+#: settings/serializers/security.py:56
+msgid "Limit the number of IP login failures"
+msgstr "限制 IP 登录失败次数"
+
+#: settings/serializers/security.py:60
+msgid "Block IP login interval"
+msgstr "禁止 IP 登录时间间隔"
#: settings/serializers/security.py:64
-msgid ""
-"Format for comma-delimited string. Such as: 192.168.10.1, 192.168.1.0/24, "
-"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
-msgstr ""
-"格式为逗号分隔的字符串。例如: 192.168.10.1, 192.168.1.0/24, "
-"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
+msgid "Login IP White List"
+msgstr "IP 登录白名单"
-#: settings/serializers/security.py:70
+#: settings/serializers/security.py:69
+msgid "Login IP Black List"
+msgstr "IP 登录黑名单"
+
+#: settings/serializers/security.py:75
msgid "User password expiration"
msgstr "用户密码过期时间"
-#: settings/serializers/security.py:72
+#: settings/serializers/security.py:77
msgid ""
"Unit: day, If the user does not update the password during the time, the "
"user password will expire failure;The password expiration reminder mail will "
@@ -3378,55 +3424,55 @@ msgstr ""
"单位:天, 如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件"
"将在密码过期前5天内由系统(每天)自动发送给用户"
-#: settings/serializers/security.py:79
+#: settings/serializers/security.py:84
msgid "Number of repeated historical passwords"
msgstr "不能设置近几次密码"
-#: settings/serializers/security.py:81
+#: settings/serializers/security.py:86
msgid ""
"Tip: When the user resets the password, it cannot be the previous n "
"historical passwords of the user"
msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
-#: settings/serializers/security.py:86
+#: settings/serializers/security.py:91
msgid "Only single device login"
msgstr "仅一台设备登录"
-#: settings/serializers/security.py:87
+#: settings/serializers/security.py:92
msgid "Next device login, pre login will be logout"
msgstr "下个设备登录,上次登录会被顶掉"
-#: settings/serializers/security.py:90
+#: settings/serializers/security.py:95
msgid "Only exist user login"
msgstr "仅已存在用户登录"
-#: settings/serializers/security.py:91
+#: settings/serializers/security.py:96
msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet"
msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败"
-#: settings/serializers/security.py:94
+#: settings/serializers/security.py:99
msgid "Only from source login"
msgstr "仅从用户来源登录"
-#: settings/serializers/security.py:95
+#: settings/serializers/security.py:100
msgid "Only log in from the user source property"
msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败"
-#: settings/serializers/security.py:99
+#: settings/serializers/security.py:104
msgid "MFA verify TTL"
msgstr "MFA 校验有效期"
-#: settings/serializers/security.py:101
+#: settings/serializers/security.py:106
msgid ""
"Unit: second, The verification MFA takes effect only when you view the "
"account password"
msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效"
-#: settings/serializers/security.py:106
+#: settings/serializers/security.py:111
msgid "Enable Login dynamic code"
msgstr "启用登录附加码"
-#: settings/serializers/security.py:107
+#: settings/serializers/security.py:112
msgid ""
"The password and additional code are sent to a third party authentication "
"system for verification"
@@ -3434,96 +3480,96 @@ msgstr ""
"密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密"
"码+6位数字 完成认证"
-#: settings/serializers/security.py:112
+#: settings/serializers/security.py:117
msgid "MFA in login page"
msgstr "MFA 在登录页面输入"
-#: settings/serializers/security.py:113
+#: settings/serializers/security.py:118
msgid "Eu security regulations(GDPR) require MFA to be on the login page"
msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全"
-#: settings/serializers/security.py:116
+#: settings/serializers/security.py:121
msgid "Enable Login captcha"
msgstr "启用登录验证码"
-#: settings/serializers/security.py:117
+#: settings/serializers/security.py:122
msgid "Enable captcha to prevent robot authentication"
msgstr "开启验证码,防止机器人登录"
-#: settings/serializers/security.py:137
+#: settings/serializers/security.py:142
msgid "Enable terminal register"
msgstr "终端注册"
-#: settings/serializers/security.py:139
+#: settings/serializers/security.py:144
msgid ""
"Allow terminal register, after all terminal setup, you should disable this "
"for security"
msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
-#: settings/serializers/security.py:143
+#: settings/serializers/security.py:148
msgid "Enable watermark"
msgstr "开启水印"
-#: settings/serializers/security.py:144
+#: settings/serializers/security.py:149
msgid "Enabled, the web session and replay contains watermark information"
msgstr "启用后,Web 会话和录像将包含水印信息"
-#: settings/serializers/security.py:148
+#: settings/serializers/security.py:153
msgid "Connection max idle time"
msgstr "连接最大空闲时间"
-#: settings/serializers/security.py:149
+#: settings/serializers/security.py:154
msgid "If idle time more than it, disconnect connection Unit: minute"
msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
-#: settings/serializers/security.py:152
+#: settings/serializers/security.py:157
msgid "Remember manual auth"
msgstr "保存手动输入密码"
-#: settings/serializers/security.py:155
+#: settings/serializers/security.py:160
msgid "Enable change auth secure mode"
msgstr "启用改密安全模式"
-#: settings/serializers/security.py:158
+#: settings/serializers/security.py:163
msgid "Insecure command alert"
msgstr "危险命令告警"
-#: settings/serializers/security.py:161
+#: settings/serializers/security.py:166
msgid "Email recipient"
msgstr "邮件收件人"
-#: settings/serializers/security.py:162
+#: settings/serializers/security.py:167
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
-#: settings/serializers/security.py:165
+#: settings/serializers/security.py:170
msgid "Batch command execution"
msgstr "批量命令执行"
-#: settings/serializers/security.py:166
+#: settings/serializers/security.py:171
msgid "Allow user run batch command or not using ansible"
msgstr "是否允许用户使用 ansible 执行批量命令"
-#: settings/serializers/security.py:169
+#: settings/serializers/security.py:174
msgid "Session share"
msgstr "会话分享"
-#: settings/serializers/security.py:170
+#: settings/serializers/security.py:175
msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作"
-#: settings/serializers/security.py:173
+#: settings/serializers/security.py:178
msgid "Remote Login Protection"
msgstr "异地登录保护"
-#: settings/serializers/security.py:175
+#: settings/serializers/security.py:180
msgid ""
"The system determines whether the login IP address belongs to a common login "
"city. If the account is logged in from a common login city, the system sends "
"a remote login reminder"
msgstr ""
-"根据登录IP是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地登"
-"录提醒"
+"根据登录 IP 是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地"
+"登录提醒"
#: settings/serializers/sms.py:7
msgid "Label"
@@ -3579,104 +3625,104 @@ msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389"
msgid "Enable XRDP"
msgstr "启用 XRDP 服务"
-#: settings/utils/ldap.py:412
+#: settings/utils/ldap.py:415
msgid "ldap:// or ldaps:// protocol is used."
msgstr "使用 ldap:// 或 ldaps:// 协议"
-#: settings/utils/ldap.py:423
+#: settings/utils/ldap.py:426
msgid "Host or port is disconnected: {}"
msgstr "主机或端口不可连接: {}"
-#: settings/utils/ldap.py:425
+#: settings/utils/ldap.py:428
msgid "The port is not the port of the LDAP service: {}"
msgstr "端口不是LDAP服务端口: {}"
-#: settings/utils/ldap.py:427
+#: settings/utils/ldap.py:430
msgid "Please add certificate: {}"
msgstr "请添加证书"
-#: settings/utils/ldap.py:431 settings/utils/ldap.py:458
-#: settings/utils/ldap.py:488 settings/utils/ldap.py:516
+#: settings/utils/ldap.py:434 settings/utils/ldap.py:461
+#: settings/utils/ldap.py:491 settings/utils/ldap.py:519
msgid "Unknown error: {}"
msgstr "未知错误: {}"
-#: settings/utils/ldap.py:445
+#: settings/utils/ldap.py:448
msgid "Bind DN or Password incorrect"
msgstr "绑定DN或密码错误"
-#: settings/utils/ldap.py:452
+#: settings/utils/ldap.py:455
msgid "Please enter Bind DN: {}"
msgstr "请输入绑定DN: {}"
-#: settings/utils/ldap.py:454
+#: settings/utils/ldap.py:457
msgid "Please enter Password: {}"
msgstr "请输入密码: {}"
-#: settings/utils/ldap.py:456
+#: settings/utils/ldap.py:459
msgid "Please enter correct Bind DN and Password: {}"
msgstr "请输入正确的绑定DN和密码: {}"
-#: settings/utils/ldap.py:474
+#: settings/utils/ldap.py:477
msgid "Invalid User OU or User search filter: {}"
msgstr "不合法的用户OU或用户过滤器: {}"
-#: settings/utils/ldap.py:505
+#: settings/utils/ldap.py:508
msgid "LDAP User attr map not include: {}"
msgstr "LDAP属性映射没有包含: {}"
-#: settings/utils/ldap.py:512
+#: settings/utils/ldap.py:515
msgid "LDAP User attr map is not dict"
msgstr "LDAP属性映射不合法"
-#: settings/utils/ldap.py:531
+#: settings/utils/ldap.py:534
msgid "LDAP authentication is not enabled"
msgstr "LDAP认证没有启用"
-#: settings/utils/ldap.py:549
+#: settings/utils/ldap.py:552
msgid "Error (Invalid LDAP server): {}"
msgstr "错误 (不合法的LDAP服务器地址): {}"
-#: settings/utils/ldap.py:551
+#: settings/utils/ldap.py:554
msgid "Error (Invalid Bind DN): {}"
msgstr "错误(不合法的绑定DN): {}"
-#: settings/utils/ldap.py:553
+#: settings/utils/ldap.py:556
msgid "Error (Invalid LDAP User attr map): {}"
msgstr "错误(不合法的LDAP属性映射): {}"
-#: settings/utils/ldap.py:555
+#: settings/utils/ldap.py:558
msgid "Error (Invalid User OU or User search filter): {}"
msgstr "错误(不合法的用户OU或用户过滤器): {}"
-#: settings/utils/ldap.py:557
+#: settings/utils/ldap.py:560
msgid "Error (Not enabled LDAP authentication): {}"
msgstr "错误(没有启用LDAP认证): {}"
-#: settings/utils/ldap.py:559
+#: settings/utils/ldap.py:562
msgid "Error (Unknown): {}"
msgstr "错误(未知): {}"
-#: settings/utils/ldap.py:562
+#: settings/utils/ldap.py:565
msgid "Succeed: Match {} s user"
msgstr "成功匹配 {} 个用户"
-#: settings/utils/ldap.py:595
+#: settings/utils/ldap.py:598
msgid "Authentication failed (configuration incorrect): {}"
msgstr "认证失败(配置错误): {}"
-#: settings/utils/ldap.py:597
+#: settings/utils/ldap.py:600
msgid "Authentication failed (before login check failed): {}"
msgstr "认证失败(登录前检查失败): {}"
-#: settings/utils/ldap.py:599
+#: settings/utils/ldap.py:602
msgid "Authentication failed (username or password incorrect): {}"
msgstr "认证失败 (用户名或密码不正确): {}"
-#: settings/utils/ldap.py:601
+#: settings/utils/ldap.py:604
msgid "Authentication failed (Unknown): {}"
msgstr "认证失败: (未知): {}"
-#: settings/utils/ldap.py:604
+#: settings/utils/ldap.py:607
msgid "Authentication success: {}"
msgstr "认证成功: {}"
@@ -3827,11 +3873,11 @@ msgstr ""
msgid "Send verification code"
msgstr "发送验证码"
-#: templates/_mfa_login_field.html:105
+#: templates/_mfa_login_field.html:106
msgid "Wait: "
msgstr "等待:"
-#: templates/_mfa_login_field.html:115
+#: templates/_mfa_login_field.html:116
msgid "The verification code has been sent"
msgstr "验证码已发送"
@@ -4164,19 +4210,19 @@ msgstr "macOS 需要下载客户端来连接 RDP 资产,Windows 系统默认
msgid "Filters"
msgstr "过滤"
-#: terminal/api/session.py:185
+#: terminal/api/session.py:189
msgid "Session does not exist: {}"
msgstr "会话不存在: {}"
-#: terminal/api/session.py:188
+#: terminal/api/session.py:192
msgid "Session is finished or the protocol not supported"
msgstr "会话已经完成或协议不支持"
-#: terminal/api/session.py:193
+#: terminal/api/session.py:197
msgid "User does not exist: {}"
msgstr "用户不存在: {}"
-#: terminal/api/session.py:197
+#: terminal/api/session.py:201
msgid "User does not have permission"
msgstr "用户没有权限"
@@ -4285,15 +4331,15 @@ msgstr "不支持批量创建"
msgid "Storage is invalid"
msgstr "存储无效"
-#: terminal/models/session.py:44 terminal/models/sharing.py:81
+#: terminal/models/session.py:45 terminal/models/sharing.py:81
msgid "Login from"
msgstr "登录来源"
-#: terminal/models/session.py:48
+#: terminal/models/session.py:49
msgid "Replay"
msgstr "回放"
-#: terminal/models/session.py:53
+#: terminal/models/session.py:54
msgid "Date end"
msgstr "结束日期"
@@ -4334,7 +4380,7 @@ msgid "Date left"
msgstr "结束日期"
#: terminal/models/sharing.py:91
-#: xpack/plugins/change_auth_plan/models/base.py:178
+#: xpack/plugins/change_auth_plan/models/base.py:189
msgid "Finished"
msgstr "结束"
@@ -4462,9 +4508,9 @@ msgstr "桶名称"
msgid "Access key"
msgstr "Access key"
-#: terminal/serializers/storage.py:34
+#: terminal/serializers/storage.py:34 users/models/user.py:582
msgid "Secret key"
-msgstr "Secret key"
+msgstr "密钥"
#: terminal/serializers/storage.py:39 terminal/serializers/storage.py:51
#: terminal/serializers/storage.py:81 terminal/serializers/storage.py:91
@@ -4591,46 +4637,50 @@ msgstr "自定义用户"
msgid "Ticket already closed"
msgstr "工单已经关闭"
-#: tickets/handler/apply_application.py:52
+#: tickets/handler/apply_application.py:53
msgid "Applied category"
msgstr "申请的类别"
-#: tickets/handler/apply_application.py:53
+#: tickets/handler/apply_application.py:54
msgid "Applied type"
msgstr "申请的类型"
-#: tickets/handler/apply_application.py:54
+#: tickets/handler/apply_application.py:55
msgid "Applied application group"
msgstr "申请的应用组"
-#: tickets/handler/apply_application.py:55 tickets/handler/apply_asset.py:48
+#: tickets/handler/apply_application.py:56 tickets/handler/apply_asset.py:52
msgid "Applied system user group"
msgstr "申请的系统用户组"
-#: tickets/handler/apply_application.py:56 tickets/handler/apply_asset.py:50
+#: tickets/handler/apply_application.py:57 tickets/handler/apply_asset.py:54
msgid "Applied date start"
msgstr "申请的开始日期"
-#: tickets/handler/apply_application.py:57 tickets/handler/apply_asset.py:51
+#: tickets/handler/apply_application.py:58 tickets/handler/apply_asset.py:55
msgid "Applied date expired"
msgstr "申请的失效日期"
-#: tickets/handler/apply_application.py:79
+#: tickets/handler/apply_application.py:80
msgid ""
"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket "
"processor: {}, ticket ID: {}"
msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}"
-#: tickets/handler/apply_asset.py:47
-msgid "Applied hostname group"
-msgstr "申请的主机名组"
+#: tickets/handler/apply_asset.py:50
+msgid "Applied node group"
+msgstr "申请的节点名称组"
-#: tickets/handler/apply_asset.py:49
+#: tickets/handler/apply_asset.py:51
+msgid "Applied hostname group"
+msgstr "申请的主机名称组"
+
+#: tickets/handler/apply_asset.py:53
msgid "Applied actions"
msgstr "申请的动作"
-#: tickets/handler/apply_asset.py:72
+#: tickets/handler/apply_asset.py:77
msgid ""
"Created by the ticket ticket title: {} ticket applicant: {} ticket "
"processor: {} ticket ID: {}"
@@ -4802,54 +4852,60 @@ msgstr "你的工单已被处理, 处理人 - {}"
msgid "Ticket has processed - {} ({})"
msgstr "你的工单已被处理, 处理人 - {} ({})"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:20
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:18
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:18
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:19
msgid "Apply name"
msgstr "应用名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:39
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:37
msgid "Apply applications"
msgstr "申请应用"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:44
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:42
msgid "Apply applications display"
msgstr "应用名称名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:48
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:46
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:41
msgid "Apply system users"
-msgstr "系统用户"
+msgstr "申请的系统用户"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:53
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:51
msgid "Apply system user display"
-msgstr "批准的系统用户名称"
+msgstr "申请的系统用户名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:73
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:63
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:71
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:73
#: tickets/serializers/ticket/ticket.py:127
msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:90
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:71
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:88
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:87
msgid "The expiration date should be greater than the start date"
msgstr "过期时间要大于开始时间"
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:22
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:14
+msgid "Select at least one asset or node"
+msgstr "资产或者节点至少选择一项"
+
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:23
+msgid "Apply nodes"
+msgstr "申请节点"
+
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:27
+msgid "Apply nodes display"
+msgstr "申请的节点名称"
+
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:32
msgid "Apply assets"
msgstr "申请资产"
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:26
-msgid "Approve assets display"
-msgstr "批准的资产名称"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:31
-msgid "Approve system users"
-msgstr "批准的系统用户"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:35
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:36
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:45
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:53
msgid "Apply assets display"
-msgstr "批准的资产名称"
+msgstr "申请的资产名称"
#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:12
msgid "Run user"
@@ -5017,12 +5073,12 @@ msgstr "复制你的公钥到这里"
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
-#: users/forms/profile.py:149 users/serializers/profile.py:74
-#: users/serializers/profile.py:150 users/serializers/profile.py:163
+#: users/forms/profile.py:149 users/serializers/profile.py:95
+#: users/serializers/profile.py:171 users/serializers/profile.py:184
msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法"
-#: users/forms/profile.py:160 users/models/user.py:577
+#: users/forms/profile.py:160 users/models/user.py:579
#: users/templates/users/user_password_update.html:48
msgid "Public key"
msgstr "SSH公钥"
@@ -5035,35 +5091,35 @@ msgstr "强制启用"
msgid "Local"
msgstr "数据库"
-#: users/models/user.py:560
+#: users/models/user.py:562
msgid "Avatar"
msgstr "头像"
-#: users/models/user.py:563
+#: users/models/user.py:565
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:574
+#: users/models/user.py:576
msgid "Private key"
msgstr "ssh私钥"
-#: users/models/user.py:593
+#: users/models/user.py:598
msgid "Source"
msgstr "来源"
-#: users/models/user.py:597
+#: users/models/user.py:602
msgid "Date password last updated"
msgstr "最后更新密码日期"
-#: users/models/user.py:600
+#: users/models/user.py:605
msgid "Need update password"
msgstr "需要更新密码"
-#: users/models/user.py:759
+#: users/models/user.py:764
msgid "Administrator"
msgstr "管理员"
-#: users/models/user.py:762
+#: users/models/user.py:767
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
@@ -5110,17 +5166,17 @@ msgstr "密码不满足安全规则"
msgid "The new password cannot be the last {} passwords"
msgstr "新密码不能是最近 {} 次的密码"
-#: users/serializers/profile.py:46
+#: users/serializers/profile.py:46 users/serializers/profile.py:66
msgid "The newly set password is inconsistent"
msgstr "两次密码不一致"
-#: users/serializers/profile.py:121 users/serializers/user.py:78
+#: users/serializers/profile.py:142 users/serializers/user.py:78
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/user.py:22
#: xpack/plugins/change_auth_plan/models/base.py:32
-#: xpack/plugins/change_auth_plan/serializers/base.py:24
+#: xpack/plugins/change_auth_plan/serializers/base.py:22
msgid "Password strategy"
msgstr "密码策略"
@@ -5390,19 +5446,7 @@ msgstr "不包含"
#: users/templates/users/user_otp_check_password.html:6
msgid "Enable OTP"
-msgstr "启用 MFA (OTP)"
-
-#: users/templates/users/user_otp_check_password.html:10
-msgid "Please enter the password of"
-msgstr "请输入"
-
-#: users/templates/users/user_otp_check_password.html:10
-msgid "account"
-msgstr "账户"
-
-#: users/templates/users/user_otp_check_password.html:10
-msgid "to complete the binding operation"
-msgstr "的密码完成绑定操作"
+msgstr "启用 MFA(OTP)"
#: users/templates/users/user_otp_enable_bind.html:6
msgid "Bind one-time password authenticator"
@@ -5412,12 +5456,12 @@ msgstr "绑定MFA验证器"
msgid ""
"Use the MFA Authenticator application to scan the following qr code for a 6-"
"bit verification code"
-msgstr "使用MFA验证器应用扫描以下二维码,获取6位验证码"
+msgstr "使用 MFA 验证器应用扫描以下二维码,获取6位验证码"
#: users/templates/users/user_otp_enable_bind.html:22
#: users/templates/users/user_verify_mfa.html:27
msgid "Six figures"
-msgstr "6位数字"
+msgstr "6 位数字"
#: users/templates/users/user_otp_enable_install_app.html:6
msgid "Install app"
@@ -5427,7 +5471,7 @@ msgstr "安装应用"
msgid ""
"Download and install the MFA Authenticator application on your phone or "
"applet of WeChat"
-msgstr "请在手机端或微信小程序下载并安装MFA验证器应用"
+msgstr "请在手机端或微信小程序下载并安装 MFA 验证器应用"
#: users/templates/users/user_otp_enable_install_app.html:18
msgid "Android downloads"
@@ -5466,33 +5510,33 @@ msgstr "账号保护已开启,请根据提示完成以下操作"
msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
msgstr "请打开 MFA 验证器,输入 6 位动态码"
-#: users/views/profile/otp.py:80
+#: users/views/profile/otp.py:86
msgid "Already bound"
msgstr "已经绑定"
-#: users/views/profile/otp.py:81
+#: users/views/profile/otp.py:87
msgid "MFA already bound, disable first, then bound"
-msgstr "MFA (OTP) 已经绑定,请先禁用,再绑定"
+msgstr "MFA(OTP) 已经绑定,请先禁用,再绑定"
-#: users/views/profile/otp.py:108
+#: users/views/profile/otp.py:114
msgid "OTP enable success"
-msgstr "MFA (OTP) 启用成功"
+msgstr "MFA(OTP) 启用成功"
-#: users/views/profile/otp.py:109
+#: users/views/profile/otp.py:115
msgid "OTP enable success, return login page"
-msgstr "MFA (OTP) 启用成功,返回到登录页面"
+msgstr "MFA(OTP) 启用成功,返回到登录页面"
-#: users/views/profile/otp.py:151
+#: users/views/profile/otp.py:157
msgid "Disable OTP"
msgstr "禁用虚拟 MFA(OTP)"
-#: users/views/profile/otp.py:157
+#: users/views/profile/otp.py:163
msgid "OTP disable success"
-msgstr "MFA (OTP) 禁用成功"
+msgstr "MFA(OTP) 禁用成功"
-#: users/views/profile/otp.py:158
+#: users/views/profile/otp.py:164
msgid "OTP disable success, return login page"
-msgstr "MFA (OTP) 禁用成功,返回登录页面"
+msgstr "MFA(OTP) 禁用成功,返回登录页面"
#: users/views/profile/password.py:36 users/views/profile/password.py:41
msgid "Password invalid"
@@ -5547,26 +5591,32 @@ msgid "The parameter 'action' must be [{}]"
msgstr "参数 'action' 必须是 [{}]"
#: xpack/plugins/change_auth_plan/meta.py:9
-#: xpack/plugins/change_auth_plan/models/asset.py:63
-#: xpack/plugins/change_auth_plan/models/asset.py:119
+#: xpack/plugins/change_auth_plan/models/asset.py:68
+#: xpack/plugins/change_auth_plan/models/asset.py:124
msgid "Change auth plan"
msgstr "改密计划"
#: xpack/plugins/change_auth_plan/models/app.py:41
-#: xpack/plugins/change_auth_plan/models/app.py:90
+#: xpack/plugins/change_auth_plan/models/asset.py:63
+#: xpack/plugins/change_auth_plan/serializers/base.py:44
+msgid "Recipient"
+msgstr "收件人"
+
+#: xpack/plugins/change_auth_plan/models/app.py:46
+#: xpack/plugins/change_auth_plan/models/app.py:95
msgid "Application change auth plan"
msgstr "应用改密计划执行"
-#: xpack/plugins/change_auth_plan/models/app.py:94
-#: xpack/plugins/change_auth_plan/models/app.py:146
+#: xpack/plugins/change_auth_plan/models/app.py:99
+#: xpack/plugins/change_auth_plan/models/app.py:151
msgid "Application change auth plan execution"
msgstr "应用改密计划执行"
-#: xpack/plugins/change_auth_plan/models/app.py:139
+#: xpack/plugins/change_auth_plan/models/app.py:144
msgid "App"
msgstr "应用"
-#: xpack/plugins/change_auth_plan/models/app.py:151
+#: xpack/plugins/change_auth_plan/models/app.py:156
msgid "Application change auth plan task"
msgstr "用用改密计划任务"
@@ -5587,12 +5637,12 @@ msgstr "替换 (由 JumpServer 生成的密钥)"
msgid "SSH Key strategy"
msgstr "SSH 密钥策略"
-#: xpack/plugins/change_auth_plan/models/asset.py:130
-#: xpack/plugins/change_auth_plan/models/asset.py:206
+#: xpack/plugins/change_auth_plan/models/asset.py:135
+#: xpack/plugins/change_auth_plan/models/asset.py:211
msgid "Change auth plan execution"
msgstr "改密计划执行"
-#: xpack/plugins/change_auth_plan/models/asset.py:213
+#: xpack/plugins/change_auth_plan/models/asset.py:218
msgid "Change auth plan task"
msgstr "改密计划任务"
@@ -5612,47 +5662,66 @@ msgstr "使用不同的随机密码"
msgid "Password rules"
msgstr "密码规则"
-#: xpack/plugins/change_auth_plan/models/base.py:100
+#: xpack/plugins/change_auth_plan/models/base.py:104
msgid "Manual trigger"
msgstr "手动触发"
-#: xpack/plugins/change_auth_plan/models/base.py:101
+#: xpack/plugins/change_auth_plan/models/base.py:105
msgid "Timing trigger"
msgstr "定时触发"
-#: xpack/plugins/change_auth_plan/models/base.py:111
+#: xpack/plugins/change_auth_plan/models/base.py:115
msgid "Change auth plan snapshot"
msgstr "改密计划快照"
-#: xpack/plugins/change_auth_plan/models/base.py:118
-#: xpack/plugins/change_auth_plan/serializers/base.py:70
+#: xpack/plugins/change_auth_plan/models/base.py:122
+#: xpack/plugins/change_auth_plan/serializers/base.py:73
msgid "Trigger mode"
msgstr "触发模式"
-#: xpack/plugins/change_auth_plan/models/base.py:173
+#: xpack/plugins/change_auth_plan/models/base.py:184
msgid "Ready"
msgstr "准备"
-#: xpack/plugins/change_auth_plan/models/base.py:174
+#: xpack/plugins/change_auth_plan/models/base.py:185
msgid "Preflight check"
msgstr "改密前的校验"
-#: xpack/plugins/change_auth_plan/models/base.py:175
+#: xpack/plugins/change_auth_plan/models/base.py:186
msgid "Change auth"
msgstr "执行改密"
-#: xpack/plugins/change_auth_plan/models/base.py:176
+#: xpack/plugins/change_auth_plan/models/base.py:187
msgid "Verify auth"
msgstr "验证密码/密钥"
-#: xpack/plugins/change_auth_plan/models/base.py:177
+#: xpack/plugins/change_auth_plan/models/base.py:188
msgid "Keep auth"
msgstr "保存密码/密钥"
-#: xpack/plugins/change_auth_plan/models/base.py:185
+#: xpack/plugins/change_auth_plan/models/base.py:196
msgid "Step"
msgstr "步骤"
+#: xpack/plugins/change_auth_plan/notifications.py:8
+msgid "Notification of implementation result of encryption change plan"
+msgstr "改密计划任务结果通知"
+
+#: xpack/plugins/change_auth_plan/notifications.py:18
+msgid ""
+"{} - The encryption change task has been completed. See the attachment for "
+"details"
+msgstr "{} - 改密任务已完成, 详情见附件"
+
+#: xpack/plugins/change_auth_plan/notifications.py:19
+msgid ""
+"{} - The encryption change task has been completed: the encryption password "
+"has not been set - please go to personal information -> file encryption "
+"password to set the encryption password"
+msgstr ""
+"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加"
+"密密码"
+
#: xpack/plugins/change_auth_plan/serializers/asset.py:31
msgid "Change Password"
msgstr "更改密码"
@@ -5665,11 +5734,15 @@ msgstr "修改 SSH Key"
msgid "Run times"
msgstr "执行次数"
-#: xpack/plugins/change_auth_plan/serializers/base.py:54
+#: xpack/plugins/change_auth_plan/serializers/base.py:45
+msgid "Currently only mail sending is supported"
+msgstr "当前只支持邮件发送"
+
+#: xpack/plugins/change_auth_plan/serializers/base.py:57
msgid "* Please enter the correct password length"
msgstr "* 请输入正确的密码长度"
-#: xpack/plugins/change_auth_plan/serializers/base.py:57
+#: xpack/plugins/change_auth_plan/serializers/base.py:60
msgid "* Password length range 6-30 bits"
msgstr "* 密码长度范围 6-30 位"
@@ -5738,34 +5811,38 @@ msgid "Qingyun Private Cloud"
msgstr "青云私有云"
#: xpack/plugins/cloud/const.py:19
+msgid "OpenStack Cloud"
+msgstr "OpenStack Cloud"
+
+#: xpack/plugins/cloud/const.py:20
msgid "Google Cloud Platform"
msgstr "谷歌云"
-#: xpack/plugins/cloud/const.py:23
+#: xpack/plugins/cloud/const.py:24
msgid "Instance name"
msgstr "实例名称"
-#: xpack/plugins/cloud/const.py:24
+#: xpack/plugins/cloud/const.py:25
msgid "Instance name and Partial IP"
msgstr "实例名称和部分IP"
-#: xpack/plugins/cloud/const.py:29
+#: xpack/plugins/cloud/const.py:30
msgid "Succeed"
msgstr "成功"
-#: xpack/plugins/cloud/const.py:33
+#: xpack/plugins/cloud/const.py:34
msgid "Unsync"
msgstr "未同步"
-#: xpack/plugins/cloud/const.py:34
+#: xpack/plugins/cloud/const.py:35
msgid "New Sync"
msgstr "新同步"
-#: xpack/plugins/cloud/const.py:35
+#: xpack/plugins/cloud/const.py:36
msgid "Synced"
msgstr "已同步"
-#: xpack/plugins/cloud/const.py:36
+#: xpack/plugins/cloud/const.py:37
msgid "Released"
msgstr "已释放"
@@ -5789,11 +5866,11 @@ msgstr "地域"
msgid "Hostname strategy"
msgstr "主机名策略"
-#: xpack/plugins/cloud/models.py:97 xpack/plugins/cloud/serializers/task.py:66
+#: xpack/plugins/cloud/models.py:97 xpack/plugins/cloud/serializers/task.py:67
msgid "Unix admin user"
msgstr "Unix 管理员"
-#: xpack/plugins/cloud/models.py:101 xpack/plugins/cloud/serializers/task.py:67
+#: xpack/plugins/cloud/models.py:101 xpack/plugins/cloud/serializers/task.py:68
msgid "Windows admin user"
msgstr "Windows 管理员"
@@ -5801,7 +5878,7 @@ msgstr "Windows 管理员"
msgid "IP network segment group"
msgstr "IP网段组"
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:70
+#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:71
msgid "Always update"
msgstr "总是更新"
@@ -5997,28 +6074,40 @@ msgstr ""
msgid "AccessKey Secret"
msgstr ""
-#: xpack/plugins/cloud/serializers/account_attrs.py:23
+#: xpack/plugins/cloud/serializers/account_attrs.py:31
msgid "Client ID"
msgstr "客户端 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:29
+#: xpack/plugins/cloud/serializers/account_attrs.py:37
msgid "Tenant ID"
msgstr "租户 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:32
+#: xpack/plugins/cloud/serializers/account_attrs.py:40
msgid "Subscription ID"
msgstr "订阅 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:87
-#: xpack/plugins/cloud/serializers/account_attrs.py:92
+#: xpack/plugins/cloud/serializers/account_attrs.py:91
+#: xpack/plugins/cloud/serializers/account_attrs.py:96
msgid "API Endpoint"
msgstr "API 端点"
-#: xpack/plugins/cloud/serializers/account_attrs.py:98
+#: xpack/plugins/cloud/serializers/account_attrs.py:102
+msgid "Auth url"
+msgstr "认证地址"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:103
+msgid "eg: http://openstack.example.com:5000/v3"
+msgstr ""
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:106
+msgid "User domain"
+msgstr "用户域"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:113
msgid "Service account key"
msgstr "账户密钥"
-#: xpack/plugins/cloud/serializers/account_attrs.py:99
+#: xpack/plugins/cloud/serializers/account_attrs.py:114
msgid "The file is in JSON format"
msgstr "JSON 格式的文件"
@@ -6039,6 +6128,10 @@ msgstr "执行次数"
msgid "Instance count"
msgstr "实例个数"
+#: xpack/plugins/cloud/serializers/task.py:65
+msgid "Linux admin user"
+msgstr "Linux 管理员"
+
#: xpack/plugins/cloud/utils.py:68
msgid "Account unavailable"
msgstr "账户无效"
@@ -6127,200 +6220,11 @@ msgstr "旗舰版"
msgid "Community edition"
msgstr "社区版"
-#~ msgid "No upload or download permission"
-#~ msgstr "没有上传下载权限"
+#~ msgid "Please enter the password of"
+#~ msgstr "请输入"
-#~ msgid "OTP not set, please set it first"
-#~ msgstr "OTP认证没有设置,请先完成设置"
+#~ msgid "account"
+#~ msgstr "账户"
-#~ msgid "Radius MFA"
-#~ msgstr "Radius MFA"
-
-#~ msgid "Help Website URL"
-#~ msgstr "官网链接"
-
-#~ msgid "default: http://www.jumpserver.org"
-#~ msgstr "如: http://dev.jumpserver.org:8080"
-
-#~ msgid "One-time password invalid, or ntp sync server time"
-#~ msgstr "MFA 验证码不正确,或者服务器端时间不对"
-
-#~ msgid "Download MFA APP, Using dynamic code"
-#~ msgstr "下载 MFA APP, 使用一次性动态码"
-
-#~ msgid "MFA Radius"
-#~ msgstr "Radius MFA"
-
-#~ msgid "Please enter verification code"
-#~ msgstr "请输入验证码"
-
-#, python-brace-format
-#~ msgid ""
-#~ "One-time password invalid, or ntp sync server time, You can also try "
-#~ "{times_try} times (The account will be temporarily locked for "
-#~ "{block_time} minutes)"
-#~ msgstr ""
-#~ "虚拟MFA 不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将"
-#~ "被临时 锁定 {block_time} 分钟)"
-
-#, python-brace-format
-#~ msgid ""
-#~ "The MFA type({mfa_type}) is not supported, You can also try {times_try} "
-#~ "times (The account will be temporarily locked for {block_time} minutes)"
-#~ msgstr ""
-#~ "该({mfa_type}) MFA 类型不支持, 您还可以尝试 {times_try} 次(账号将被临时 "
-#~ "锁定 {block_time} 分钟)"
-
-#~ msgid "One-time password"
-#~ msgstr "一次性密码"
-
-#~ msgid "Go"
-#~ msgstr "立即"
-
-#, python-brace-format
-#~ msgid "Hello {name}"
-#~ msgstr "你好 {name}"
-
-#~ msgid "Login direct"
-#~ msgstr "直接登录"
-
-#~ msgid "to apply for a password reset email."
-#~ msgstr "申请重置"
-
-#~ msgid "Please login and reset your MFA."
-#~ msgstr "请登录并重新设置你的 MFA"
-
-#~ msgid "Please login and reset your ssh public key."
-#~ msgstr "请登录并重新设置你的密钥"
-
-# msgid "Update user"
-# msgstr "更新用户"
-#, python-format
-#~ msgid ""
-#~ "\n"
-#~ " \n"
-#~ "
Your account has been created successfully
\n"
-#~ "
\n"
-#~ "
\n"
-#~ "
\n"
-#~ " "
-#~ msgstr ""
-#~ "\n"
-#~ " \n"
-#~ "
您的账户已创建成功
\n"
-#~ "
\n"
-#~ " 用户名: %(username)s\n"
-#~ "
\n"
-#~ " 密码:
请点击这里设置密码 (这个链接有效期1小时, 超"
-#~ "过时间您可以
重新申请"
-#~ ")\n"
-#~ "
\n"
-#~ "
\n"
-#~ "
---
\n"
-#~ "
直接登录\n"
-#~ "
\n"
-#~ "
\n"
-#~ " "
-
-#, python-format
-#~ msgid "Hello %(name)s"
-#~ msgstr "你好 %(name)s"
-
-#~ msgid "This link is valid for 1 hour. After it expires,"
-#~ msgstr "这个链接有效期1小时, 超过时间您可以"
-
-#~ msgid "Welcome to the JumpServer open source Bastion Host"
-#~ msgstr "欢迎使用JumpServer开源堡垒机"
-
-#~ msgid "Login IP"
-#~ msgstr "登录IP"
-
-#~ msgid "The user `{}` is not in the current organization: `{}`"
-#~ msgstr "用户 `{}` 不在当前组织: `{}`"
-
-#~ msgid "Login Confirm"
-#~ msgstr "登录复核"
-
-#~ msgid "{} need confirm by {}"
-#~ msgstr "{} 需要 {} 复核"
-
-#~ msgid "Enabled, please go to the user detail add approver"
-#~ msgstr "启用后, 请在用户详情中添加审批人"
-
-#~ msgid ""
-#~ "\n"
-#~ "Time: {}"
-#~ msgstr ""
-#~ "\n"
-#~ "时间:{}"
-
-#~ msgid "asset permission"
-#~ msgstr "资产授权"
-
-#~ msgid "Asset permissions will expired"
-#~ msgstr "资产授权即将过期"
-
-#, python-brace-format
-#~ msgid ""
-#~ "\n"
-#~ "Organization: {org}\n"
-#~ "Permissions: {perms}\n"
-#~ msgstr ""
-#~ "\n"
-#~ "组织: {org}\n"
-#~ "权限: {perms}\n"
-
-#~ msgid "asset permissions of organization"
-#~ msgstr "组织资产授权"
-
-#~ msgid "application permissions of organization"
-#~ msgstr "组织的应用授权 {}"
-
-#, python-brace-format
-#~ msgid ""
-#~ "\n"
-#~ " Organization: {org} \n"
-#~ " Permissions: {perms} \n"
-#~ msgstr ""
-#~ "\n"
-#~ "组织: {org} \n"
-#~ "授权: {perms} \n"
-
-#~ msgid "You've been hacked"
-#~ msgstr "你被攻击了"
-
-#~ msgid "Binding DingTalk failed"
-#~ msgstr "绑定钉钉失败"
-
-#~ msgid "Binding FeiShu failed"
-#~ msgstr "绑定飞书失败"
-
-#~ msgid "Binding WeCom failed"
-#~ msgstr "绑定企业微信失败"
-
-#~ msgid "Enable Login MFA"
-#~ msgstr "启用登录MFA"
-
-#~ msgid "Enable login password add-on"
-#~ msgstr "启用登录密码附加码"
-
-#~ msgid "OpenID"
-#~ msgstr "OpenID"
-
-#~ msgid "CAS"
-#~ msgstr "CAS"
-
-#~ msgid "Only "
-#~ msgstr "仅能从用户配置来源登录"
+#~ msgid "to complete the binding operation"
+#~ msgstr "的密码完成绑定操作"
diff --git a/apps/notifications/models/site_msg.py b/apps/notifications/models/site_msg.py
index 556c00607..3e3c09baa 100644
--- a/apps/notifications/models/site_msg.py
+++ b/apps/notifications/models/site_msg.py
@@ -1,5 +1,4 @@
from django.db import models
-from django.utils.translation import gettext_lazy as _
from common.db.models import JMSModel
diff --git a/apps/notifications/signals_handler.py b/apps/notifications/signals_handler.py
index 019d2a3da..79964464a 100644
--- a/apps/notifications/signals_handler.py
+++ b/apps/notifications/signals_handler.py
@@ -46,7 +46,6 @@ def on_site_message_create(sender, instance, created, **kwargs):
'message': instance.message,
'users': user_ids
}
- data = json.dumps(data)
new_site_msg_chan.publish(data)
diff --git a/apps/notifications/ws.py b/apps/notifications/ws.py
index 423639356..d1032b237 100644
--- a/apps/notifications/ws.py
+++ b/apps/notifications/ws.py
@@ -1,10 +1,9 @@
import threading
import json
-from redis.exceptions import ConnectionError
from channels.generic.websocket import JsonWebsocketConsumer
-from common.db.utils import close_old_connections
from common.utils import get_logger
+from common.db.utils import safe_db_connection
from .site_msg import SiteMessageUtil
from .signals_handler import new_site_msg_chan
@@ -13,15 +12,13 @@ logger = get_logger(__name__)
class SiteMsgWebsocket(JsonWebsocketConsumer):
refresh_every_seconds = 10
- chan = None
def connect(self):
user = self.scope["user"]
if user.is_authenticated:
self.accept()
- self.chan = new_site_msg_chan.subscribe()
- thread = threading.Thread(target=self.unread_site_msg_count)
+ thread = threading.Thread(target=self.watch_recv_new_site_msg)
thread.start()
else:
self.close()
@@ -45,45 +42,18 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
logger.debug('Send unread count to user: {} {}'.format(user_id, unread_count))
self.send_json({'type': 'unread_count', 'unread_count': unread_count})
- def unread_site_msg_count(self):
+ def watch_recv_new_site_msg(self):
+ ws = self
user_id = str(self.scope["user"].id)
- self.send_unread_msg_count()
-
- try:
- msgs = self.chan.listen()
- # 开始之前关闭连接,因为server端可能关闭了连接,而 client 还在 CONN_MAX_AGE 中
- close_old_connections()
- for message in msgs:
- if message['type'] != 'message':
- continue
-
- try:
- msg = json.loads(message['data'].decode())
- except json.JSONDecoder as e:
- logger.debug('Decode json error: ', e)
- continue
- if not msg:
- continue
-
- logger.debug('New site msg recv, may be mine: {}'.format(msg))
- users = msg.get('users', [])
- logger.debug('Message users: {}'.format(users))
- if user_id in users:
- self.send_unread_msg_count()
- except ConnectionError:
- logger.error('Redis chan closed')
- finally:
- logger.info('Notification ws thread end')
- close_old_connections()
-
- def disconnect(self, close_code):
- try:
- if self.chan is not None:
- self.chan.close()
- self.close()
- finally:
- close_old_connections()
- logger.info('Notification websocket disconnect')
+ # 先发一个消息再说
+ with safe_db_connection():
+ self.send_unread_msg_count()
+ def handle_new_site_msg_recv(msg):
+ users = msg.get('users', [])
+ logger.debug('New site msg recv, message users: {}'.format(users))
+ if user_id in users:
+ ws.send_unread_msg_count()
+ new_site_msg_chan.keep_handle_msg(handle_new_site_msg_recv)
diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py
index 72715885b..ff22172b6 100644
--- a/apps/orgs/signals_handler/common.py
+++ b/apps/orgs/signals_handler/common.py
@@ -6,9 +6,8 @@ from functools import partial
from django.dispatch import receiver
from django.utils.functional import LazyObject
-from common.db.utils import close_old_connections
from django.db.models.signals import m2m_changed
-from django.db.models.signals import post_save, post_delete, pre_delete
+from django.db.models.signals import post_save, pre_delete
from orgs.utils import tmp_to_org
from orgs.models import Organization, OrganizationMember
@@ -47,25 +46,9 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
logger.debug("Start subscribe for expire orgs mapping from memory")
def keep_subscribe_org_mapping():
- while True:
- try:
- subscribe = orgs_mapping_for_memory_pub_sub.subscribe()
- msgs = subscribe.listen()
- # 开始之前关闭连接,因为server端可能关闭了连接,而 client 还在 CONN_MAX_AGE 中
- close_old_connections()
- for message in msgs:
- if message['type'] != 'message':
- continue
- if message['data'] == b'error':
- raise ValueError
- Organization.expire_orgs_mapping()
- logger.debug('Expire orgs mapping: ' + str(message['data']))
- except Exception as e:
- logger.exception(f'subscribe_orgs_mapping_expire: {e}')
- Organization.expire_orgs_mapping()
- finally:
- # 结束收关闭连接
- close_old_connections()
+ orgs_mapping_for_memory_pub_sub.keep_handle_msg(
+ lambda org_id: Organization.expire_orgs_mapping()
+ )
t = threading.Thread(target=keep_subscribe_org_mapping)
t.daemon = True
diff --git a/apps/perms/filters.py b/apps/perms/filters.py
index f56927436..da82b5090 100644
--- a/apps/perms/filters.py
+++ b/apps/perms/filters.py
@@ -143,7 +143,7 @@ class AssetPermissionFilter(PermissionBaseFilter):
if not _nodes:
return queryset.none()
- node = _nodes.get()
+ node = _nodes.first()
if not is_query_all:
queryset = queryset.filter(nodes=node)
@@ -170,13 +170,13 @@ class AssetPermissionFilter(PermissionBaseFilter):
return queryset
if not assets:
return queryset.none()
- asset = assets.get()
+ assetids = list(assets.values_list('id', flat=True))
if not is_query_all:
- queryset = queryset.filter(assets=asset)
+ queryset = queryset.filter(assets__in=assetids)
return queryset
inherit_all_nodekeys = set()
- inherit_nodekeys = asset.nodes.values_list('key', flat=True)
+ inherit_nodekeys = set(assets.values_list('nodes__key', flat=True))
for key in inherit_nodekeys:
ancestor_keys = Node.get_node_ancestor_keys(key, with_self=True)
@@ -185,8 +185,8 @@ class AssetPermissionFilter(PermissionBaseFilter):
inherit_all_nodeids = Node.objects.filter(key__in=inherit_all_nodekeys).values_list('id', flat=True)
inherit_all_nodeids = list(inherit_all_nodeids)
- qs1 = queryset.filter(assets=asset).distinct()
- qs2 = queryset.filter(nodes__id__in=inherit_all_nodeids).distinct()
+ qs1 = queryset.filter(assets__in=assetids).distinct()
+ qs2 = queryset.filter(nodes__in=inherit_all_nodeids).distinct()
qs = UnionQuerySet(qs1, qs2)
return qs
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index 2047faa08..02fb9004c 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -32,6 +32,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'radius': serializers.RadiusSettingSerializer,
'cas': serializers.CASSettingSerializer,
'sso': serializers.SSOSettingSerializer,
+ 'saml2': serializers.SAML2SettingSerializer,
'clean': serializers.CleaningSerializer,
'other': serializers.OtherSettingSerializer,
'sms': serializers.SMSSettingSerializer,
diff --git a/apps/settings/models.py b/apps/settings/models.py
index 128693bb9..f92e09f34 100644
--- a/apps/settings/models.py
+++ b/apps/settings/models.py
@@ -97,6 +97,7 @@ class Setting(models.Model):
'AUTH_OPENID': [settings.AUTH_BACKEND_OIDC_CODE, settings.AUTH_BACKEND_OIDC_PASSWORD],
'AUTH_RADIUS': [settings.AUTH_BACKEND_RADIUS],
'AUTH_CAS': [settings.AUTH_BACKEND_CAS],
+ 'AUTH_SAML2': [settings.AUTH_BACKEND_SAML2],
}
setting_backends = backends_map[name]
auth_backends = settings.AUTHENTICATION_BACKENDS
@@ -130,6 +131,10 @@ class Setting(models.Model):
def refresh_AUTH_OPENID(cls):
cls.refresh_authentications('AUTH_OPENID')
+ @classmethod
+ def refresh_AUTH_SAML2(cls):
+ cls.refresh_authentications('AUTH_SAML2')
+
def refresh_keycloak_to_openid_if_need(self):
watch_config_names = [
'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', 'AUTH_OPENID_SERVER_URL',
diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py
index 4a2f77ebe..c675f4070 100644
--- a/apps/settings/serializers/auth/__init__.py
+++ b/apps/settings/serializers/auth/__init__.py
@@ -8,3 +8,4 @@ from .wecom import *
from .sso import *
from .base import *
from .sms import *
+from .saml2 import *
diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py
index 2873225b7..d4ea37f36 100644
--- a/apps/settings/serializers/auth/base.py
+++ b/apps/settings/serializers/auth/base.py
@@ -14,6 +14,7 @@ class AuthSettingSerializer(serializers.Serializer):
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth'))
AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom Auth'))
AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))
+ AUTH_SAML2 = serializers.BooleanField(default=False, label=_("SAML2 Auth"))
FORGOT_PASSWORD_URL = serializers.CharField(
required=False, allow_blank=True, max_length=1024,
label=_("Forgot password url")
diff --git a/apps/settings/serializers/auth/saml2.py b/apps/settings/serializers/auth/saml2.py
new file mode 100644
index 000000000..724bcf17a
--- /dev/null
+++ b/apps/settings/serializers/auth/saml2.py
@@ -0,0 +1,30 @@
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'SAML2SettingSerializer',
+]
+
+
+class SAML2SettingSerializer(serializers.Serializer):
+ AUTH_SAML2 = serializers.BooleanField(
+ default=False, required=False, label=_('Enable SAML2 Auth')
+ )
+ SAML2_IDP_METADATA_URL = serializers.URLField(
+ allow_blank=True, required=False, label=_('IDP Metadata URL')
+ )
+ SAML2_IDP_METADATA_XML = serializers.CharField(
+ allow_blank=True, required=False, label=_('IDP Metadata XML')
+ )
+ SAML2_SP_KEY_CONTENT = serializers.CharField(
+ allow_blank=True, required=False,
+ write_only=True, label=_('SP Private Key')
+ )
+ SAML2_SP_CERT_CONTENT = serializers.CharField(
+ allow_blank=True, required=False,
+ write_only=True, label=_('SP Public Cert')
+ )
+ SAML2_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))
+ SAML2_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
+ AUTH_SAML2_ALWAYS_UPDATE_USER = serializers.BooleanField(required=False, label=_('Always update user'))
diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py
index b8c9e6de6..6463cdff2 100644
--- a/apps/settings/serializers/security.py
+++ b/apps/settings/serializers/security.py
@@ -1,7 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
-from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
+from acls.serializers.rules import ip_group_help_text, ip_group_child_validator
class SecurityPasswordRuleSerializer(serializers.Serializer):
@@ -27,13 +27,10 @@ class SecurityPasswordRuleSerializer(serializers.Serializer):
)
-def ip_child_validator(ip_child):
- is_valid = is_ip_address(ip_child) \
- or is_ip_network(ip_child) \
- or is_ip_segment(ip_child)
- if not is_valid:
- error = _('IP address invalid: `{}`').format(ip_child)
- raise serializers.ValidationError(error)
+login_ip_limit_time_help_text = _(
+ 'Unit: minute, If the user has failed to log in for a limited number of times, '
+ 'no login is allowed during this time interval.'
+)
class SecurityAuthSerializer(serializers.Serializer):
@@ -47,23 +44,31 @@ class SecurityAuthSerializer(serializers.Serializer):
)
SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(
min_value=3, max_value=99999,
- label=_('Limit the number of login failures')
+ label=_('Limit the number of user login failures')
)
SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(
min_value=5, max_value=99999, required=True,
- label=_('Block logon interval'),
- help_text=_(
- 'Unit: minute, If the user has failed to log in for a limited number of times, '
- 'no login is allowed during this time interval.'
- )
+ label=_('Block user login interval'),
+ help_text=login_ip_limit_time_help_text
+ )
+ SECURITY_LOGIN_IP_LIMIT_COUNT = serializers.IntegerField(
+ min_value=3, max_value=99999,
+ label=_('Limit the number of IP login failures')
+ )
+ SECURITY_LOGIN_IP_LIMIT_TIME = serializers.IntegerField(
+ min_value=5, max_value=99999, required=True,
+ label=_('Block IP login interval'),
+ help_text=login_ip_limit_time_help_text
+ )
+ SECURITY_LOGIN_IP_WHITE_LIST = serializers.ListField(
+ default=[], label=_('Login IP White List'), allow_empty=True,
+ child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]),
+ help_text=ip_group_help_text
)
SECURITY_LOGIN_IP_BLACK_LIST = serializers.ListField(
default=[], label=_('Login IP Black List'), allow_empty=True,
- child=serializers.CharField(max_length=1024, validators=[ip_child_validator]),
- help_text=_(
- 'Format for comma-delimited string. Such as: '
- '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64'
- )
+ child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]),
+ help_text=ip_group_help_text
)
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
min_value=1, max_value=99999, required=True,
diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py
index 483b26ee2..241574995 100644
--- a/apps/settings/signals_handler.py
+++ b/apps/settings/signals_handler.py
@@ -11,7 +11,6 @@ from jumpserver.utils import current_request
from common.decorator import on_transaction_commit
from common.utils import get_logger, ssh_key_gen
from common.utils.connection import RedisPubSub
-from common.db.utils import close_old_connections
from common.signals import django_ready
from .models import Setting
@@ -81,23 +80,9 @@ def subscribe_settings_change(sender, **kwargs):
logger.debug("Start subscribe setting change")
def keep_subscribe_settings_change():
- while True:
- try:
- sub = setting_pub_sub.subscribe()
- msgs = sub.listen()
- # 开始之前关闭连接,因为server端可能关闭了连接,而 client 还在 CONN_MAX_AGE 中
- close_old_connections()
- for msg in msgs:
- if msg["type"] != "message":
- continue
- item = msg['data'].decode()
- logger.debug("Found setting change: {}".format(str(item)))
- Setting.refresh_item(item)
- except Exception as e:
- logger.exception(f'subscribe_settings_change: {e}')
- Setting.refresh_all_settings()
- finally:
- close_old_connections()
+ setting_pub_sub.keep_handle_msg(
+ lambda name: Setting.refresh_item(name)
+ )
t = threading.Thread(target=keep_subscribe_settings_change)
t.daemon = True
diff --git a/apps/static/css/otp.css b/apps/static/css/otp.css
index 4c9ed2606..f78916ac1 100644
--- a/apps/static/css/otp.css
+++ b/apps/static/css/otp.css
@@ -136,6 +136,10 @@ article ul li:last-child{
border-radius: 6px;
color: white;
}
+
+.next:hover {
+ color: white;
+}
/*绑定TOTP*/
/*版权信息*/
diff --git a/apps/templates/_footer.html b/apps/templates/_footer.html
index 21ed61e77..3c05f9eb1 100644
--- a/apps/templates/_footer.html
+++ b/apps/templates/_footer.html
@@ -1,7 +1,7 @@
{% load i18n %}