diff --git a/apps/applications/const.py b/apps/applications/const.py index 313477c25..0bf130f6f 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -27,6 +27,7 @@ class AppType(models.TextChoices): sqlserver = 'sqlserver', 'SQLServer' redis = 'redis', 'Redis' mongodb = 'mongodb', 'MongoDB' + clickhouse = 'clickhouse', 'ClickHouse' # remote-app category chrome = 'chrome', 'Chrome' @@ -42,7 +43,7 @@ class AppType(models.TextChoices): return { AppCategory.db: [ cls.mysql, cls.mariadb, cls.oracle, cls.pgsql, - cls.sqlserver, cls.redis, cls.mongodb + cls.sqlserver, cls.redis, cls.mongodb, cls.clickhouse ], AppCategory.remote_app: [ cls.chrome, cls.mysql_workbench, @@ -82,4 +83,4 @@ class AppType(models.TextChoices): if AppCategory.is_xpack(category): return True - return tp in ['oracle', 'postgresql', 'sqlserver'] + return tp in ['oracle', 'postgresql', 'sqlserver', 'clickhouse'] diff --git a/apps/applications/migrations/0024_alter_application_type.py b/apps/applications/migrations/0024_alter_application_type.py new file mode 100644 index 000000000..4f26e0905 --- /dev/null +++ b/apps/applications/migrations/0024_alter_application_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-04 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0023_auto_20220715_1556'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='type', + field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('clickhouse', 'ClickHouse'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/applications/serializers/attrs/application_type/__init__.py b/apps/applications/serializers/attrs/application_type/__init__.py index 603dd89a4..cebf0027c 100644 --- a/apps/applications/serializers/attrs/application_type/__init__.py +++ b/apps/applications/serializers/attrs/application_type/__init__.py @@ -6,6 +6,7 @@ from .pgsql import * from .sqlserver import * from .redis import * from .mongodb import * +from .clickhouse import * from .chrome import * from .mysql_workbench import * diff --git a/apps/applications/serializers/attrs/application_type/clickhouse.py b/apps/applications/serializers/attrs/application_type/clickhouse.py new file mode 100644 index 000000000..5593f0fb1 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/clickhouse.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from ..application_category import DBSerializer + +__all__ = ['ClickHouseSerializer'] + + +class ClickHouseSerializer(DBSerializer): + port = serializers.IntegerField(default=9000, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/attrs.py b/apps/applications/serializers/attrs/attrs.py index 1f3fb7944..ab49cd7f2 100644 --- a/apps/applications/serializers/attrs/attrs.py +++ b/apps/applications/serializers/attrs/attrs.py @@ -31,6 +31,7 @@ type_serializer_classes_mapping = { const.AppType.sqlserver.value: application_type.SQLServerSerializer, const.AppType.redis.value: application_type.RedisSerializer, const.AppType.mongodb.value: application_type.MongoDBSerializer, + const.AppType.clickhouse.value: application_type.ClickHouseSerializer, # cloud const.AppType.k8s.value: application_type.K8SSerializer } diff --git a/apps/assets/migrations/0022_auto_20181012_1717.py b/apps/assets/migrations/0022_auto_20181012_1717.py index db470c599..8849249ea 100644 --- a/apps/assets/migrations/0022_auto_20181012_1717.py +++ b/apps/assets/migrations/0022_auto_20181012_1717.py @@ -21,8 +21,8 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=64, verbose_name='Name')), ('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)), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')), ], options={ diff --git a/apps/assets/migrations/0084_auto_20220112_1959.py b/apps/assets/migrations/0084_auto_20220112_1959.py index 23183e71d..5385980e9 100644 --- a/apps/assets/migrations/0084_auto_20220112_1959.py +++ b/apps/assets/migrations/0084_auto_20220112_1959.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): fields=[ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False)), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), diff --git a/apps/assets/migrations/0093_alter_systemuser_protocol.py b/apps/assets/migrations/0093_alter_systemuser_protocol.py new file mode 100644 index 000000000..963fe1fd9 --- /dev/null +++ b/apps/assets/migrations/0093_alter_systemuser_protocol.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-04 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0092_commandfilter_nodes'), + ] + + operations = [ + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('clickhouse', 'ClickHouse'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 7f44094e0..cec03827e 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -50,8 +50,8 @@ class CommandFilter(OrgModelMixin): ) 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) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) created_by = models.CharField( max_length=128, blank=True, default='', verbose_name=_('Created by') ) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index e20664071..4c7454594 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -34,6 +34,7 @@ class ProtocolMixin: sqlserver = 'sqlserver', 'SQLServer' redis = 'redis', 'Redis' mongodb = 'mongodb', 'MongoDB' + clickhouse = 'clickhouse', 'ClickHouse' k8s = 'k8s', 'K8S' SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp] @@ -46,7 +47,7 @@ class ProtocolMixin: ] APPLICATION_CATEGORY_DB_PROTOCOLS = [ Protocol.mysql, Protocol.mariadb, Protocol.oracle, - Protocol.postgresql, Protocol.sqlserver, + Protocol.postgresql, Protocol.sqlserver, Protocol.clickhouse, Protocol.redis, Protocol.mongodb ] APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ diff --git a/apps/audits/api.py b/apps/audits/api.py index ff1bb0309..4cd73f12b 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -1,21 +1,29 @@ # -*- coding: utf-8 -*- # -from rest_framework.mixins import ListModelMixin, CreateModelMixin +from importlib import import_module + +from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin from django.db.models import F, Value from django.db.models.functions import Concat +from django.conf import settings from rest_framework.permissions import IsAuthenticated from rest_framework import generics from common.drf.api import JMSReadOnlyModelViewSet +from common.plugins.es import QuerySet as ESQuerySet from common.drf.filters import DatetimeRangeFilter from common.api import CommonGenericViewSet from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin from orgs.utils import current_org from ops.models import CommandExecution from . import filters +from .backends import TYPE_ENGINE_MAPPING from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer -from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer +from .serializers import ( + OperateLogSerializer, OperateLogActionDetailSerializer, + PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer +) class FTPLogViewSet(CreateModelMixin, @@ -68,7 +76,7 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView): return qs -class OperateLogViewSet(ListModelMixin, OrgGenericViewSet): +class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): model = OperateLog serializer_class = OperateLogSerializer extra_filter_backends = [DatetimeRangeFilter] @@ -79,6 +87,22 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet): search_fields = ['resource'] ordering = ['-datetime'] + def get_serializer_class(self): + if self.request.query_params.get('type') == 'action_detail': + return OperateLogActionDetailSerializer + return super().get_serializer_class() + + def get_queryset(self): + qs = OperateLog.objects.all() + es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG + if es_config: + engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) + store = engine_mod.OperateLogStore(es_config) + if store.ping(timeout=2): + qs = ESQuerySet(store) + qs.model = OperateLog + return qs + class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): queryset = PasswordChangeLog.objects.all() diff --git a/apps/audits/backends/__init__.py b/apps/audits/backends/__init__.py new file mode 100644 index 000000000..244e2c0f3 --- /dev/null +++ b/apps/audits/backends/__init__.py @@ -0,0 +1,18 @@ +from importlib import import_module + +from django.conf import settings + + +TYPE_ENGINE_MAPPING = { + 'db': 'audits.backends.db', + 'es': 'audits.backends.es', +} + + +def get_operate_log_storage(default=False): + engine_mod = import_module(TYPE_ENGINE_MAPPING['db']) + es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG + if not default and es_config: + engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) + storage = engine_mod.OperateLogStore(es_config) + return storage diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py new file mode 100644 index 000000000..14222eb13 --- /dev/null +++ b/apps/audits/backends/db.py @@ -0,0 +1,38 @@ +# ~*~ coding: utf-8 ~*~ +from django.utils.translation import ugettext_lazy as _ + +from audits.models import OperateLog + + +class OperateLogStore(object): + def __init__(self, config): + self.model = OperateLog + self.max_length = 1024 + self.max_length_tip_msg = _( + 'The text content is too long. Use Elasticsearch to store operation logs' + ) + + @staticmethod + def ping(timeout=None): + return True + + def save(self, **kwargs): + log_id = kwargs.get('id', '') + before = kwargs.get('before') or {} + after = kwargs.get('after') or {} + if len(str(before)) > self.max_length: + before = {_('Tips'): self.max_length_tip_msg} + if len(str(after)) > self.max_length: + after = {_('Tips'): self.max_length_tip_msg} + + op_log = self.model.objects.filter(pk=log_id).first() + if op_log is not None: + raw_after = op_log.after or {} + raw_before = op_log.before or {} + raw_before.update(before) + raw_after.update(after) + op_log.before = raw_before + op_log.after = raw_after + op_log.save() + else: + self.model.objects.create(**kwargs) diff --git a/apps/audits/backends/es.py b/apps/audits/backends/es.py new file mode 100644 index 000000000..7947b8f04 --- /dev/null +++ b/apps/audits/backends/es.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +import uuid + +from common.utils.timezone import local_now_display +from common.utils import get_logger +from common.utils.encode import Singleton +from common.plugins.es import ES + + +logger = get_logger(__file__) + + +class OperateLogStore(ES, metaclass=Singleton): + def __init__(self, config): + properties = { + "id": { + "type": "keyword" + }, + "user": { + "type": "keyword" + }, + "action": { + "type": "keyword" + }, + "resource_type": { + "type": "keyword" + }, + "org_id": { + "type": "keyword" + }, + "datetime": { + "type": "date", + "format": "yyyy-MM-dd HH:mm:ss" + } + } + exact_fields = {} + match_fields = { + 'id', 'user', 'action', 'resource_type', + 'resource', 'remote_addr', 'org_id' + } + keyword_fields = { + 'id', 'user', 'action', 'resource_type', 'org_id' + } + if not config.get('INDEX'): + config['INDEX'] = 'jumpserver_operate_log' + super().__init__(config, properties, keyword_fields, exact_fields, match_fields) + self.pre_use_check() + + @staticmethod + def make_data(data): + op_id = data.get('id', str(uuid.uuid4())) + datetime_param = data.get('datetime', local_now_display()) + data = { + 'id': op_id, 'user': data['user'], 'action': data['action'], + 'resource_type': data['resource_type'], 'resource': data['resource'], + 'remote_addr': data['remote_addr'], 'datetime': datetime_param, + 'before': data['before'], 'after': data['after'], 'org_id': data['org_id'] + } + return data + + def save(self, **kwargs): + log_id = kwargs.get('id', '') + before = kwargs.get('before') or {} + after = kwargs.get('after') or {} + + op_log = self.get({'id': log_id}) + if op_log is not None: + data = {'doc': {}} + raw_after = op_log.get('after') or {} + raw_before = op_log.get('before') or {} + raw_before.update(before) + raw_after.update(after) + data['doc']['before'] = raw_before + data['doc']['after'] = raw_after + self.es.update( + index=self.index, doc_type=self.doc_type, + id=op_log.get('es_id'), body=data, refresh=True + ) + else: + data = self.make_data(kwargs) + self.es.index( + index=self.index, doc_type=self.doc_type, body=data, + refresh=True + ) diff --git a/apps/audits/const.py b/apps/audits/const.py index 9ff993556..a49363e0a 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -7,11 +7,13 @@ DEFAULT_CITY = _("Unknown") MODELS_NEED_RECORD = ( # users 'User', 'UserGroup', + # authentication + 'AccessKey', 'TempToken', # acls 'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', # assets 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', - 'CommandFilter', 'Platform', 'AuthBook', + 'CommandFilter', 'Platform', 'Label', # applications 'Application', # orgs @@ -20,6 +22,13 @@ MODELS_NEED_RECORD = ( 'Setting', # perms 'AssetPermission', 'ApplicationPermission', + # notifications + 'SystemMsgSubscription', 'UserMsgSubscription', + # Terminal + 'Terminal', 'Endpoint', 'EndpointRule', 'CommandStorage', 'ReplayStorage', + # rbac + 'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding', # xpack - 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', + 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'ApplicationChangeAuthPlan', + 'GatherUserTask', 'Interface', ) diff --git a/apps/audits/handler.py b/apps/audits/handler.py new file mode 100644 index 000000000..c0dd0ac95 --- /dev/null +++ b/apps/audits/handler.py @@ -0,0 +1,183 @@ +from datetime import datetime + +from django.db import transaction +from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_request_ip, get_logger +from common.utils.timezone import as_current_tz +from common.utils.encode import Singleton +from common.local import encrypted_field_set +from settings.serializers import SettingsSerializer +from jumpserver.utils import current_request +from audits.models import OperateLog +from orgs.utils import get_current_org_id + +from .backends import get_operate_log_storage + + +logger = get_logger(__name__) + + +class ModelClient: + @staticmethod + def save(**kwargs): + log_id = kwargs.get('id', '') + op_log = OperateLog.objects.filter(pk=log_id).first() + if op_log is not None: + raw_after = op_log.after or {} + raw_before = op_log.before or {} + cur_before = kwargs.get('before') or {} + cur_after = kwargs.get('after') or {} + raw_before.update(cur_before) + raw_after.update(cur_after) + op_log.before = raw_before + op_log.after = raw_after + op_log.save() + else: + OperateLog.objects.create(**kwargs) + + +class OperatorLogHandler(metaclass=Singleton): + CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY' + + def __init__(self): + self.log_client = self.get_storage_client() + + @staticmethod + def get_storage_client(): + client = get_operate_log_storage() + return client + + @staticmethod + def _consistent_type_to_str(value1, value2): + if isinstance(value1, datetime): + value1 = as_current_tz(value1).strftime('%Y-%m-%d %H:%M:%S') + if isinstance(value2, datetime): + value2 = as_current_tz(value2).strftime('%Y-%m-%d %H:%M:%S') + return value1, value2 + + def _look_for_two_dict_change(self, left_dict, right_dict): + # 以右边的字典为基础 + before, after = {}, {} + for key, value in right_dict.items(): + pre_value = left_dict.get(key, '') + pre_value, value = self._consistent_type_to_str(pre_value, value) + if sorted(str(value)) == sorted(str(pre_value)): + continue + if pre_value: + before[key] = pre_value + if value: + after[key] = value + return before, after + + def cache_instance_before_data(self, instance_dict): + instance_id = instance_dict.get('id') + if instance_id is None: + return + + key = '%s_%s' % (self.CACHE_KEY, instance_id) + cache.set(key, instance_dict, 3 * 60) + + def get_instance_dict_from_cache(self, instance_id): + if instance_id is None: + return None + + key = '%s_%s' % (self.CACHE_KEY, instance_id) + cache_instance = cache.get(key, {}) + log_id = cache_instance.get('operate_log_id') + return log_id, cache_instance + + def get_instance_current_with_cache_diff(self, current_instance): + log_id, before, after = None, None, None + instance_id = current_instance.get('id') + if instance_id is None: + return log_id, before, after + + log_id, cache_instance = self.get_instance_dict_from_cache(instance_id) + if not cache_instance: + return log_id, before, after + + before, after = self._look_for_two_dict_change( + cache_instance, current_instance + ) + return log_id, before, after + + @staticmethod + def get_resource_display_from_setting(resource): + resource_display = None + setting_serializer = SettingsSerializer() + label = setting_serializer.get_field_label(resource) + if label is not None: + resource_display = label + return resource_display + + def get_resource_display(self, resource): + resource_display = str(resource) + return_value = self.get_resource_display_from_setting(resource_display) + if return_value is not None: + resource_display = return_value + return resource_display + + def __data_processing(self, dict_item, loop=True): + encrypt_value = '******' + for key, value in dict_item.items(): + if isinstance(value, bool): + value = _('Yes') if value else _('No') + elif isinstance(value, (list, tuple)): + value = ','.join(value) + elif isinstance(value, dict) and loop: + self.__data_processing(value, loop=False) + if key in encrypted_field_set: + value = encrypt_value + dict_item[key] = value + return dict_item + + def data_processing(self, before, after): + if before: + before = self.__data_processing(before) + if after: + after = self.__data_processing(after) + return before, after + + def create_or_update_operate_log( + self, action, resource_type, resource=None, + force=False, log_id=None, before=None, after=None + ): + user = current_request.user if current_request else None + if not user or not user.is_authenticated: + return + + remote_addr = get_request_ip(current_request) + resource_display = self.get_resource_display(resource) + before, after = self.data_processing(before, after) + if not force and not any([before, after]): + # 前后都没变化,没必要生成日志,除非手动强制保存 + return + + data = { + 'id': log_id, "user": str(user), 'action': action, + 'resource_type': str(resource_type), 'resource': resource_display, + 'remote_addr': remote_addr, 'before': before, 'after': after, + 'org_id': get_current_org_id(), + } + with transaction.atomic(): + if self.log_client.ping(timeout=1): + client = self.log_client + else: + logger.info('Switch default operate log storage save.') + client = get_operate_log_storage(default=True) + + try: + client.save(**data) + except Exception as e: + error_msg = 'An error occurred saving OperateLog.' \ + 'Error: %s, Data: %s' % (e, data) + logger.error(error_msg) + + +op_handler = OperatorLogHandler() +create_or_update_operate_log = op_handler.create_or_update_operate_log +cache_instance_before_data = op_handler.cache_instance_before_data +get_instance_current_with_cache_diff = op_handler.get_instance_current_with_cache_diff +get_instance_dict_from_cache = op_handler.get_instance_dict_from_cache diff --git a/apps/audits/migrations/0015_auto_20221011_1745.py b/apps/audits/migrations/0015_auto_20221011_1745.py new file mode 100644 index 000000000..dafd35dc8 --- /dev/null +++ b/apps/audits/migrations/0015_auto_20221011_1745.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2022-10-11 09:45 + +import common.db.encoder +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0014_auto_20220505_1902'), + ] + + operations = [ + migrations.AddField( + model_name='operatelog', + name='after', + field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True), + ), + migrations.AddField( + model_name='operatelog', + name='before', + field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 5dd8eb0a2..affd6045c 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -4,8 +4,9 @@ from django.db import models from django.db.models import Q from django.utils.translation import gettext, ugettext_lazy as _ from django.utils import timezone -from common.utils import lazyproperty +from common.utils import lazyproperty +from common.db.encoder import ModelJSONFieldEncoder from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org @@ -65,6 +66,8 @@ class OperateLog(OrgModelMixin): resource = models.CharField(max_length=128, verbose_name=_("Resource")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) + before = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) + after = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -78,6 +81,21 @@ class OperateLog(OrgModelMixin): self.org_id = Organization.ROOT_ID return super(OperateLog, self).save(*args, **kwargs) + @classmethod + def from_dict(cls, d): + self = cls() + for k, v in d.items(): + setattr(self, k, v) + return self + + @classmethod + def from_multi_dict(cls, l): + operate_logs = [] + for d in l: + operate_log = cls.from_dict(d) + operate_logs.append(operate_log) + return operate_logs + class Meta: verbose_name = _("Operate log") diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index bc78ef238..2b6c20686 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -47,6 +47,12 @@ class UserLoginLogSerializer(serializers.ModelSerializer): } +class OperateLogActionDetailSerializer(serializers.ModelSerializer): + class Meta: + model = models.OperateLog + fields = ('before', 'after') + + class OperateLogSerializer(serializers.ModelSerializer): action_display = serializers.CharField(source='get_action_display', label=_('Action')) diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index 9ac565bc6..19186b568 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # -import time +import uuid from django.db.models.signals import ( - post_save, m2m_changed, pre_delete + post_save, m2m_changed, pre_delete, pre_save ) from django.dispatch import receiver from django.conf import settings @@ -16,24 +16,32 @@ from django.utils import translation from rest_framework.renderers import JSONRenderer from rest_framework.request import Request -from assets.models import Asset, SystemUser +from users.models import User +from assets.models import Asset, SystemUser, CommandFilter +from terminal.models import Session, Command +from perms.models import AssetPermission, ApplicationPermission +from rbac.models import Role + +from audits.utils import model_to_dict_for_operate_log as model_to_dict +from audits.handler import ( + get_instance_current_with_cache_diff, cache_instance_before_data, + create_or_update_operate_log, get_instance_dict_from_cache +) from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need from jumpserver.utils import current_request -from users.models import User from users.signals import post_user_change_password -from terminal.models import Session, Command -from .utils import write_login_log, create_operate_log +from .utils import write_login_log from . import models, serializers from .models import OperateLog -from orgs.utils import current_org -from perms.models import AssetPermission, ApplicationPermission +from .const import MODELS_NEED_RECORD from terminal.backends.command.serializers import SessionCommandSerializer from terminal.serializers import SessionSerializer -from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR +from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json + logger = get_logger(__name__) sys_logger = get_syslogger(__name__) json_render = JSONRenderer() @@ -62,70 +70,6 @@ class AuthBackendLabelMapping(LazyObject): AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() - -M2M_NEED_RECORD = { - User.groups.through._meta.object_name: ( - _('User and Group'), - _('{User} JOINED {UserGroup}'), - _('{User} LEFT {UserGroup}') - ), - SystemUser.assets.through._meta.object_name: ( - _('Asset and SystemUser'), - _('{Asset} ADD {SystemUser}'), - _('{Asset} REMOVE {SystemUser}') - ), - Asset.nodes.through._meta.object_name: ( - _('Node and Asset'), - _('{Node} ADD {Asset}'), - _('{Node} REMOVE {Asset}') - ), - AssetPermission.users.through._meta.object_name: ( - _('User asset permissions'), - _('{AssetPermission} ADD {User}'), - _('{AssetPermission} REMOVE {User}'), - ), - AssetPermission.user_groups.through._meta.object_name: ( - _('User group asset permissions'), - _('{AssetPermission} ADD {UserGroup}'), - _('{AssetPermission} REMOVE {UserGroup}'), - ), - AssetPermission.assets.through._meta.object_name: ( - _('Asset permission'), - _('{AssetPermission} ADD {Asset}'), - _('{AssetPermission} REMOVE {Asset}'), - ), - AssetPermission.nodes.through._meta.object_name: ( - _('Node permission'), - _('{AssetPermission} ADD {Node}'), - _('{AssetPermission} REMOVE {Node}'), - ), - AssetPermission.system_users.through._meta.object_name: ( - _('Asset permission and SystemUser'), - _('{AssetPermission} ADD {SystemUser}'), - _('{AssetPermission} REMOVE {SystemUser}'), - ), - ApplicationPermission.users.through._meta.object_name: ( - _('User application permissions'), - _('{ApplicationPermission} ADD {User}'), - _('{ApplicationPermission} REMOVE {User}'), - ), - ApplicationPermission.user_groups.through._meta.object_name: ( - _('User group application permissions'), - _('{ApplicationPermission} ADD {UserGroup}'), - _('{ApplicationPermission} REMOVE {UserGroup}'), - ), - ApplicationPermission.applications.through._meta.object_name: ( - _('Application permission'), - _('{ApplicationPermission} ADD {Application}'), - _('{ApplicationPermission} REMOVE {Application}'), - ), - ApplicationPermission.system_users.through._meta.object_name: ( - _('Application permission and SystemUser'), - _('{ApplicationPermission} ADD {SystemUser}'), - _('{ApplicationPermission} REMOVE {SystemUser}'), - ), -} - M2M_ACTION = { POST_ADD: OperateLog.ACTION_CREATE, POST_REMOVE: OperateLog.ACTION_DELETE, @@ -137,60 +81,115 @@ M2M_ACTION = { def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs): if action not in M2M_ACTION: return - - user = current_request.user if current_request else None - if not user or not user.is_authenticated: + if not instance: return - sender_name = sender._meta.object_name - if sender_name in M2M_NEED_RECORD: - org_id = current_org.id - remote_addr = get_request_ip(current_request) - user = str(user) - resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name] - action = M2M_ACTION[action] - if action == OperateLog.ACTION_CREATE: - resource_tmpl = resource_tmpl_add - elif action == OperateLog.ACTION_DELETE: - resource_tmpl = resource_tmpl_remove + resource_type = instance._meta.verbose_name + current_instance = model_to_dict(instance, include_model_fields=False) - to_create = [] - objs = model.objects.filter(pk__in=pk_set) + instance_id = current_instance.get('id') + log_id, before_instance = get_instance_dict_from_cache(instance_id) - instance_name = instance._meta.object_name - instance_value = str(instance) + field_name = str(model._meta.verbose_name) + objs = model.objects.filter(pk__in=pk_set) + objs_display = [str(o) for o in objs] + action = M2M_ACTION[action] + changed_field = current_instance.get(field_name, []) - model_name = model._meta.object_name + after, before, before_value = None, None, None + if action == OperateLog.ACTION_CREATE: + before_value = list(set(changed_field) - set(objs_display)) + elif action == OperateLog.ACTION_DELETE: + before_value = list( + set(changed_field).symmetric_difference(set(objs_display)) + ) - for obj in objs: - resource = resource_tmpl.format(**{ - instance_name: instance_value, - model_name: str(obj) - })[:128] # `resource` 字段只有 128 个字符长 😔 + if changed_field: + after = {field_name: changed_field} + if before_value: + before = {field_name: before_value} - to_create.append(OperateLog( - user=user, action=action, resource_type=resource_type, - resource=resource, remote_addr=remote_addr, org_id=org_id - )) - OperateLog.objects.bulk_create(to_create) + if sorted(str(before)) == sorted(str(after)): + return + + create_or_update_operate_log( + OperateLog.ACTION_UPDATE, resource_type, + resource=instance, log_id=log_id, before=before, after=after + ) + + +def signal_of_operate_log_whether_continue(sender, instance, created, update_fields=None): + condition = True + if not instance: + condition = False + if instance and getattr(instance, SKIP_SIGNAL, False): + condition = False + # 终端模型的 create 事件由系统产生,不记录 + if instance._meta.object_name == 'Terminal' and created: + condition = False + # last_login 改变是最后登录日期, 每次登录都会改变 + if instance._meta.object_name == 'User' and \ + update_fields and 'last_login' in update_fields: + condition = False + # 不在记录白名单中,跳过 + if sender._meta.object_name not in MODELS_NEED_RECORD: + condition = False + return condition + + +@receiver(pre_save) +def on_object_pre_create_or_update(sender, instance=None, raw=False, using=None, update_fields=None, **kwargs): + ok = signal_of_operate_log_whether_continue( + sender, instance, False, update_fields + ) + if not ok: + return + instance_before_data = {'id': instance.id} + raw_instance = type(instance).objects.filter(pk=instance.id).first() + if raw_instance: + instance_before_data = model_to_dict(raw_instance) + operate_log_id = str(uuid.uuid4()) + instance_before_data['operate_log_id'] = operate_log_id + setattr(instance, 'operate_log_id', operate_log_id) + cache_instance_before_data(instance_before_data) @receiver(post_save) def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): - # last_login 改变是最后登录日期, 每次登录都会改变 - if instance._meta.object_name == 'User' and \ - update_fields and 'last_login' in update_fields: + ok = signal_of_operate_log_whether_continue( + sender, instance, created, update_fields + ) + if not ok: return + + log_id, before, after = None, None, None if created: action = models.OperateLog.ACTION_CREATE + after = model_to_dict(instance) + log_id = getattr(instance, 'operate_log_id', None) else: action = models.OperateLog.ACTION_UPDATE - create_operate_log(action, sender, instance) + current_instance = model_to_dict(instance) + log_id, before, after = get_instance_current_with_cache_diff(current_instance) + + resource_type = sender._meta.verbose_name + create_or_update_operate_log( + action, resource_type, resource=instance, + log_id=log_id, before=before, after=after + ) @receiver(pre_delete) def on_object_delete(sender, instance=None, **kwargs): - create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) + ok = signal_of_operate_log_whether_continue(sender, instance, False) + if not ok: + return + + resource_type = sender._meta.verbose_name + create_or_update_operate_log( + models.OperateLog.ACTION_DELETE, resource_type, + resource=instance, before=model_to_dict(instance) + ) @receiver(post_user_change_password, sender=User) diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 1fadbccee..8ce23a498 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -1,14 +1,15 @@ import csv import codecs -from django.http import HttpResponse -from django.db import transaction -from django.utils import translation +from itertools import chain -from audits.models import OperateLog -from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger -from jumpserver.utils import current_request -from .const import DEFAULT_CITY, MODELS_NEED_RECORD +from django.http import HttpResponse +from django.db import models + +from settings.serializers import SettingsSerializer +from common.utils import validate_ip, get_ip_city, get_logger +from common.db import fields +from .const import DEFAULT_CITY logger = get_logger(__name__) @@ -46,23 +47,60 @@ def write_login_log(*args, **kwargs): UserLoginLog.objects.create(**kwargs) -def create_operate_log(action, sender, resource): - user = current_request.user if current_request else None - if not user or not user.is_authenticated: - return - model_name = sender._meta.object_name - if model_name not in MODELS_NEED_RECORD: - return - with translation.override('en'): - resource_type = sender._meta.verbose_name - remote_addr = get_request_ip(current_request) +def get_resource_display(resource): + resource_display = str(resource) + setting_serializer = SettingsSerializer() + label = setting_serializer.get_field_label(resource_display) + if label is not None: + resource_display = label + return resource_display - data = { - "user": str(user), 'action': action, 'resource_type': resource_type, - 'resource': str(resource), 'remote_addr': remote_addr, - } - with transaction.atomic(): - try: - OperateLog.objects.create(**data) - except Exception as e: - logger.error("Create operate log error: {}".format(e)) + +def model_to_dict_for_operate_log( + instance, include_model_fields=True, include_related_fields=True +): + need_continue_fields = ['date_updated'] + opts = instance._meta + data = {} + for f in chain(opts.concrete_fields, opts.private_fields): + if isinstance(f, (models.FileField, models.ImageField)): + continue + + if getattr(f, 'attname', None) in need_continue_fields: + continue + + value = getattr(instance, f.name) or getattr(instance, f.attname) + if not isinstance(value, bool) and not value: + continue + + if getattr(f, 'primary_key', False): + f.verbose_name = 'id' + elif isinstance(f, ( + fields.EncryptCharField, fields.EncryptTextField, + fields.EncryptJsonDictCharField, fields.EncryptJsonDictTextField + )) or getattr(f, 'attname', '') == 'password': + value = 'encrypt|%s' % value + elif isinstance(value, list): + value = [str(v) for v in value] + + if include_model_fields or getattr(f, 'primary_key', False): + data[str(f.verbose_name)] = value + + if include_related_fields: + for f in chain(opts.many_to_many, opts.related_objects): + value = [] + if instance.pk is not None: + related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '') + if related_name: + try: + value = [str(i) for i in getattr(instance, related_name).all()] + except: + pass + if not value: + continue + try: + field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name + data[str(field_key)] = value + except: + pass + return data diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index e52f76577..a27c10d55 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -88,7 +88,7 @@ class ConnectionTokenMixin: filename, ssh_token = self.get_ssh_token(token) else: raise ValueError('Protocol not support: {}'.format(protocol)) - + filename = urllib.parse.unquote(filename) return { "filename": filename, "protocol": protocol, diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py index 95ebe6edc..8c676117c 100644 --- a/apps/authentication/api/password.py +++ b/apps/authentication/api/password.py @@ -1,13 +1,73 @@ from rest_framework.generics import CreateAPIView from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from django.utils.translation import ugettext as _ +from django.template.loader import render_to_string -from authentication.serializers import PasswordVerifySerializer +from common.utils.verify_code import SendAndVerifyCodeUtil from common.permissions import IsValidUser +from common.utils.random import random_string +from common.utils import get_object_or_none +from authentication.serializers import ( + PasswordVerifySerializer, ResetPasswordCodeSerializer +) +from settings.utils import get_login_title +from users.models import User from authentication.mixins import authenticate from authentication.errors import PasswordInvalid from authentication.mixins import AuthMixin +class UserResetPasswordSendCodeApi(CreateAPIView): + permission_classes = (AllowAny,) + serializer_class = ResetPasswordCodeSerializer + + @staticmethod + def is_valid_user( **kwargs): + user = get_object_or_none(User, **kwargs) + if not user: + err_msg = _('User does not exist: {}').format(_("No user matched")) + return None, err_msg + if not user.is_local: + err_msg = _( + 'The user is from {}, please go to the corresponding system to change the password' + ).format(user.get_source_display()) + return None, err_msg + return user, None + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + form_type = serializer.validated_data['form_type'] + username = serializer.validated_data['username'] + code = random_string(6, lower=False, upper=False) + other_args = {} + + if form_type == 'phone': + backend = 'sms' + target = serializer.validated_data['phone'] + user, err = self.is_valid_user(username=username, phone=target) + if not user: + return Response({'error': err}, status=400) + else: + backend = 'email' + target = serializer.validated_data['email'] + user, err = self.is_valid_user(username=username, email=target) + if not user: + return Response({'error': err}, status=400) + + subject = '%s: %s' % (get_login_title(), _('Forgot password')) + context = { + 'user': user, 'title': subject, 'code': code, + } + message = render_to_string('authentication/_msg_reset_password_code.html', context) + other_args['subject'] = subject + other_args['message'] = message + + SendAndVerifyCodeUtil(target, code, backend=backend, **other_args).gen_and_send_async() + return Response({'data': 'ok'}, status=200) + + class UserPasswordVerifyApi(AuthMixin, CreateAPIView): permission_classes = (IsValidUser,) serializer_class = PasswordVerifySerializer diff --git a/apps/authentication/const.py b/apps/authentication/const.py index d85afed75..d7e0690db 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -2,7 +2,7 @@ from django.db.models import TextChoices from authentication.confirm import CONFIRM_BACKENDS from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin -from .mfa import MFAOtp, MFASms, MFARadius +from .mfa import MFAOtp, MFASms, MFARadius, MFACustom RSA_PRIVATE_KEY = 'rsa_private_key' RSA_PUBLIC_KEY = 'rsa_public_key' @@ -35,3 +35,4 @@ class MFAType(TextChoices): OTP = MFAOtp.name, MFAOtp.display_name SMS = MFASms.name, MFASms.display_name Radius = MFARadius.name, MFARadius.display_name + Custom = MFACustom.name, MFACustom.display_name diff --git a/apps/authentication/mfa/__init__.py b/apps/authentication/mfa/__init__.py index 16279eb0d..077b6c12c 100644 --- a/apps/authentication/mfa/__init__.py +++ b/apps/authentication/mfa/__init__.py @@ -1,5 +1,4 @@ from .otp import MFAOtp, otp_failed_msg from .sms import MFASms from .radius import MFARadius - -MFA_BACKENDS = [MFAOtp, MFASms, MFARadius] +from .custom import MFACustom diff --git a/apps/authentication/mfa/custom.py b/apps/authentication/mfa/custom.py new file mode 100644 index 000000000..3b671f709 --- /dev/null +++ b/apps/authentication/mfa/custom.py @@ -0,0 +1,59 @@ +from django.conf import settings +from django.utils.module_loading import import_string +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger +from .base import BaseMFA + +logger = get_logger(__file__) + +mfa_custom_method = None + +if settings.MFA_CUSTOM: + """ 保证自定义认证方法在服务运行时不能被更改,只在第一次调用时加载一次 """ + try: + mfa_custom_method_path = 'data.mfa.main.check_code' + mfa_custom_method = import_string(mfa_custom_method_path) + except Exception as e: + logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e)) + +custom_failed_msg = _("MFA Custom code invalid") + + +class MFACustom(BaseMFA): + name = 'mfa_custom' + display_name = 'Custom' + placeholder = _("MFA custom verification code") + + def check_code(self, code): + assert self.is_authenticated() + ok = False + try: + ok = mfa_custom_method(user=self.user, code=code) + except Exception as exc: + logger.error('Custom authenticate error: {}'.format(exc)) + msg = '' if ok else custom_failed_msg + return ok, msg + + def is_active(self): + return True + + @staticmethod + def global_enabled(): + return settings.MFA_CUSTOM and callable(mfa_custom_method) + + def get_enable_url(self) -> str: + return '' + + def can_disable(self): + return False + + def disable(self): + return '' + + @staticmethod + def help_text_of_disable(): + return _("MFA custom global enabled, cannot disable") + + def get_disable_url(self) -> str: + return '' diff --git a/apps/authentication/mfa/sms.py b/apps/authentication/mfa/sms.py index d23b68766..aa426bf57 100644 --- a/apps/authentication/mfa/sms.py +++ b/apps/authentication/mfa/sms.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from .base import BaseMFA -from common.sdk.sms import SendAndVerifySMSUtil +from common.utils.verify_code import SendAndVerifyCodeUtil sms_failed_msg = _("SMS verify code invalid") @@ -15,7 +15,7 @@ class MFASms(BaseMFA): def __init__(self, user): super().__init__(user) phone = user.phone if self.is_authenticated() else '' - self.sms = SendAndVerifySMSUtil(phone) + self.sms = SendAndVerifyCodeUtil(phone, backend=self.name) def check_code(self, code): assert self.is_authenticated() @@ -37,7 +37,7 @@ class MFASms(BaseMFA): return True def send_challenge(self): - self.sms.gen_and_send() + self.sms.gen_and_send_async() @staticmethod def global_enabled(): diff --git a/apps/authentication/migrations/0001_initial.py b/apps/authentication/migrations/0001_initial.py index 6e45f932b..4c2251138 100644 --- a/apps/authentication/migrations/0001_initial.py +++ b/apps/authentication/migrations/0001_initial.py @@ -1,5 +1,6 @@ # Generated by Django 2.1.7 on 2019-02-28 08:07 +import common.db.models from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -27,7 +28,7 @@ class Migration(migrations.Migration): models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='AccessKeySecret')), ('user', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, + on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys', to=settings.AUTH_USER_MODEL, verbose_name='User')), ], diff --git a/apps/authentication/migrations/0002_auto_20190729_1423.py b/apps/authentication/migrations/0002_auto_20190729_1423.py index e117c8673..c4333f899 100644 --- a/apps/authentication/migrations/0002_auto_20190729_1423.py +++ b/apps/authentication/migrations/0002_auto_20190729_1423.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='accesskey', name='date_created', - field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2019, 7, 29, 6, 23, 54, 115123, tzinfo=utc)), + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2019, 7, 29, 6, 23, 54, 115123, tzinfo=utc), verbose_name='Date created'), preserve_default=False, ), migrations.AddField( diff --git a/apps/authentication/migrations/0006_auto_20211227_1059.py b/apps/authentication/migrations/0006_auto_20211227_1059.py index 945c5ff5c..b01758bbd 100644 --- a/apps/authentication/migrations/0006_auto_20211227_1059.py +++ b/apps/authentication/migrations/0006_auto_20211227_1059.py @@ -1,8 +1,8 @@ # Generated by Django 3.1.13 on 2021-12-27 02:59 +import common.db.models from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='ssotoken', name='user', - field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'), + field=models.ForeignKey(db_constraint=False, on_delete=common.db.models.CASCADE_SIGNAL_SKIP, to=settings.AUTH_USER_MODEL, verbose_name='User'), ), ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 518db566c..9b78a8bad 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -16,10 +16,10 @@ class AccessKey(models.Model): default=uuid.uuid4, editable=False) secret = models.UUIDField(verbose_name='AccessKeySecret', default=uuid.uuid4, editable=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', - on_delete=models.CASCADE, related_name='access_keys') + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), + on_delete=models.CASCADE_SIGNAL_SKIP, related_name='access_keys') is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_created = models.DateTimeField(auto_now_add=True) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) def get_id(self): return str(self.id) @@ -51,7 +51,7 @@ class SSOToken(models.JMSBaseModel): """ authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) expired = models.BooleanField(default=False, verbose_name=_('Expired')) - user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False) + user = models.ForeignKey('users.User', on_delete=models.CASCADE_SIGNAL_SKIP, verbose_name=_('User'), db_constraint=False) class Meta: verbose_name = _('SSO token') diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index f52274e49..152ba0fc6 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -1,15 +1,41 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.drf.fields import EncryptedField __all__ = [ 'MFAChallengeSerializer', 'MFASelectTypeSerializer', - 'PasswordVerifySerializer', + 'PasswordVerifySerializer', 'ResetPasswordCodeSerializer', ] +class ResetPasswordCodeSerializer(serializers.Serializer): + form_type = serializers.CharField(default='email') + username = serializers.CharField() + email = serializers.CharField(allow_blank=True) + phone = serializers.CharField(allow_blank=True) + + def create(self, attrs): + error = [] + form_type = attrs.get('form_type', 'email') + username = attrs.get('username') + if not username: + error.append(_('The {} cannot be empty').format(_('Username'))) + if form_type == 'phone': + phone = attrs.get('phone') + if not phone: + error.append(_('The {} cannot be empty').format(_('Phone'))) + else: + email = attrs.get('email') + if not email: + error.append(_('The {} cannot be empty').format(_('Email'))) + + if error: + raise serializers.ValidationError(error) + + class PasswordVerifySerializer(serializers.Serializer): password = EncryptedField() diff --git a/apps/authentication/templates/authentication/_msg_reset_password_code.html b/apps/authentication/templates/authentication/_msg_reset_password_code.html new file mode 100644 index 000000000..fc8d7a64e --- /dev/null +++ b/apps/authentication/templates/authentication/_msg_reset_password_code.html @@ -0,0 +1,21 @@ +{% load i18n %} + +
+ + + + + + + + + + + + + + + + +
{{ title }}
{% trans 'Hello' %} {{ user.name }},
{% trans 'Verify code' %}: {{ code }}
{% trans 'Copy the verification code to the Reset Password page to reset the password.' %}
{% trans 'The validity period of the verification code is one minute' %}
+
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index cfbac879f..99805a4e8 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -32,7 +32,8 @@ urlpatterns = [ path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'), path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'), path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'), - path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-codej'), + path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-code'), + path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'), path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), ] diff --git a/apps/common/const/signals.py b/apps/common/const/signals.py index 690a30161..78117ef13 100644 --- a/apps/common/const/signals.py +++ b/apps/common/const/signals.py @@ -15,3 +15,5 @@ POST_CLEAR = 'post_clear' POST_PREFIX = 'post' PRE_PREFIX = 'pre' + +SKIP_SIGNAL = 'skip_signal' diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index 0757bc068..ed00a0931 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import signer, crypto +from common.local import add_encrypted_field_set __all__ = [ @@ -149,6 +150,10 @@ class EncryptMixin: class EncryptTextField(EncryptMixin, models.TextField): description = _("Encrypt field using Secret Key") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + add_encrypted_field_set(self.verbose_name) + class EncryptCharField(EncryptMixin, models.CharField): @staticmethod @@ -163,6 +168,7 @@ class EncryptCharField(EncryptMixin, models.CharField): def __init__(self, *args, **kwargs): self.change_max_length(kwargs) super().__init__(*args, **kwargs) + add_encrypted_field_set(self.verbose_name) def deconstruct(self): name, path, args, kwargs = super().deconstruct() @@ -174,11 +180,15 @@ class EncryptCharField(EncryptMixin, models.CharField): class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + add_encrypted_field_set(self.verbose_name) class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + add_encrypted_field_set(self.verbose_name) class PortField(models.IntegerField): diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 92b6c4803..c3b00c62c 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -19,6 +19,8 @@ from django.db.models import QuerySet from django.db.models.functions import Concat from django.utils.translation import ugettext_lazy as _ +from ..const.signals import SKIP_SIGNAL + class Choice(str): def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label` @@ -124,6 +126,9 @@ class JMSModel(JMSBaseModel): class Meta: abstract = True + def __str__(self): + return str(self.id) + def concated_display(name1, name2): return Concat(F(name1), Value('('), F(name2), Value(')')) @@ -238,3 +243,14 @@ class MultiTableChildQueryset(QuerySet): self._batched_insert(objs, self.model._meta.local_fields, batch_size) return objs + + +def CASCADE_SIGNAL_SKIP(collector, field, sub_objs, using): + # 级联删除时,操作日志标记不保存,以免用户混淆 + try: + for obj in sub_objs: + setattr(obj, SKIP_SIGNAL, True) + except: + pass + + CASCADE(collector, field, sub_objs, using) diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index f97925f43..353b3a75b 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -4,6 +4,7 @@ from rest_framework import serializers from common.utils import decrypt_password +from common.local import add_encrypted_field_set __all__ = [ 'ReadableHiddenField', 'EncryptedField' @@ -32,6 +33,7 @@ class EncryptedField(serializers.CharField): write_only = True kwargs['write_only'] = write_only super().__init__(**kwargs) + add_encrypted_field_set(self.label) def to_internal_value(self, value): value = super().to_internal_value(value) diff --git a/apps/common/local.py b/apps/common/local.py index d0299b43e..947ae3d6b 100644 --- a/apps/common/local.py +++ b/apps/common/local.py @@ -1,7 +1,13 @@ from werkzeug.local import Local thread_local = Local() +encrypted_field_set = set() def _find(attr): return getattr(thread_local, attr, None) + + +def add_encrypted_field_set(label): + if label: + encrypted_field_set.add(str(label)) diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 9f824e5e2..1965129bb 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -7,7 +7,7 @@ from rest_framework import permissions from rest_framework.request import Request from common.exceptions import UserConfirmRequired -from audits.utils import create_operate_log +from audits.handler import create_or_update_operate_log from audits.models import OperateLog __all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"] @@ -62,10 +62,18 @@ class RecordViewLogMixin: def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) resource = self.get_resource_display(request) - create_operate_log(self.ACTION, self.model, resource) + resource_type = self.model._meta.verbose_name + create_or_update_operate_log( + self.ACTION, resource_type, force=True, + resource=resource + ) return response def retrieve(self, request, *args, **kwargs): response = super().retrieve(request, *args, **kwargs) - create_operate_log(self.ACTION, self.model, self.get_object()) + resource_type = self.model._meta.verbose_name + create_or_update_operate_log( + self.ACTION, resource_type, force=True, + resource=self.get_object() + ) return response diff --git a/apps/common/plugins/__init__.py b/apps/common/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/plugins/es.py b/apps/common/plugins/es.py new file mode 100644 index 000000000..7419e2d1a --- /dev/null +++ b/apps/common/plugins/es.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- +# +import datetime +import inspect + +from collections.abc import Iterable +from functools import reduce, partial +from itertools import groupby +from uuid import UUID + +from django.utils.translation import gettext_lazy as _ +from django.db.models import QuerySet as DJQuerySet +from elasticsearch import Elasticsearch +from elasticsearch.helpers import bulk +from elasticsearch.exceptions import RequestError, NotFoundError + +from common.utils.common import lazyproperty +from common.utils import get_logger +from common.utils.timezone import local_now_date_display +from common.exceptions import JMSException + + +logger = get_logger(__file__) + + +class InvalidElasticsearch(JMSException): + default_code = 'invalid_elasticsearch' + default_detail = _('Invalid elasticsearch config') + + +class NotSupportElasticsearch8(JMSException): + default_code = 'not_support_elasticsearch8' + default_detail = _('Not Support Elasticsearch8') + + +class ES(object): + def __init__(self, config, properties, keyword_fields, exact_fields=None, match_fields=None): + + self.config = config + hosts = self.config.get('HOSTS') + kwargs = self.config.get('OTHER', {}) + + ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False) + if ignore_verify_certs: + kwargs['verify_certs'] = None + self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs) + self.index_prefix = self.config.get('INDEX') or 'jumpserver' + self.is_index_by_date = bool(self.config.get('INDEX_BY_DATE', False)) + + self.index = None + self.query_index = None + self.properties = properties + self.exact_fields, self.match_fields, self.keyword_fields = set(), set(), set() + + if isinstance(keyword_fields, Iterable): + self.keyword_fields.update(keyword_fields) + if isinstance(exact_fields, Iterable): + self.exact_fields.update(exact_fields) + if isinstance(match_fields, Iterable): + self.match_fields.update(match_fields) + + self.init_index() + self.doc_type = self.config.get("DOC_TYPE") or '_doc' + if self.is_new_index_type(): + self.doc_type = '_doc' + self.exact_fields.update(self.keyword_fields) + else: + self.match_fields.update(self.keyword_fields) + + def init_index(self): + if self.is_index_by_date: + date = local_now_date_display() + self.index = '%s-%s' % (self.index_prefix, date) + self.query_index = '%s-alias' % self.index_prefix + else: + self.index = self.config.get("INDEX") or 'jumpserver' + self.query_index = self.config.get("INDEX") or 'jumpserver' + + def is_new_index_type(self): + if not self.ping(timeout=2): + return False + + info = self.es.info() + version = info['version']['number'].split('.')[0] + + if version == '8': + raise NotSupportElasticsearch8 + + try: + # 获取索引信息,如果没有定义,直接返回 + data = self.es.indices.get_mapping(self.index) + except NotFoundError: + return False + + try: + if version == '6': + # 检测索引是不是新的类型 es6 + properties = data[self.index]['mappings']['data']['properties'] + else: + # 检测索引是不是新的类型 es7 default index type: _doc + properties = data[self.index]['mappings']['properties'] + + for keyword in self.keyword_fields: + if not properties[keyword]['type'] == 'keyword': + break + else: + return True + except KeyError: + return False + + def pre_use_check(self): + if not self.ping(timeout=3): + raise InvalidElasticsearch + self._ensure_index_exists() + + def _ensure_index_exists(self): + info = self.es.info() + version = info['version']['number'].split('.')[0] + if version == '6': + mappings = {'mappings': {'data': {'properties': self.properties}}} + else: + mappings = {'mappings': {'properties': self.properties}} + + if self.is_index_by_date: + mappings['aliases'] = { + self.query_index: {} + } + + try: + self.es.indices.create(self.index, body=mappings) + return + except RequestError as e: + if e.error == 'resource_already_exists_exception': + logger.warning(e) + else: + logger.exception(e) + + def make_data(self, data): + return [] + + def save(self, **kwargs): + data = self.make_data(kwargs) + return self.es.index(index=self.index, doc_type=self.doc_type, body=data) + + def bulk_save(self, command_set, raise_on_error=True): + actions = [] + for command in command_set: + data = dict( + _index=self.index, + _type=self.doc_type, + _source=self.make_data(command), + ) + actions.append(data) + return bulk(self.es, actions, index=self.index, raise_on_error=raise_on_error) + + def get(self, query: dict): + item = None + data = self.filter(query, size=1) + if len(data) >= 1: + item = data[0] + return item + + def filter(self, query: dict, from_=None, size=None, sort=None): + try: + data = self._filter(query, from_, size, sort) + except Exception as e: + logger.error('ES filter error: {}'.format(e)) + data = [] + return data + + def _filter(self, query: dict, from_=None, size=None, sort=None): + body = self.get_query_body(**query) + + data = self.es.search( + index=self.query_index, doc_type=self.doc_type, body=body, + from_=from_, size=size, sort=sort + ) + source_data = [] + for item in data['hits']['hits']: + if item: + item['_source'].update({'es_id': item['_id']}) + source_data.append(item['_source']) + + return source_data + + def count(self, **query): + try: + body = self.get_query_body(**query) + data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body) + count = data["count"] + except Exception as e: + logger.error('ES count error: {}'.format(e)) + count = 0 + return count + + def __getattr__(self, item): + return getattr(self.es, item) + + def all(self): + """返回所有数据""" + raise NotImplementedError("Not support") + + def ping(self, timeout=None): + try: + return self.es.ping(request_timeout=timeout) + except Exception: + return False + + @staticmethod + def handler_time_field(data): + datetime__gte = data.get('datetime__gte') + datetime__lte = data.get('datetime__lte') + datetime_range = {} + + if datetime__gte: + if isinstance(datetime__gte, datetime.datetime): + datetime__gte = datetime__gte.strftime('%Y-%m-%d %H:%M:%S') + datetime_range['gte'] = datetime__gte + if datetime__lte: + if isinstance(datetime__lte, datetime.datetime): + datetime__lte = datetime__lte.strftime('%Y-%m-%d %H:%M:%S') + datetime_range['lte'] = datetime__lte + return 'datetime', datetime_range + + def get_query_body(self, **kwargs): + new_kwargs = {} + for k, v in kwargs.items(): + if isinstance(v, UUID): + v = str(v) + if k == 'pk': + k = 'id' + new_kwargs[k] = v + kwargs = new_kwargs + + index_in_field = 'id__in' + exact_fields = self.exact_fields + match_fields = self.match_fields + + match = {} + exact = {} + index = {} + + if index_in_field in kwargs: + index['values'] = kwargs[index_in_field] + + for k, v in kwargs.items(): + if k in exact_fields: + exact[k] = v + elif k in match_fields: + match[k] = v + + # 处理时间 + time_field_name, time_range = self.handler_time_field(kwargs) + + # 处理组织 + should = [] + org_id = match.get('org_id') + + real_default_org_id = '00000000-0000-0000-0000-000000000002' + root_org_id = '00000000-0000-0000-0000-000000000000' + + if org_id == root_org_id: + match.pop('org_id') + elif org_id in (real_default_org_id, ''): + match.pop('org_id') + should.append({ + 'bool': { + 'must_not': [ + { + 'wildcard': {'org_id': '*'} + } + ]} + }) + should.append({'match': {'org_id': real_default_org_id}}) + + # 构建 body + body = { + 'query': { + 'bool': { + 'must': [ + {'match': {k: v}} for k, v in match.items() + ], + 'should': should, + 'filter': [ + { + 'term': {k: v} + } for k, v in exact.items() + ] + [ + { + 'range': { + time_field_name: time_range + } + } + ] + [ + { + 'ids': {k: v} + } for k, v in index.items() + ] + } + }, + } + return body + + +class QuerySet(DJQuerySet): + default_days_ago = 7 + max_result_window = 10000 + + def __init__(self, es_instance): + self._method_calls = [] + self._slice = None # (from_, size) + self._storage = es_instance + + # 命令列表模糊搜索时报错 + super().__init__() + + @lazyproperty + def _grouped_method_calls(self): + _method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])} + return _method_calls + + @lazyproperty + def _filter_kwargs(self): + _method_calls = self._grouped_method_calls + filter_calls = _method_calls.get('filter') + if not filter_calls: + return {} + names, multi_args, multi_kwargs = zip(*filter_calls) + kwargs = reduce(lambda x, y: {**x, **y}, multi_kwargs, {}) + + striped_kwargs = {} + for k, v in kwargs.items(): + k = k.replace('__exact', '') + k = k.replace('__startswith', '') + k = k.replace('__icontains', '') + striped_kwargs[k] = v + return striped_kwargs + + @lazyproperty + def _sort(self): + order_by = self._grouped_method_calls.get('order_by') + if order_by: + for call in reversed(order_by): + fields = call[1] + if fields: + field = fields[-1] + + if field.startswith('-'): + direction = 'desc' + else: + direction = 'asc' + field = field.lstrip('-+') + sort = f'{field}:{direction}' + return sort + + def __execute(self): + _filter_kwargs = self._filter_kwargs + _sort = self._sort + from_, size = self._slice or (None, None) + data = self._storage.filter(_filter_kwargs, from_=from_, size=size, sort=_sort) + return self.model.from_multi_dict(data) + + def __stage_method_call(self, item, *args, **kwargs): + _clone = self.__clone() + _clone._method_calls.append((item, args, kwargs)) + return _clone + + def __clone(self): + uqs = QuerySet(self._storage) + uqs._method_calls = self._method_calls.copy() + uqs._slice = self._slice + uqs.model = self.model + return uqs + + def get(self, **kwargs): + kwargs.update(self._filter_kwargs) + return self._storage.get(kwargs) + + def count(self, limit_to_max_result_window=True): + filter_kwargs = self._filter_kwargs + count = self._storage.count(**filter_kwargs) + if limit_to_max_result_window: + count = min(count, self.max_result_window) + return count + + def __getattribute__(self, item): + if any(( + item.startswith('__'), + item in QuerySet.__dict__, + )): + return object.__getattribute__(self, item) + + origin_attr = object.__getattribute__(self, item) + if not inspect.ismethod(origin_attr): + return origin_attr + + attr = partial(self.__stage_method_call, item) + return attr + + def __getitem__(self, item): + max_window = self.max_result_window + if isinstance(item, slice): + if self._slice is None: + clone = self.__clone() + from_ = item.start or 0 + if item.stop is None: + size = self.max_result_window - from_ + else: + size = item.stop - from_ + + if from_ + size > max_window: + if from_ >= max_window: + from_ = max_window + size = 0 + else: + size = max_window - from_ + clone._slice = (from_, size) + return clone + return self.__execute()[item] + + def __repr__(self): + return self.__execute().__repr__() + + def __iter__(self): + return iter(self.__execute()) + + def __len__(self): + return self.count() diff --git a/apps/common/sdk/sms/__init__.py b/apps/common/sdk/sms/__init__.py index 21a51e37b..69a0e13e3 100644 --- a/apps/common/sdk/sms/__init__.py +++ b/apps/common/sdk/sms/__init__.py @@ -1,2 +1 @@ from .endpoint import SMS, BACKENDS -from .utils import SendAndVerifySMSUtil diff --git a/apps/common/sdk/sms/exceptions.py b/apps/common/sdk/sms/exceptions.py new file mode 100644 index 000000000..1fa3956bb --- /dev/null +++ b/apps/common/sdk/sms/exceptions.py @@ -0,0 +1,21 @@ +from django.utils.translation import gettext_lazy as _ + +from common.exceptions import JMSException + + +class CodeExpired(JMSException): + default_code = 'verify_code_expired' + default_detail = _('The verification code has expired. Please resend it') + + +class CodeError(JMSException): + default_code = 'verify_code_error' + default_detail = _('The verification code is incorrect') + + +class CodeSendTooFrequently(JMSException): + default_code = 'code_send_too_frequently' + default_detail = _('Please wait {} seconds before sending') + + def __init__(self, ttl): + super().__init__(detail=self.default_detail.format(ttl)) diff --git a/apps/common/sdk/sms/utils.py b/apps/common/sdk/sms/utils.py deleted file mode 100644 index 7474acc55..000000000 --- a/apps/common/sdk/sms/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import random - -from django.core.cache import cache -from django.utils.translation import gettext_lazy as _ - -from .endpoint import SMS -from common.utils import get_logger -from common.exceptions import JMSException - -logger = get_logger(__file__) - - -class CodeExpired(JMSException): - default_code = 'verify_code_expired' - default_detail = _('The verification code has expired. Please resend it') - - -class CodeError(JMSException): - default_code = 'verify_code_error' - default_detail = _('The verification code is incorrect') - - -class CodeSendTooFrequently(JMSException): - default_code = 'code_send_too_frequently' - default_detail = _('Please wait {} seconds before sending') - - def __init__(self, ttl): - super().__init__(detail=self.default_detail.format(ttl)) - - -class SendAndVerifySMSUtil: - KEY_TMPL = 'auth-verify-code-{}' - TIMEOUT = 60 - - def __init__(self, phone, key_suffix=None, timeout=None): - self.phone = phone - self.code = '' - self.timeout = timeout or self.TIMEOUT - self.key_suffix = key_suffix or str(phone) - 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) - except JMSException: - self.clear() - raise - - def generate(self): - code = ''.join(random.sample('0123456789', 4)) - self.code = code - return code - - def clear(self): - cache.delete(self.key) - - def send(self, code): - """ - 发送信息的方法,如果有错误直接抛出 api 异常 - """ - sms = SMS() - sms.send_verify_code(self.phone, code) - cache.set(self.key, self.code, self.timeout) - logger.info(f'Send sms verify code to {self.phone}: {code}') - - def verify(self, code): - right = cache.get(self.key) - if not right: - raise CodeExpired - - if right != code: - raise CodeError - - self.clear() - return True - - def ttl(self): - return cache.ttl(self.key) - - def get_code(self): - return cache.get(self.key) diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py new file mode 100644 index 000000000..abf4d6056 --- /dev/null +++ b/apps/common/utils/verify_code.py @@ -0,0 +1,94 @@ +from django.core.cache import cache +from django.conf import settings +from django.core.mail import send_mail +from celery import shared_task + +from common.sdk.sms.exceptions import CodeError, CodeExpired, CodeSendTooFrequently +from common.sdk.sms.endpoint import SMS +from common.exceptions import JMSException +from common.utils.random import random_string +from common.utils import get_logger + + +logger = get_logger(__file__) + + +@shared_task +def send_async(sender): + sender.gen_and_send() + + +class SendAndVerifyCodeUtil(object): + KEY_TMPL = 'auth-verify-code-{}' + + def __init__(self, target, code=None, key=None, backend='email', timeout=60, **kwargs): + self.target = target + self.code = code + self.timeout = timeout + self.backend = backend + self.key = key or self.KEY_TMPL.format(target) + self.other_args = kwargs + + def gen_and_send_async(self): + return send_async.delay(self) + + def gen_and_send(self): + ttl = self.__ttl() + if ttl > 0: + logger.error('Send sms too frequently, delay {}'.format(ttl)) + raise CodeSendTooFrequently(ttl) + + try: + if not self.code: + self.code = self.__generate() + self.__send(self.code) + except JMSException: + self.__clear() + raise + + def verify(self, code): + right = cache.get(self.key) + if not right: + raise CodeExpired + + if right != code: + raise CodeError + + self.__clear() + return True + + def __clear(self): + cache.delete(self.key) + + def __ttl(self): + return cache.ttl(self.key) + + def __get_code(self): + return cache.get(self.key) + + def __generate(self): + code = random_string(4, lower=False, upper=False) + self.code = code + return code + + def __send_with_sms(self): + sms = SMS() + sms.send_verify_code(self.target, self.code) + + def __send_with_email(self): + subject = self.other_args.get('subject') + message = self.other_args.get('message') + from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER + send_mail(subject, message, from_email, [self.target], html_message=message) + + def __send(self, code): + """ + 发送信息的方法,如果有错误直接抛出 api 异常 + """ + if self.backend == 'sms': + self.__send_with_sms() + else: + self.__send_with_email() + + cache.set(self.key, self.code, self.timeout) + logger.info(f'Send verify code to {self.target}: {code}') diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d7e4aee14..55303e2e1 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -227,6 +227,10 @@ class Config(dict): 'AUTH_CUSTOM': False, 'AUTH_CUSTOM_FILE_MD5': '', + # Custom Config + 'MFA_CUSTOM': False, + 'MFA_CUSTOM_FILE_MD5': '', + # Auth LDAP settings 'AUTH_LDAP': False, 'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389', @@ -416,6 +420,9 @@ class Config(dict): 'TERMINAL_HOST_KEY': '', 'TERMINAL_TELNET_REGEX': '', 'TERMINAL_COMMAND_STORAGE': {}, + # Luna 页面 + # 默认图形化分辨率 + 'TERMINAL_GRAPHICAL_RESOLUTION': 'Auto', # 未来废弃(目前迁移会用) 'TERMINAL_RDP_ADDR': '', # 保留(Luna还在用) diff --git a/apps/jumpserver/rewriting/pagination.py b/apps/jumpserver/rewriting/pagination.py new file mode 100644 index 000000000..cd6ad2642 --- /dev/null +++ b/apps/jumpserver/rewriting/pagination.py @@ -0,0 +1,6 @@ +from django.conf import settings +from rest_framework.pagination import LimitOffsetPagination + + +class MaxLimitOffsetPagination(LimitOffsetPagination): + max_limit = settings.MAX_LIMIT_PER_PAGE or 100 diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 2cf47e9fe..b4f1d3f9f 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -51,7 +51,6 @@ AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB AUTH_LDAP_SYNC_ORG_ID = CONFIG.AUTH_LDAP_SYNC_ORG_ID AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS - # ============================================================================== # 认证 OpenID 配置参数 # 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html @@ -180,7 +179,6 @@ if CONNECTION_TOKEN_EXPIRATION < 5 * 60: # 最少5分钟 CONNECTION_TOKEN_EXPIRATION = 5 * 60 - RBAC_BACKEND = 'rbac.backends.RBACBackend' AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend' AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' @@ -203,7 +201,7 @@ AUTHENTICATION_BACKENDS = [ # 只做权限校验 RBAC_BACKEND, # 密码形式 - AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS, + AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS, # 跳转形式 AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2, @@ -236,7 +234,22 @@ if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH): # 自定义认证模块 AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM) -AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2] +MFA_BACKEND_OTP = 'authentication.mfa.otp.MFAOtp' +MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius' +MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms' +MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom' + +MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS] + +MFA_CUSTOM = CONFIG.MFA_CUSTOM +MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5 +MFA_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'mfa', 'main.py') +if MFA_CUSTOM and MFA_CUSTOM_FILE_MD5 == get_file_md5(MFA_CUSTOM_FILE_PATH): + # 自定义多因子认证模块 + MFA_BACKENDS.append(MFA_BACKEND_CUSTOM) + +AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, + AUTH_BACKEND_OAUTH2] ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 3c929e97a..737cb8574 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -19,6 +19,16 @@ def exist_or_default(path, default): return path +def parse_sentinels_host(sentinels_host): + service_name, sentinels = None, None + try: + service_name, hosts = sentinels_host.split('/', 1) + sentinels = [tuple(h.split(':', 1)) for h in hosts.split(',')] + except Exception: + pass + return service_name, sentinels + + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) VERSION = const.VERSION BASE_DIR = const.BASE_DIR @@ -276,26 +286,53 @@ REDIS_SSL_CA = exist_or_default(os.path.join(CERTS_DIR, 'redis_ca.pem'), None) REDIS_SSL_CA = exist_or_default(os.path.join(CERTS_DIR, 'redis_ca.crt'), REDIS_SSL_CA) REDIS_SSL_REQUIRED = 'none' REDIS_USE_SSL = CONFIG.REDIS_USE_SSL +REDIS_PROTOCOL = 'rediss' if REDIS_USE_SSL else 'redis' +# Cache use sentinel +REDIS_SENTINEL_HOSTS = CONFIG.REDIS_SENTINEL_HOSTS +REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS = parse_sentinels_host(REDIS_SENTINEL_HOSTS) +REDIS_SENTINEL_PASSWORD = CONFIG.REDIS_SENTINEL_PASSWORD +if CONFIG.REDIS_SENTINEL_SOCKET_TIMEOUT: + REDIS_SENTINEL_SOCKET_TIMEOUT = float(CONFIG.REDIS_SENTINEL_SOCKET_TIMEOUT) +else: + REDIS_SENTINEL_SOCKET_TIMEOUT = None -REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % { - 'protocol': 'rediss' if REDIS_USE_SSL else 'redis', - 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, - 'port': CONFIG.REDIS_PORT, +# Cache config +REDIS_OPTIONS = { + "REDIS_CLIENT_KWARGS": { + "health_check_interval": 30 + }, + "CONNECTION_POOL_KWARGS": { + 'ssl_cert_reqs': REDIS_SSL_REQUIRED, + "ssl_keyfile": REDIS_SSL_KEY, + "ssl_certfile": REDIS_SSL_CERT, + "ssl_ca_certs": REDIS_SSL_CA + } if REDIS_USE_SSL else {} } +if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: + REDIS_LOCATION_NO_DB = "%(protocol)s://%(service_name)s/{}" % { + 'protocol': REDIS_PROTOCOL, 'service_name': REDIS_SENTINEL_SERVICE_NAME, + } + REDIS_OPTIONS.update({ + 'CLIENT_CLASS': 'django_redis.client.SentinelClient', + 'SENTINELS': REDIS_SENTINELS, 'PASSWORD': CONFIG.REDIS_PASSWORD, + 'SENTINEL_KWARGS': { + 'password': REDIS_SENTINEL_PASSWORD, + 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT + } + }) + DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory' +else: + REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % { + 'protocol': REDIS_PROTOCOL, 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, 'port': CONFIG.REDIS_PORT, + } + + REDIS_CACHE_DEFAULT = { 'BACKEND': 'redis_lock.django_cache.RedisCache', 'LOCATION': REDIS_LOCATION_NO_DB.format(CONFIG.REDIS_DB_CACHE), - 'OPTIONS': { - "REDIS_CLIENT_KWARGS": {"health_check_interval": 30}, - "CONNECTION_POOL_KWARGS": { - 'ssl_cert_reqs': REDIS_SSL_REQUIRED, - "ssl_keyfile": REDIS_SSL_KEY, - "ssl_certfile": REDIS_SSL_CERT, - "ssl_ca_certs": REDIS_SSL_CA - } if REDIS_USE_SSL else {} - } + 'OPTIONS': REDIS_OPTIONS } REDIS_CACHE_SESSION = dict(REDIS_CACHE_DEFAULT) REDIS_CACHE_SESSION['LOCATION'] = REDIS_LOCATION_NO_DB.format(CONFIG.REDIS_DB_SESSION) diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 4e9676379..5cf0bc95c 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -81,6 +81,9 @@ TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX +# 默认图形化分辨率 +TERMINAL_GRAPHICAL_RESOLUTION = CONFIG.TERMINAL_GRAPHICAL_RESOLUTION + # Asset user auth external backend, default AuthBook backend BACKEND_ASSET_USER_AUTH_VAULT = False @@ -178,5 +181,9 @@ HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key' SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key' +OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG + +MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE + # Magnus DB Port MAGNUS_PORTS = CONFIG.MAGNUS_PORTS diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 10f69cffd..ccde975a5 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -4,8 +4,9 @@ import os import ssl from .base import ( - REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, - REDIS_SSL_REQUIRED, REDIS_USE_SSL + REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL, + REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD, + REDIS_SENTINEL_SOCKET_TIMEOUT ) from ..const import CONFIG, PROJECT_DIR @@ -46,7 +47,7 @@ REST_FRAMEWORK = { 'SEARCH_PARAM': "search", 'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'DEFAULT_PAGINATION_CLASS': 'jumpserver.rewriting.pagination.MaxLimitOffsetPagination', 'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler', } @@ -90,16 +91,28 @@ else: if REDIS_SSL_CERT and REDIS_SSL_KEY: redis_ssl.load_cert_chain(REDIS_SSL_CERT, REDIS_SSL_KEY) +REDIS_HOST = { + 'db': CONFIG.REDIS_DB_WS, + 'password': CONFIG.REDIS_PASSWORD or None, + 'ssl': redis_ssl, +} + +if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: + REDIS_HOST['sentinels'] = REDIS_SENTINELS + REDIS_HOST['master_name'] = REDIS_SENTINEL_SERVICE_NAME + REDIS_HOST['sentinel_kwargs'] = { + 'password': REDIS_SENTINEL_PASSWORD, + 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT + } +else: + REDIS_HOST['address'] = (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT) + + CHANNEL_LAYERS = { 'default': { 'BACKEND': 'common.cache.RedisChannelLayer', 'CONFIG': { - "hosts": [{ - 'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT), - 'db': CONFIG.REDIS_DB_WS, - 'password': CONFIG.REDIS_PASSWORD or None, - 'ssl': redis_ssl - }], + "hosts": [REDIS_HOST], }, }, } @@ -109,13 +122,28 @@ ASGI_APPLICATION = 'jumpserver.routing.application' CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery') # Celery using redis as broker -CELERY_BROKER_URL = '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % { - 'protocol': 'rediss' if REDIS_USE_SSL else 'redis', - 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, - 'port': CONFIG.REDIS_PORT, - 'db': CONFIG.REDIS_DB_CELERY, -} +CELERY_BROKER_URL_FORMAT = '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' +if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: + CELERY_BROKER_URL = ';'.join([CELERY_BROKER_URL_FORMAT % { + 'protocol': 'sentinel', 'password': CONFIG.REDIS_PASSWORD, + 'host': item[0], 'port': item[1], 'db': CONFIG.REDIS_DB_CELERY + } for item in REDIS_SENTINELS]) + SENTINEL_OPTIONS = { + 'master_name': REDIS_SENTINEL_SERVICE_NAME, + 'sentinel_kwargs': { + 'password': REDIS_SENTINEL_PASSWORD, + 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT + } + } + CELERY_BROKER_TRANSPORT_OPTIONS = CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = SENTINEL_OPTIONS +else: + CELERY_BROKER_URL = CELERY_BROKER_URL_FORMAT % { + 'protocol': 'rediss' if REDIS_USE_SSL else 'redis', + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, + 'db': CONFIG.REDIS_DB_CELERY, + } CELERY_TASK_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle' CELERY_RESULT_BACKEND = CELERY_BROKER_URL diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 978aa3137..0b1bfab48 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7522cd9a7e7853d078c81006cea7f6dbe4fb9d51ae7c6dddd50e8471536d4c0d -size 133026 +oid sha256:b21e8af3ad29606b9ff36bbf5da8dd03a041b25e046c3cedabb8650390c0a4c7 +size 132358 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 9005e5563..ce9f73624 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-20 17:20+0800\n" +"POT-Creation-Date: 2022-11-10 10:55+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -30,20 +30,20 @@ msgstr "Acls" #: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 -#: terminal/models/storage.py:26 terminal/models/task.py:16 -#: terminal/models/terminal.py:100 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:669 -#: xpack/plugins/cloud/models.py:28 +#: terminal/models/storage.py:27 terminal/models/task.py:16 +#: terminal/models/terminal.py:101 users/forms/profile.py:33 +#: users/models/group.py:15 users/models/user.py:673 +#: xpack/plugins/cloud/models.py:27 msgid "Name" msgstr "名前" #: acls/models/base.py:27 assets/models/cmd_filter.py:88 -#: assets/models/user.py:251 terminal/models/endpoint.py:90 +#: assets/models/user.py:252 terminal/models/endpoint.py:90 msgid "Priority" msgstr "優先順位" #: acls/models/base.py:28 assets/models/cmd_filter.py:88 -#: assets/models/user.py:251 terminal/models/endpoint.py:91 +#: assets/models/user.py:252 terminal/models/endpoint.py:91 msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" @@ -62,11 +62,11 @@ msgstr "アクティブ" #: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 #: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 -#: terminal/models/storage.py:29 terminal/models/terminal.py:114 +#: terminal/models/storage.py:30 terminal/models/terminal.py:115 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:706 +#: users/models/group.py:16 users/models/user.py:712 #: xpack/plugins/change_auth_plan/models/base.py:44 -#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 +#: xpack/plugins/cloud/models.py:34 xpack/plugins/cloud/models.py:118 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "コメント" @@ -86,15 +86,16 @@ msgid "Login confirm" msgstr "ログイン確認" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37 -#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100 -#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220 -#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41 -#: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44 -#: terminal/models/sharing.py:33 terminal/notifications.py:91 -#: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:899 users/models/user.py:930 +#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:38 +#: audits/models.py:63 audits/models.py:105 audits/serializers.py:106 +#: authentication/models.py:19 authentication/models.py:54 +#: authentication/models.py:78 notifications/models/notification.py:12 +#: orgs/models.py:220 perms/models/base.py:84 rbac/builtin.py:120 +#: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:13 terminal/models/session.py:45 +#: terminal/models/sharing.py:33 terminal/notifications.py:94 +#: terminal/notifications.py:142 tickets/models/comment.py:21 users/const.py:14 +#: users/models/user.py:905 users/models/user.py:936 #: users/serializers/group.py:19 msgid "User" msgstr "ユーザー" @@ -105,7 +106,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:93 audits/models.py:63 audits/serializers.py:51 +#: assets/models/cmd_filter.py:93 audits/models.py:64 audits/serializers.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" msgstr "アクション" @@ -130,14 +131,14 @@ msgstr "システムユーザー" #: assets/models/asset.py:386 assets/models/authbook.py:19 #: assets/models/backup.py:31 assets/models/cmd_filter.py:42 #: assets/models/gathered_user.py:14 assets/serializers/label.py:30 -#: assets/serializers/system_user.py:268 audits/models.py:39 +#: assets/serializers/system_user.py:268 audits/models.py:40 #: authentication/models.py:66 authentication/models.py:90 #: perms/models/asset_permission.py:23 terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:46 -#: terminal/notifications.py:90 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:47 +#: terminal/notifications.py:93 #: xpack/plugins/change_auth_plan/models/asset.py:199 #: xpack/plugins/change_auth_plan/serializers/asset.py:181 -#: xpack/plugins/cloud/models.py:223 +#: xpack/plugins/cloud/models.py:225 msgid "Asset" msgstr "資産" @@ -155,13 +156,15 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること #: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:17 #: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 -#: assets/models/gathered_user.py:15 audits/models.py:121 +#: assets/models/gathered_user.py:15 audits/models.py:139 #: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 +#: authentication/models.py:260 authentication/serializers/password_mfa.py:25 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:667 -#: users/templates/users/_msg_user_created.html:12 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:102 +#: users/models/user.py:671 users/templates/users/_msg_user_created.html:12 +#: users/templates/users/forgot_password.html:51 +#: users/templates/users/forgot_password.html:98 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 #: xpack/plugins/cloud/serializers/account_attrs.py:26 @@ -185,13 +188,13 @@ msgstr "" #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 msgid "IP" msgstr "IP" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:211 #: assets/serializers/account.py:14 assets/serializers/gathered_user.py:23 -#: settings/serializers/terminal.py:7 +#: settings/serializers/terminal.py:9 msgid "Hostname" msgstr "ホスト名" @@ -204,7 +207,7 @@ msgstr "" "ション: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:213 -#: assets/models/domain.py:63 assets/models/user.py:252 +#: assets/models/domain.py:63 assets/models/user.py:253 #: terminal/serializers/session.py:31 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "プロトコル" @@ -223,7 +226,7 @@ msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" #: acls/serializers/rules/rules.py:20 -#: xpack/plugins/cloud/serializers/task.py:23 +#: xpack/plugins/cloud/serializers/task.py:24 msgid "IP address invalid: `{}`" msgstr "IPアドレスが無効: '{}'" @@ -248,7 +251,6 @@ msgstr "アプリケーション" #: applications/const.py:8 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:26 -#: xpack/plugins/change_auth_plan/models/app.py:32 msgid "Database" msgstr "データベース" @@ -256,7 +258,7 @@ msgstr "データベース" msgid "Remote app" msgstr "リモートアプリ" -#: applications/const.py:35 +#: applications/const.py:36 msgid "Custom" msgstr "カスタム" @@ -264,14 +266,15 @@ msgstr "カスタム" #: assets/models/backup.py:32 assets/models/cmd_filter.py:49 #: authentication/models.py:67 authentication/models.py:95 #: perms/models/application_permission.py:28 +#: xpack/plugins/change_auth_plan/models/app.py:32 msgid "Application" msgstr "アプリケーション" #: applications/models/account.py:15 assets/models/authbook.py:20 -#: assets/models/cmd_filter.py:46 assets/models/user.py:342 audits/models.py:40 +#: assets/models/cmd_filter.py:46 assets/models/user.py:343 audits/models.py:41 #: authentication/models.py:83 perms/models/application_permission.py:33 #: perms/models/asset_permission.py:25 terminal/backends/command/models.py:22 -#: terminal/backends/command/serializers.py:36 terminal/models/session.py:48 +#: terminal/backends/command/serializers.py:36 terminal/models/session.py:49 #: xpack/plugins/change_auth_plan/models/app.py:36 #: xpack/plugins/change_auth_plan/models/app.py:147 #: xpack/plugins/change_auth_plan/serializers/app.py:65 @@ -279,7 +282,7 @@ msgid "System user" msgstr "システムユーザー" #: applications/models/account.py:17 assets/models/authbook.py:21 -#: settings/serializers/auth/cas.py:18 +#: settings/serializers/auth/cas.py:20 msgid "Version" msgstr "バージョン" @@ -298,7 +301,7 @@ msgstr "アプリケーションアカウントの秘密を変更できます" #: applications/models/application.py:221 #: applications/serializers/application.py:101 assets/models/label.py:21 #: perms/models/application_permission.py:21 -#: perms/serializers/application/user_permission.py:33 +#: perms/serializers/application/user_permission.py:33 settings/models.py:35 #: tickets/models/ticket/apply_application.py:15 #: xpack/plugins/change_auth_plan/models/app.py:25 msgid "Category" @@ -306,10 +309,10 @@ msgstr "カテゴリ" #: applications/models/application.py:224 #: applications/serializers/application.py:103 assets/models/backup.py:49 -#: assets/models/cmd_filter.py:86 assets/models/user.py:250 +#: assets/models/cmd_filter.py:86 assets/models/user.py:251 #: authentication/models.py:70 perms/models/application_permission.py:24 #: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:58 terminal/models/storage.py:143 +#: terminal/models/storage.py:59 terminal/models/storage.py:145 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:18 #: tickets/models/ticket/general.py:273 @@ -323,8 +326,8 @@ msgstr "タイプ" msgid "Domain" msgstr "ドメイン" -#: applications/models/application.py:230 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:63 +#: applications/models/application.py:230 xpack/plugins/cloud/models.py:32 +#: xpack/plugins/cloud/serializers/account.py:64 msgid "Attrs" msgstr "ツールバーの" @@ -353,22 +356,25 @@ msgstr "タイプ表示" #: applications/serializers/application.py:105 assets/models/asset.py:230 #: assets/models/base.py:181 assets/models/cluster.py:26 -#: assets/models/domain.py:26 assets/models/gathered_user.py:19 -#: assets/models/group.py:22 assets/models/label.py:25 -#: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28 -#: assets/serializers/cmd_filter.py:48 common/db/models.py:114 +#: assets/models/cmd_filter.py:53 assets/models/domain.py:26 +#: assets/models/gathered_user.py:19 assets/models/group.py:22 +#: assets/models/label.py:25 assets/serializers/account.py:18 +#: assets/serializers/cmd_filter.py:28 assets/serializers/cmd_filter.py:48 +#: authentication/models.py:22 common/db/models.py:116 #: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 #: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:931 -#: xpack/plugins/cloud/models.py:125 +#: users/models/group.py:18 users/models/user.py:937 +#: xpack/plugins/change_auth_plan/models/base.py:45 +#: xpack/plugins/cloud/models.py:127 msgid "Date created" msgstr "作成された日付" #: applications/serializers/application.py:106 assets/models/base.py:182 -#: assets/models/gathered_user.py:20 assets/serializers/account.py:21 -#: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49 -#: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40 -#: orgs/models.py:224 +#: assets/models/cmd_filter.py:54 assets/models/gathered_user.py:20 +#: assets/serializers/account.py:21 assets/serializers/cmd_filter.py:29 +#: assets/serializers/cmd_filter.py:49 common/db/models.py:117 +#: common/mixins/models.py:51 ops/models/adhoc.py:40 orgs/models.py:224 +#: xpack/plugins/change_auth_plan/models/base.py:46 msgid "Date updated" msgstr "更新日" @@ -387,13 +393,14 @@ msgid "Cluster" msgstr "クラスター" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:15 +#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:16 +#: settings/serializers/auth/sms.py:67 terminal/models/endpoint.py:15 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" #: applications/serializers/attrs/application_category/db.py:12 +#: applications/serializers/attrs/application_type/clickhouse.py:10 #: applications/serializers/attrs/application_type/mongodb.py:10 #: applications/serializers/attrs/application_type/mysql.py:10 #: applications/serializers/attrs/application_type/mysql_workbench.py:22 @@ -402,13 +409,13 @@ msgstr "ホスト" #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 #: assets/models/asset.py:214 assets/models/domain.py:62 -#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:66 +#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" #: applications/serializers/attrs/application_category/db.py:16 -#: settings/serializers/email.py:36 +#: settings/serializers/email.py:37 msgid "Use SSL" msgstr "SSLの使用" @@ -603,12 +610,12 @@ msgstr "ホスト名生" #: assets/models/asset.py:215 assets/serializers/account.py:16 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41 -#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43 +#: xpack/plugins/cloud/models.py:106 xpack/plugins/cloud/serializers/task.py:44 msgid "Protocols" msgstr "プロトコル" #: assets/models/asset.py:218 assets/models/cmd_filter.py:38 -#: assets/models/user.py:242 perms/models/asset_permission.py:24 +#: assets/models/user.py:243 perms/models/asset_permission.py:24 #: xpack/plugins/change_auth_plan/models/asset.py:43 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" @@ -621,11 +628,11 @@ msgid "Is active" msgstr "アクティブです。" #: assets/models/asset.py:222 assets/models/cluster.py:19 -#: assets/models/user.py:239 assets/models/user.py:394 +#: assets/models/user.py:240 assets/models/user.py:395 msgid "Admin user" msgstr "管理ユーザー" -#: assets/models/asset.py:225 +#: assets/models/asset.py:225 xpack/plugins/cloud/const.py:32 msgid "Public IP" msgstr "パブリックIP" @@ -640,11 +647,11 @@ msgstr "ラベル" #: assets/models/asset.py:229 assets/models/base.py:183 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:56 #: assets/models/cmd_filter.py:103 assets/models/group.py:21 -#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71 -#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:714 +#: common/db/models.py:114 common/mixins/models.py:49 orgs/models.py:71 +#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:720 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 -#: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30 +#: xpack/plugins/cloud/models.py:124 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "によって作成された" @@ -697,7 +704,7 @@ msgid "Can view asset history account secret" msgstr "資産履歴アカウントパスワードを表示できます" #: assets/models/backup.py:30 perms/models/base.py:54 -#: settings/serializers/terminal.py:12 +#: settings/serializers/terminal.py:14 msgid "All" msgstr "すべて" @@ -722,8 +729,8 @@ msgstr "手動トリガー" msgid "Timing trigger" msgstr "タイミングトリガー" -#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31 -#: perms/models/base.py:89 terminal/models/session.py:58 +#: assets/models/backup.py:105 audits/models.py:45 ops/models/command.py:31 +#: perms/models/base.py:89 terminal/models/session.py:59 #: tickets/models/ticket/apply_application.py:29 #: tickets/models/ticket/apply_asset.py:23 #: xpack/plugins/change_auth_plan/models/base.py:112 @@ -751,17 +758,17 @@ msgstr "アカウントのバックアップスナップショット" msgid "Trigger mode" msgstr "トリガーモード" -#: assets/models/backup.py:119 audits/models.py:127 +#: assets/models/backup.py:119 audits/models.py:145 #: terminal/models/sharing.py:108 #: xpack/plugins/change_auth_plan/models/base.py:201 #: xpack/plugins/change_auth_plan/serializers/app.py:66 #: xpack/plugins/change_auth_plan/serializers/asset.py:180 -#: xpack/plugins/cloud/models.py:179 +#: xpack/plugins/cloud/models.py:181 msgid "Reason" msgstr "理由" -#: assets/models/backup.py:121 audits/serializers.py:82 -#: audits/serializers.py:97 ops/models/adhoc.py:260 +#: assets/models/backup.py:121 audits/serializers.py:88 +#: audits/serializers.py:103 ops/models/adhoc.py:260 #: terminal/serializers/session.py:36 #: xpack/plugins/change_auth_plan/models/base.py:202 #: xpack/plugins/change_auth_plan/serializers/app.py:67 @@ -783,10 +790,10 @@ msgstr "不明" msgid "Ok" msgstr "OK" -#: assets/models/base.py:32 audits/models.py:118 +#: assets/models/base.py:32 audits/models.py:136 #: xpack/plugins/change_auth_plan/serializers/app.py:88 #: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:35 +#: xpack/plugins/cloud/const.py:41 msgid "Failed" msgstr "失敗しました" @@ -800,10 +807,10 @@ msgstr "確認済みの日付" #: assets/models/base.py:177 assets/serializers/base.py:15 #: assets/serializers/base.py:37 assets/serializers/system_user.py:29 -#: audits/signal_handlers.py:50 authentication/confirm/password.py:9 +#: audits/signal_handlers.py:58 authentication/confirm/password.py:9 #: authentication/forms.py:32 #: authentication/templates/authentication/login.html:228 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:46 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 #: users/forms/profile.py:22 users/serializers/user.py:94 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 @@ -837,7 +844,10 @@ msgstr "帯域幅" msgid "Contact" msgstr "連絡先" -#: assets/models/cluster.py:22 users/models/user.py:689 +#: assets/models/cluster.py:22 authentication/serializers/password_mfa.py:29 +#: users/forms/profile.py:104 users/models/user.py:693 +#: users/templates/users/forgot_password.html:59 +#: users/templates/users/forgot_password.html:103 msgid "Phone" msgstr "電話" @@ -863,7 +873,7 @@ msgid "Default" msgstr "デフォルト" #: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:916 +#: users/models/user.py:922 msgid "System" msgstr "システム" @@ -872,7 +882,7 @@ msgid "Default Cluster" msgstr "デフォルトクラスター" #: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:675 +#: users/models/group.py:31 users/models/user.py:679 msgid "User group" msgstr "ユーザーグループ" @@ -885,7 +895,7 @@ msgid "Regex" msgstr "正規情報" #: assets/models/cmd_filter.py:72 ops/models/command.py:26 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:55 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:56 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 msgid "Command" @@ -1003,7 +1013,7 @@ msgid "Parent key" msgstr "親キー" #: assets/models/node.py:566 assets/serializers/system_user.py:267 -#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70 +#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers/task.py:72 msgid "Node" msgstr "ノード" @@ -1011,78 +1021,78 @@ msgstr "ノード" msgid "Can match node" msgstr "ノードを一致させることができます" -#: assets/models/user.py:233 +#: assets/models/user.py:234 msgid "Automatic managed" msgstr "自動管理" -#: assets/models/user.py:234 +#: assets/models/user.py:235 msgid "Manually input" msgstr "手動入力" -#: assets/models/user.py:238 +#: assets/models/user.py:239 msgid "Common user" msgstr "共通ユーザー" -#: assets/models/user.py:241 +#: assets/models/user.py:242 msgid "Username same with user" msgstr "ユーザーと同じユーザー名" -#: assets/models/user.py:244 assets/serializers/domain.py:30 +#: assets/models/user.py:245 assets/serializers/domain.py:30 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 #: xpack/plugins/change_auth_plan/models/asset.py:39 msgid "Assets" msgstr "資産" -#: assets/models/user.py:248 users/apps.py:9 +#: assets/models/user.py:249 users/apps.py:9 msgid "Users" msgstr "ユーザー" -#: assets/models/user.py:249 +#: assets/models/user.py:250 msgid "User groups" msgstr "ユーザーグループ" -#: assets/models/user.py:253 +#: assets/models/user.py:254 msgid "Auto push" msgstr "オートプッシュ" -#: assets/models/user.py:254 +#: assets/models/user.py:255 msgid "Sudo" msgstr "すど" -#: assets/models/user.py:255 +#: assets/models/user.py:256 msgid "Shell" msgstr "シェル" -#: assets/models/user.py:256 +#: assets/models/user.py:257 msgid "Login mode" msgstr "ログインモード" -#: assets/models/user.py:257 +#: assets/models/user.py:258 msgid "SFTP Root" msgstr "SFTPルート" -#: assets/models/user.py:258 assets/serializers/system_user.py:37 +#: assets/models/user.py:259 assets/serializers/system_user.py:37 #: authentication/models.py:52 msgid "Token" msgstr "トークン" -#: assets/models/user.py:259 +#: assets/models/user.py:260 msgid "Home" msgstr "ホーム" -#: assets/models/user.py:260 +#: assets/models/user.py:261 msgid "System groups" msgstr "システムグループ" -#: assets/models/user.py:263 +#: assets/models/user.py:264 msgid "User switch" msgstr "ユーザースイッチ" -#: assets/models/user.py:264 +#: assets/models/user.py:265 msgid "Switch from" msgstr "から切り替え" -#: assets/models/user.py:344 +#: assets/models/user.py:345 msgid "Can match system user" msgstr "システムユーザーに一致できます" @@ -1160,8 +1170,8 @@ msgstr "CPU情報" msgid "Actions" msgstr "アクション" -#: assets/serializers/backup.py:31 ops/mixin.py:106 ops/mixin.py:147 -#: settings/serializers/auth/ldap.py:65 +#: assets/serializers/backup.py:31 ops/mixin.py:26 ops/mixin.py:106 +#: ops/mixin.py:147 settings/serializers/auth/ldap.py:66 #: xpack/plugins/change_auth_plan/serializers/base.py:43 msgid "Periodic perform" msgstr "定期的なパフォーマンス" @@ -1171,7 +1181,7 @@ msgstr "定期的なパフォーマンス" msgid "Currently only mail sending is supported" msgstr "現在、メール送信のみがサポートされています" -#: assets/serializers/base.py:16 users/models/user.py:697 +#: assets/serializers/base.py:16 users/models/user.py:703 msgid "Private key" msgstr "ssh秘密鍵" @@ -1423,152 +1433,168 @@ msgstr "一致する資産がない、タスクを停止" msgid "Audits" msgstr "監査" -#: audits/models.py:27 audits/models.py:59 +#: audits/backends/db.py:12 +msgid "The text content is too long. Use Elasticsearch to store operation logs" +msgstr "文章の内容が長すぎる。Elasticsearchで操作履歴を保存する" + +#: audits/backends/db.py:24 audits/backends/db.py:26 +msgid "Tips" +msgstr "謎々" + +#: audits/handler.py:126 +msgid "Yes" +msgstr "是" + +#: audits/handler.py:126 +msgid "No" +msgstr "否" + +#: audits/models.py:28 audits/models.py:60 #: authentication/templates/authentication/_access_key_modal.html:65 #: rbac/tree.py:226 msgid "Delete" msgstr "削除" -#: audits/models.py:28 +#: audits/models.py:29 msgid "Upload" msgstr "アップロード" -#: audits/models.py:29 +#: audits/models.py:30 msgid "Download" msgstr "ダウンロード" -#: audits/models.py:30 +#: audits/models.py:31 msgid "Rmdir" msgstr "Rmdir" -#: audits/models.py:31 +#: audits/models.py:32 msgid "Rename" msgstr "名前の変更" -#: audits/models.py:32 +#: audits/models.py:33 msgid "Mkdir" msgstr "Mkdir" -#: audits/models.py:33 +#: audits/models.py:34 msgid "Symlink" msgstr "Symlink" -#: audits/models.py:38 audits/models.py:66 audits/models.py:89 -#: terminal/models/session.py:51 terminal/models/sharing.py:96 +#: audits/models.py:39 audits/models.py:67 audits/models.py:107 +#: terminal/models/session.py:52 terminal/models/sharing.py:96 msgid "Remote addr" msgstr "リモートaddr" -#: audits/models.py:41 +#: audits/models.py:42 msgid "Operate" msgstr "操作" -#: audits/models.py:42 +#: audits/models.py:43 msgid "Filename" msgstr "ファイル名" -#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:104 +#: audits/models.py:44 audits/models.py:135 terminal/models/sharing.py:104 #: tickets/views/approve.py:115 #: xpack/plugins/change_auth_plan/serializers/app.py:87 #: xpack/plugins/change_auth_plan/serializers/asset.py:198 msgid "Success" msgstr "成功" -#: audits/models.py:47 +#: audits/models.py:48 msgid "File transfer log" msgstr "ファイル転送ログ" -#: audits/models.py:56 +#: audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:223 msgid "Create" msgstr "作成" -#: audits/models.py:57 rbac/tree.py:224 +#: audits/models.py:58 rbac/tree.py:224 msgid "View" msgstr "表示" -#: audits/models.py:58 rbac/tree.py:225 templates/_csv_import_export.html:18 +#: audits/models.py:59 rbac/tree.py:225 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/models.py:64 audits/serializers.py:63 +#: audits/models.py:65 audits/serializers.py:69 msgid "Resource Type" msgstr "リソースタイプ" -#: audits/models.py:65 +#: audits/models.py:66 msgid "Resource" msgstr "リソース" -#: audits/models.py:67 audits/models.py:90 +#: audits/models.py:68 audits/models.py:108 #: terminal/backends/command/serializers.py:40 msgid "Datetime" msgstr "時間" -#: audits/models.py:82 +#: audits/models.py:100 msgid "Operate log" msgstr "ログの操作" -#: audits/models.py:88 +#: audits/models.py:106 msgid "Change by" msgstr "による変更" -#: audits/models.py:96 +#: audits/models.py:114 msgid "Password change log" msgstr "パスワード変更ログ" -#: audits/models.py:111 +#: audits/models.py:129 msgid "Disabled" msgstr "無効" -#: audits/models.py:112 settings/models.py:37 +#: audits/models.py:130 settings/models.py:37 msgid "Enabled" msgstr "有効化" -#: audits/models.py:113 +#: audits/models.py:131 msgid "-" msgstr "-" -#: audits/models.py:122 +#: audits/models.py:140 msgid "Login type" msgstr "ログインタイプ" -#: audits/models.py:123 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:141 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "ログインIP" -#: audits/models.py:124 +#: audits/models.py:142 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "ログイン都市" -#: audits/models.py:125 audits/serializers.py:44 +#: audits/models.py:143 audits/serializers.py:44 msgid "User agent" msgstr "ユーザーエージェント" -#: audits/models.py:126 +#: audits/models.py:144 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:692 +#: users/forms/profile.py:65 users/models/user.py:696 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:128 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:175 -#: xpack/plugins/cloud/models.py:227 +#: audits/models.py:146 terminal/models/status.py:33 +#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:177 +#: xpack/plugins/cloud/models.py:229 msgid "Status" msgstr "ステータス" -#: audits/models.py:129 +#: audits/models.py:147 msgid "Date login" msgstr "日付ログイン" -#: audits/models.py:130 audits/serializers.py:46 +#: audits/models.py:148 audits/serializers.py:46 msgid "Authentication backend" msgstr "認証バックエンド" -#: audits/models.py:169 +#: audits/models.py:187 msgid "User login log" msgstr "ユーザーログインログ" @@ -1588,234 +1614,67 @@ msgstr "MFAディスプレイ" msgid "Reason display" msgstr "理由表示" -#: audits/serializers.py:84 +#: audits/serializers.py:90 msgid "Hosts display" msgstr "ホスト表示" -#: audits/serializers.py:96 ops/models/command.py:27 -#: xpack/plugins/cloud/models.py:173 +#: audits/serializers.py:102 ops/models/command.py:27 +#: xpack/plugins/cloud/models.py:175 msgid "Result" msgstr "結果" -#: audits/serializers.py:98 terminal/serializers/storage.py:157 +#: audits/serializers.py:104 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "ホスト" -#: audits/serializers.py:99 +#: audits/serializers.py:105 msgid "Run as" msgstr "として実行" -#: audits/serializers.py:101 +#: audits/serializers.py:107 msgid "Run as display" msgstr "ディスプレイとして実行する" -#: audits/serializers.py:102 authentication/models.py:81 +#: audits/serializers.py:108 authentication/models.py:81 #: rbac/serializers/rolebinding.py:21 msgid "User display" msgstr "ユーザー表示" -#: audits/signal_handlers.py:49 +#: audits/signal_handlers.py:57 msgid "SSH Key" msgstr "SSHキー" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:59 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:52 +#: audits/signal_handlers.py:60 msgid "Auth Token" msgstr "認証トークン" -#: audits/signal_handlers.py:53 authentication/notifications.py:73 +#: audits/signal_handlers.py:61 authentication/notifications.py:73 #: authentication/views/login.py:73 authentication/views/wecom.py:178 -#: notifications/backends/__init__.py:11 users/models/user.py:728 +#: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 +#: users/models/user.py:734 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers.py:54 authentication/views/feishu.py:144 +#: audits/signal_handlers.py:62 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: users/models/user.py:730 +#: settings/serializers/auth/feishu.py:10 users/models/user.py:736 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 +#: audits/signal_handlers.py:63 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 -#: users/models/user.py:729 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:735 msgid "DingTalk" msgstr "DingTalk" -#: audits/signal_handlers.py:56 authentication/models.py:267 +#: audits/signal_handlers.py:64 authentication/models.py:267 msgid "Temporary token" msgstr "仮パスワード" -#: audits/signal_handlers.py:68 -msgid "User and Group" -msgstr "ユーザーとグループ" - -#: audits/signal_handlers.py:69 -#, python-brace-format -msgid "{User} JOINED {UserGroup}" -msgstr "{User} に参加 {UserGroup}" - -#: audits/signal_handlers.py:70 -#, python-brace-format -msgid "{User} LEFT {UserGroup}" -msgstr "{User} のそばを通る {UserGroup}" - -#: audits/signal_handlers.py:73 -msgid "Asset and SystemUser" -msgstr "資産およびシステム・ユーザー" - -#: audits/signal_handlers.py:74 -#, python-brace-format -msgid "{Asset} ADD {SystemUser}" -msgstr "{Asset} 追加 {SystemUser}" - -#: audits/signal_handlers.py:75 -#, python-brace-format -msgid "{Asset} REMOVE {SystemUser}" -msgstr "{Asset} 削除 {SystemUser}" - -#: audits/signal_handlers.py:78 -msgid "Node and Asset" -msgstr "ノードと資産" - -#: audits/signal_handlers.py:79 -#, python-brace-format -msgid "{Node} ADD {Asset}" -msgstr "{Node} 追加 {Asset}" - -#: audits/signal_handlers.py:80 -#, python-brace-format -msgid "{Node} REMOVE {Asset}" -msgstr "{Node} 削除 {Asset}" - -#: audits/signal_handlers.py:83 -msgid "User asset permissions" -msgstr "ユーザー資産の権限" - -#: audits/signal_handlers.py:84 -#, python-brace-format -msgid "{AssetPermission} ADD {User}" -msgstr "{AssetPermission} 追加 {User}" - -#: audits/signal_handlers.py:85 -#, python-brace-format -msgid "{AssetPermission} REMOVE {User}" -msgstr "{AssetPermission} 削除 {User}" - -#: audits/signal_handlers.py:88 -msgid "User group asset permissions" -msgstr "ユーザーグループの資産権限" - -#: audits/signal_handlers.py:89 -#, python-brace-format -msgid "{AssetPermission} ADD {UserGroup}" -msgstr "{AssetPermission} 追加 {UserGroup}" - -#: audits/signal_handlers.py:90 -#, python-brace-format -msgid "{AssetPermission} REMOVE {UserGroup}" -msgstr "{AssetPermission} 削除 {UserGroup}" - -#: audits/signal_handlers.py:93 perms/models/asset_permission.py:29 -msgid "Asset permission" -msgstr "資産権限" - -#: audits/signal_handlers.py:94 -#, python-brace-format -msgid "{AssetPermission} ADD {Asset}" -msgstr "{AssetPermission} 追加 {Asset}" - -#: audits/signal_handlers.py:95 -#, python-brace-format -msgid "{AssetPermission} REMOVE {Asset}" -msgstr "{AssetPermission} 削除 {Asset}" - -#: audits/signal_handlers.py:98 -msgid "Node permission" -msgstr "ノード権限" - -#: audits/signal_handlers.py:99 -#, python-brace-format -msgid "{AssetPermission} ADD {Node}" -msgstr "{AssetPermission} 追加 {Node}" - -#: audits/signal_handlers.py:100 -#, python-brace-format -msgid "{AssetPermission} REMOVE {Node}" -msgstr "{AssetPermission} 削除 {Node}" - -#: audits/signal_handlers.py:103 -msgid "Asset permission and SystemUser" -msgstr "資産権限とSystemUser" - -#: audits/signal_handlers.py:104 -#, python-brace-format -msgid "{AssetPermission} ADD {SystemUser}" -msgstr "{AssetPermission} 追加 {SystemUser}" - -#: audits/signal_handlers.py:105 -#, python-brace-format -msgid "{AssetPermission} REMOVE {SystemUser}" -msgstr "{AssetPermission} 削除 {SystemUser}" - -#: audits/signal_handlers.py:108 -msgid "User application permissions" -msgstr "ユーザーアプリケーションの権限" - -#: audits/signal_handlers.py:109 -#, python-brace-format -msgid "{ApplicationPermission} ADD {User}" -msgstr "{ApplicationPermission} 追加 {User}" - -#: audits/signal_handlers.py:110 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {User}" -msgstr "{ApplicationPermission} 削除 {User}" - -#: audits/signal_handlers.py:113 -msgid "User group application permissions" -msgstr "ユーザーグループアプリケーションの権限" - -#: audits/signal_handlers.py:114 -#, python-brace-format -msgid "{ApplicationPermission} ADD {UserGroup}" -msgstr "{ApplicationPermission} 追加 {UserGroup}" - -#: audits/signal_handlers.py:115 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {UserGroup}" -msgstr "{ApplicationPermission} 削除 {UserGroup}" - -#: audits/signal_handlers.py:118 perms/models/application_permission.py:38 -msgid "Application permission" -msgstr "申請許可" - -#: audits/signal_handlers.py:119 -#, python-brace-format -msgid "{ApplicationPermission} ADD {Application}" -msgstr "{ApplicationPermission} 追加 {Application}" - -#: audits/signal_handlers.py:120 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {Application}" -msgstr "{ApplicationPermission} 削除 {Application}" - -#: audits/signal_handlers.py:123 -msgid "Application permission and SystemUser" -msgstr "アプリケーション権限とSystemUser" - -#: audits/signal_handlers.py:124 -#, python-brace-format -msgid "{ApplicationPermission} ADD {SystemUser}" -msgstr "{ApplicationPermission} 追加 {SystemUser}" - -#: audits/signal_handlers.py:125 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {SystemUser}" -msgstr "{ApplicationPermission} 削除 {SystemUser}" - #: authentication/api/confirm.py:40 msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" @@ -1824,7 +1683,36 @@ msgstr "この操作には、MFAを検証する必要があります" msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" -#: authentication/apps.py:7 +#: authentication/api/password.py:29 terminal/api/session.py:224 +#: users/views/profile/reset.py:74 +msgid "User does not exist: {}" +msgstr "ユーザーが存在しない: {}" + +#: authentication/api/password.py:29 +msgid "No user matched" +msgstr "ユーザーにマッチしなかった" + +#: authentication/api/password.py:33 +msgid "" +"The user is from {}, please go to the corresponding system to change the " +"password" +msgstr "" +"ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" +"さい。" + +#: authentication/api/password.py:59 +#: authentication/templates/authentication/login.html:256 +#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:34 +msgid "Forgot password" +msgstr "パスワードを忘れた" + +#: authentication/apps.py:7 settings/serializers/auth/base.py:10 +#: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 +#: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 +#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 +#: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" msgstr "認証" @@ -2081,6 +1969,19 @@ msgstr "動的コード" msgid "Please input security code" msgstr "セキュリティコードを入力してください" +#: authentication/mfa/custom.py:20 +msgid "MFA Custom code invalid" +msgstr "カスタム MFA 検証コードの検証に失敗しました" + +#: authentication/mfa/custom.py:26 +msgid "MFA custom verification code" +msgstr "カスタム MFA 検証コード" + +#: authentication/mfa/custom.py:56 +msgid "MFA custom global enabled, cannot disable" +msgstr "" +"カスタム MFA はグローバルに有効になっており、無効にすることはできません" + #: authentication/mfa/otp.py:7 msgid "OTP code invalid, or server time error" msgstr "OTPコードが無効、またはサーバー時間エラー" @@ -2113,7 +2014,7 @@ msgstr "Radius globalが有効になり、無効にできません" msgid "SMS verify code invalid" msgstr "SMS検証コードが無効" -#: authentication/mfa/sms.py:12 +#: authentication/mfa/sms.py:12 settings/serializers/auth/sms.py:27 msgid "SMS" msgstr "SMS" @@ -2159,13 +2060,13 @@ msgstr "SSO token" #: authentication/models.py:72 authentication/models.py:261 #: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:17 +#: settings/serializers/auth/radius.py:19 msgid "Secret" msgstr "ひみつ" #: authentication/models.py:74 authentication/models.py:264 #: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:711 +#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:717 msgid "Date expired" msgstr "期限切れの日付" @@ -2234,7 +2135,7 @@ msgid "binding reminder" msgstr "バインディングリマインダー" #: authentication/serializers/connection_token.py:23 -#: xpack/plugins/cloud/models.py:34 +#: xpack/plugins/cloud/models.py:33 msgid "Validity" msgstr "有効性" @@ -2246,6 +2147,21 @@ msgstr "期限切れ時間" msgid "Asset or application required" msgstr "アセットまたはアプリが必要" +#: authentication/serializers/password_mfa.py:25 +#: authentication/serializers/password_mfa.py:29 +#: authentication/serializers/password_mfa.py:33 +#: users/templates/users/forgot_password.html:95 +msgid "The {} cannot be empty" +msgstr "{} 空にしてはならない" + +#: authentication/serializers/password_mfa.py:33 +#: notifications/backends/__init__.py:10 settings/serializers/email.py:19 +#: settings/serializers/email.py:50 users/forms/profile.py:103 +#: users/models/user.py:675 users/templates/users/forgot_password.html:55 +#: users/templates/users/forgot_password.html:108 +msgid "Email" +msgstr "メール" + #: authentication/serializers/token.py:79 #: perms/serializers/application/permission.py:20 #: perms/serializers/application/permission.py:41 @@ -2272,7 +2188,7 @@ msgid "ID" msgstr "ID" #: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:93 terminal/notifications.py:141 +#: terminal/notifications.py:96 terminal/notifications.py:144 msgid "Date" msgstr "日付" @@ -2281,14 +2197,14 @@ msgid "Show" msgstr "表示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: settings/serializers/security.py:39 users/models/user.py:556 +#: settings/serializers/security.py:39 users/models/user.py:557 #: users/serializers/profile.py:116 users/templates/users/mfa_setting.html:61 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "無効化" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:557 users/serializers/profile.py:117 +#: users/models/user.py:558 users/serializers/profile.py:117 #: users/templates/users/mfa_setting.html:26 #: users/templates/users/mfa_setting.html:68 msgid "Enable" @@ -2309,7 +2225,6 @@ msgid "Play CAPTCHA as audio file" msgstr "CAPTCHAをオーディオファイルとして再生する" #: authentication/templates/authentication/_captcha_field.html:15 -#: users/forms/profile.py:103 msgid "Captcha" msgstr "キャプチャ" @@ -2335,9 +2250,10 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_different_city.html:3 #: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3 +#: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:402 ops/tasks.py:145 ops/tasks.py:148 +#: jumpserver/conf.py:406 ops/tasks.py:145 ops/tasks.py:148 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2394,6 +2310,21 @@ msgstr "このリンクは1時間有効です。有効期限が切れた後" msgid "request new one" msgstr "新しいものを要求する" +#: authentication/templates/authentication/_msg_reset_password_code.html:12 +#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 +#: users/forms/profile.py:105 users/templates/users/forgot_password.html:63 +msgid "Verify code" +msgstr "コードの確認" + +#: authentication/templates/authentication/_msg_reset_password_code.html:15 +msgid "" +"Copy the verification code to the Reset Password page to reset the password." +msgstr "パスワードリセットページにcaptchaをコピーし、パスワードをリセットする" + +#: authentication/templates/authentication/_msg_reset_password_code.html:18 +msgid "The validity period of the verification code is one minute" +msgstr "認証コードの有効時間は 1 分" + #: authentication/templates/authentication/_msg_rest_password_success.html:5 msgid "Your password has just been successfully updated" msgstr "パスワードが正常に更新されました" @@ -2438,12 +2369,6 @@ msgid "Welcome back, please enter username and password to login" msgstr "" "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" -#: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:16 -#: users/templates/users/forgot_password.html:17 -msgid "Forgot password" -msgstr "パスワードを忘れた" - #: authentication/templates/authentication/login.html:264 #: templates/_header_bar.html:89 msgid "Login" @@ -2486,7 +2411,7 @@ msgid "Copy success" msgstr "コピー成功" #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 -#: xpack/plugins/cloud/const.py:26 +#: xpack/plugins/cloud/const.py:27 msgid "LAN" msgstr "ローカルエリアネットワーク" @@ -2634,35 +2559,35 @@ msgstr "%(name)s は正常に更新されました" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:80 +#: common/db/fields.py:81 msgid "Marshal dict data to char field" msgstr "チャーフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:84 +#: common/db/fields.py:85 msgid "Marshal dict data to text field" msgstr "テキストフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:96 +#: common/db/fields.py:97 msgid "Marshal list data to char field" msgstr "元帥リストデータをチャーフィールドに" -#: common/db/fields.py:100 +#: common/db/fields.py:101 msgid "Marshal list data to text field" msgstr "マーシャルリストデータをテキストフィールドに" -#: common/db/fields.py:104 +#: common/db/fields.py:105 msgid "Marshal data to char field" msgstr "チャーフィールドへのマーシャルデータ" -#: common/db/fields.py:108 +#: common/db/fields.py:109 msgid "Marshal data to text field" msgstr "テキストフィールドへのマーシャルデータ" -#: common/db/fields.py:150 +#: common/db/fields.py:151 msgid "Encrypt field using Secret Key" msgstr "Secret Keyを使用したフィールドの暗号化" -#: common/db/models.py:113 +#: common/db/models.py:115 msgid "Updated by" msgstr "によって更新" @@ -2732,6 +2657,14 @@ msgstr "選択項目のみエクスポート" msgid "Export filtered: %s" msgstr "検索のエクスポート: %s" +#: common/plugins/es.py:28 +msgid "Invalid elasticsearch config" +msgstr "無効なElasticsearch構成" + +#: common/plugins/es.py:33 +msgid "Not Support Elasticsearch8" +msgstr "サポートされていません Elasticsearch8" + #: common/sdk/im/exceptions.py:23 msgid "Network error, please contact system administrator" msgstr "ネットワークエラー、システム管理者に連絡してください" @@ -2776,15 +2709,15 @@ msgstr "SMSプロバイダーはサポートしていません: {}" msgid "SMS verification code signature or template invalid" msgstr "SMS検証コードの署名またはテンプレートが無効" -#: common/sdk/sms/utils.py:15 +#: common/sdk/sms/exceptions.py:8 msgid "The verification code has expired. Please resend it" msgstr "確認コードの有効期限が切れています。再送信してください" -#: common/sdk/sms/utils.py:20 +#: common/sdk/sms/exceptions.py:13 msgid "The verification code is incorrect" msgstr "確認コードが正しくありません" -#: common/sdk/sms/utils.py:25 +#: common/sdk/sms/exceptions.py:18 msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" @@ -2804,11 +2737,11 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" -#: jumpserver/conf.py:401 +#: jumpserver/conf.py:405 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:403 +#: jumpserver/conf.py:407 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -2852,15 +2785,26 @@ msgstr "" msgid "Notifications" msgstr "通知" -#: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:671 -msgid "Email" -msgstr "メール" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "サイトメッセージ" +#: notifications/models/notification.py:14 +msgid "receive backend" +msgstr "メッセージのバックエンド" + +#: notifications/models/notification.py:17 +msgid "User message" +msgstr "ユーザメッセージ" + +#: notifications/models/notification.py:20 +msgid "{} subscription" +msgstr "{} 購読" + +#: notifications/models/notification.py:32 +msgid "System message" +msgstr "システムメッセージ" + #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "タスク開始待ち" @@ -2874,12 +2818,12 @@ msgid "App ops" msgstr "アプリ操作" #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 -#: settings/serializers/auth/ldap.py:72 +#: settings/serializers/auth/ldap.py:73 msgid "Cycle perform" msgstr "サイクル実行" #: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150 -#: settings/serializers/auth/ldap.py:69 +#: settings/serializers/auth/ldap.py:70 msgid "Regularly perform" msgstr "定期的に実行する" @@ -3069,7 +3013,7 @@ msgstr "アプリ組織" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85 #: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48 -#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 +#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 msgid "Organization" msgstr "組織" @@ -3087,7 +3031,7 @@ msgid "Can view all joined org" msgstr "参加しているすべての組織を表示できます" #: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:679 +#: users/models/user.py:683 msgid "Role" msgstr "ロール" @@ -3103,6 +3047,10 @@ msgstr "管理者は権限を変更しています。お待ちください" msgid "The authorization cannot be revoked for the time being" msgstr "当分の間、承認を取り消すことはできません。" +#: perms/models/application_permission.py:38 +msgid "Application permission" +msgstr "申請許可" + #: perms/models/application_permission.py:111 msgid "Permed application" msgstr "許可されたアプリケーション" @@ -3119,6 +3067,10 @@ msgstr "ユーザーアプリを表示できます" msgid "Can view usergroup apps" msgstr "ユーザー・グループ認可の適用を表示できます" +#: perms/models/asset_permission.py:29 +msgid "Asset permission" +msgstr "資産権限" + #: perms/models/asset_permission.py:134 msgid "Ungrouped" msgstr "グループ化されていません" @@ -3360,19 +3312,15 @@ msgstr "Webターミナルを表示できます" msgid "Can view file manager" msgstr "ファイルマネージャを表示できます" -#: rbac/models/permission.py:26 -msgid "Permission" +#: rbac/models/permission.py:26 rbac/models/role.py:34 +msgid "Permissions" msgstr "権限" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: settings/serializers/auth/oauth2.py:35 +#: settings/serializers/auth/oauth2.py:37 msgid "Scope" msgstr "スコープ" -#: rbac/models/role.py:34 -msgid "Permissions" -msgstr "権限" - #: rbac/models/role.py:36 msgid "Built-in" msgstr "内蔵" @@ -3549,6 +3497,10 @@ msgstr "携帯番号をテストこのフィールドは必須です" msgid "Settings" msgstr "設定" +#: settings/models.py:36 +msgid "Encrypted" +msgstr "暗号化された" + #: settings/models.py:158 msgid "Can change email setting" msgstr "メール設定を変更できます" @@ -3589,122 +3541,134 @@ msgstr "ターミナルの設定を変えることができます" msgid "Can change other setting" msgstr "他の設定を変えることができます" -#: settings/serializers/auth/base.py:10 +#: settings/serializers/auth/base.py:10 settings/serializers/basic.py:27 +msgid "Basic" +msgstr "基本" + +#: settings/serializers/auth/base.py:12 msgid "CAS Auth" msgstr "CAS 認証" -#: settings/serializers/auth/base.py:11 +#: settings/serializers/auth/base.py:13 msgid "OPENID Auth" msgstr "OPENID 認証" -#: settings/serializers/auth/base.py:12 +#: settings/serializers/auth/base.py:14 msgid "RADIUS Auth" msgstr "RADIUS 認証" -#: settings/serializers/auth/base.py:13 +#: settings/serializers/auth/base.py:15 msgid "DingTalk Auth" msgstr "くぎ 認証" -#: settings/serializers/auth/base.py:14 +#: settings/serializers/auth/base.py:16 msgid "FeiShu Auth" msgstr "飛本 認証" -#: settings/serializers/auth/base.py:15 +#: settings/serializers/auth/base.py:17 msgid "WeCom Auth" msgstr "企業微信 認証" -#: settings/serializers/auth/base.py:16 +#: settings/serializers/auth/base.py:18 msgid "SSO Auth" msgstr "SSO Token 認証" -#: settings/serializers/auth/base.py:17 +#: settings/serializers/auth/base.py:19 msgid "SAML2 Auth" msgstr "SAML2 認証" -#: settings/serializers/auth/base.py:20 settings/serializers/basic.py:36 +#: settings/serializers/auth/base.py:22 settings/serializers/basic.py:38 msgid "Forgot password url" msgstr "パスワードのURLを忘れた" -#: settings/serializers/auth/base.py:26 +#: settings/serializers/auth/base.py:28 msgid "Enable login redirect msg" msgstr "ログインリダイレクトの有効化msg" #: settings/serializers/auth/cas.py:10 +msgid "CAS" +msgstr "CAS" + +#: settings/serializers/auth/cas.py:12 msgid "Enable CAS Auth" msgstr "CAS 認証の有効化" -#: settings/serializers/auth/cas.py:11 settings/serializers/auth/oidc.py:48 +#: settings/serializers/auth/cas.py:13 settings/serializers/auth/oidc.py:49 msgid "Server url" msgstr "サービス側アドレス" -#: settings/serializers/auth/cas.py:14 +#: settings/serializers/auth/cas.py:16 msgid "Proxy server url" msgstr "コールバックアドレス" -#: settings/serializers/auth/cas.py:16 settings/serializers/auth/oauth2.py:53 -#: settings/serializers/auth/saml2.py:32 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同期ログアウト" -#: settings/serializers/auth/cas.py:21 +#: settings/serializers/auth/cas.py:23 msgid "Username attr" msgstr "ユーザー名のプロパティ" -#: settings/serializers/auth/cas.py:24 +#: settings/serializers/auth/cas.py:26 msgid "Enable attributes map" msgstr "属性マップの有効化" -#: settings/serializers/auth/cas.py:26 settings/serializers/auth/saml2.py:31 +#: settings/serializers/auth/cas.py:28 settings/serializers/auth/saml2.py:33 msgid "Rename attr" msgstr "マッピングのプロパティ" -#: settings/serializers/auth/cas.py:27 +#: settings/serializers/auth/cas.py:29 msgid "Create user if not" msgstr "そうでない場合はユーザーを作成" -#: settings/serializers/auth/dingtalk.py:13 +#: settings/serializers/auth/dingtalk.py:15 msgid "Enable DingTalk Auth" msgstr "ピン認証の有効化" -#: settings/serializers/auth/feishu.py:12 +#: settings/serializers/auth/feishu.py:14 msgid "Enable FeiShu Auth" msgstr "飛本認証の有効化" -#: settings/serializers/auth/ldap.py:41 +#: settings/serializers/auth/ldap.py:39 +msgid "LDAP" +msgstr "LDAP" + +#: settings/serializers/auth/ldap.py:42 msgid "LDAP server" msgstr "LDAPサーバー" -#: settings/serializers/auth/ldap.py:42 +#: settings/serializers/auth/ldap.py:43 msgid "eg: ldap://localhost:389" msgstr "例: ldap://localhost:389" -#: settings/serializers/auth/ldap.py:44 +#: settings/serializers/auth/ldap.py:45 msgid "Bind DN" msgstr "DN のバインド" -#: settings/serializers/auth/ldap.py:49 +#: settings/serializers/auth/ldap.py:50 msgid "User OU" msgstr "ユーザー OU" -#: settings/serializers/auth/ldap.py:50 +#: settings/serializers/auth/ldap.py:51 msgid "Use | split multi OUs" msgstr "使用 | splitマルチ OU" -#: settings/serializers/auth/ldap.py:53 +#: settings/serializers/auth/ldap.py:54 msgid "User search filter" msgstr "ユーザー検索フィルター" -#: settings/serializers/auth/ldap.py:54 +#: settings/serializers/auth/ldap.py:55 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)" -#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:55 -#: settings/serializers/auth/oidc.py:36 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "ユーザー属性マッピング" -#: settings/serializers/auth/ldap.py:58 +#: settings/serializers/auth/ldap.py:59 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -3712,77 +3676,85 @@ msgstr "" "ユーザー属性マッピングは、LDAPのユーザー属性をjumpserverユーザーにマッピング" "する方法、username, name,emailはjumpserverのユーザーが必要とする属性です" -#: settings/serializers/auth/ldap.py:76 +#: settings/serializers/auth/ldap.py:77 msgid "Connect timeout" msgstr "接続タイムアウト" -#: settings/serializers/auth/ldap.py:78 +#: settings/serializers/auth/ldap.py:79 msgid "Search paged size" msgstr "ページサイズを検索" -#: settings/serializers/auth/ldap.py:80 +#: settings/serializers/auth/ldap.py:81 msgid "Enable LDAP auth" msgstr "LDAP認証の有効化" -#: settings/serializers/auth/oauth2.py:20 +#: settings/serializers/auth/oauth2.py:19 +msgid "OAuth2" +msgstr "OAuth2" + +#: settings/serializers/auth/oauth2.py:22 msgid "Enable OAuth2 Auth" msgstr "OAuth2認証の有効化" -#: settings/serializers/auth/oauth2.py:23 +#: settings/serializers/auth/oauth2.py:25 msgid "Logo" msgstr "アイコン" -#: settings/serializers/auth/oauth2.py:26 +#: settings/serializers/auth/oauth2.py:28 msgid "Service provider" msgstr "サービスプロバイダー" -#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18 +#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "クライアントID" -#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21 +#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "クライアント秘密" -#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62 +#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:63 msgid "Provider auth endpoint" msgstr "認証エンドポイントアドレス" -#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65 +#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:66 msgid "Provider token endpoint" msgstr "プロバイダートークンエンドポイント" -#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29 +#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "クライアント認証方式" -#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:72 msgid "Provider userinfo endpoint" msgstr "プロバイダーuserinfoエンドポイント" -#: settings/serializers/auth/oauth2.py:51 settings/serializers/auth/oidc.py:74 +#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:75 msgid "Provider end session endpoint" msgstr "プロバイダーのセッション終了エンドポイント" -#: settings/serializers/auth/oauth2.py:58 settings/serializers/auth/oidc.py:92 -#: settings/serializers/auth/saml2.py:33 +#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:93 +#: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "常にユーザーを更新" -#: settings/serializers/auth/oidc.py:15 +#: settings/serializers/auth/oidc.py:12 +msgid "OIDC" +msgstr "OIDC" + +#: settings/serializers/auth/oidc.py:16 msgid "Base site url" msgstr "ベースサイトのアドレス" -#: settings/serializers/auth/oidc.py:31 +#: settings/serializers/auth/oidc.py:32 msgid "Share session" msgstr "セッションの共有" -#: settings/serializers/auth/oidc.py:33 +#: settings/serializers/auth/oidc.py:34 msgid "Ignore ssl verification" msgstr "Ssl検証を無視する" -#: settings/serializers/auth/oidc.py:37 +#: settings/serializers/auth/oidc.py:38 msgid "" "User attr map present how to map OpenID user attr to jumpserver, username," "name,email is jumpserver attr" @@ -3790,83 +3762,91 @@ msgstr "" "ユーザー属性マッピングは、OpenIDのユーザー属性をjumpserverユーザーにマッピン" "グする方法、username, name,emailはjumpserverのユーザーが必要とする属性です" -#: settings/serializers/auth/oidc.py:45 +#: settings/serializers/auth/oidc.py:46 msgid "Use Keycloak" msgstr "Keycloakを使用する" -#: settings/serializers/auth/oidc.py:51 +#: settings/serializers/auth/oidc.py:52 msgid "Realm name" msgstr "レルム名" -#: settings/serializers/auth/oidc.py:57 +#: settings/serializers/auth/oidc.py:58 msgid "Enable OPENID Auth" msgstr "OIDC認証の有効化" -#: settings/serializers/auth/oidc.py:59 +#: settings/serializers/auth/oidc.py:60 msgid "Provider endpoint" msgstr "プロバイダーエンドポイント" -#: settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oidc.py:69 msgid "Provider jwks endpoint" msgstr "プロバイダーjwksエンドポイント" -#: settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oidc.py:78 msgid "Provider sign alg" msgstr "プロビダーサインalg" -#: settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oidc.py:81 msgid "Provider sign key" msgstr "プロバイダ署名キー" -#: settings/serializers/auth/oidc.py:82 +#: settings/serializers/auth/oidc.py:83 msgid "Scopes" msgstr "スコープ" -#: settings/serializers/auth/oidc.py:84 +#: settings/serializers/auth/oidc.py:85 msgid "Id token max age" msgstr "IDトークンの最大年齢" -#: settings/serializers/auth/oidc.py:87 +#: settings/serializers/auth/oidc.py:88 msgid "Id token include claims" msgstr "IDトークンにはクレームが含まれます" -#: settings/serializers/auth/oidc.py:89 +#: settings/serializers/auth/oidc.py:90 msgid "Use state" msgstr "使用状態" -#: settings/serializers/auth/oidc.py:90 +#: settings/serializers/auth/oidc.py:91 msgid "Use nonce" msgstr "Nonceを使用" #: settings/serializers/auth/radius.py:13 +msgid "Radius" +msgstr "Radius" + +#: settings/serializers/auth/radius.py:15 msgid "Enable Radius Auth" msgstr "Radius認証の有効化" -#: settings/serializers/auth/radius.py:19 +#: settings/serializers/auth/radius.py:21 msgid "OTP in Radius" msgstr "Radius のOTP" -#: settings/serializers/auth/saml2.py:12 +#: settings/serializers/auth/saml2.py:11 +msgid "SAML2" +msgstr "SAML2" + +#: settings/serializers/auth/saml2.py:14 msgid "Enable SAML2 Auth" msgstr "SAML2認証の有効化" -#: settings/serializers/auth/saml2.py:15 +#: settings/serializers/auth/saml2.py:17 msgid "IDP metadata URL" msgstr "IDP metadata アドレス" -#: settings/serializers/auth/saml2.py:18 +#: settings/serializers/auth/saml2.py:20 msgid "IDP metadata XML" msgstr "IDP metadata XML" -#: settings/serializers/auth/saml2.py:21 +#: settings/serializers/auth/saml2.py:23 msgid "SP advanced settings" msgstr "詳細設定" -#: settings/serializers/auth/saml2.py:25 +#: settings/serializers/auth/saml2.py:27 msgid "SP private key" msgstr "SP プライベートキー" -#: settings/serializers/auth/saml2.py:29 +#: settings/serializers/auth/saml2.py:31 msgid "SP cert" msgstr "SP 証明書" @@ -3878,50 +3858,50 @@ msgstr "SMSの有効化" msgid "SMS provider / Protocol" msgstr "SMSプロバイダ / プロトコル" -#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:43 -#: settings/serializers/auth/sms.py:51 settings/serializers/auth/sms.py:60 -#: settings/serializers/auth/sms.py:71 settings/serializers/email.py:65 +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 msgid "Signature" msgstr "署名" -#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:44 -#: settings/serializers/auth/sms.py:52 settings/serializers/auth/sms.py:61 +#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:46 +#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 msgid "Template code" msgstr "テンプレートコード" -#: settings/serializers/auth/sms.py:29 +#: settings/serializers/auth/sms.py:31 msgid "Test phone" msgstr "テスト電話" -#: settings/serializers/auth/sms.py:58 +#: settings/serializers/auth/sms.py:60 msgid "App Access Address" msgstr "アプリケーションアドレス" -#: settings/serializers/auth/sms.py:59 +#: settings/serializers/auth/sms.py:61 msgid "Signature channel number" msgstr "署名チャネル番号" -#: settings/serializers/auth/sms.py:67 +#: settings/serializers/auth/sms.py:69 msgid "Enterprise code(SP id)" msgstr "企業コード(SP id)" -#: settings/serializers/auth/sms.py:68 +#: settings/serializers/auth/sms.py:70 msgid "Shared secret(Shared secret)" msgstr "パスワードを共有する(Shared secret)" -#: settings/serializers/auth/sms.py:69 +#: settings/serializers/auth/sms.py:71 msgid "Original number(Src id)" msgstr "元の番号(Src id)" -#: settings/serializers/auth/sms.py:70 +#: settings/serializers/auth/sms.py:72 msgid "Business type(Service id)" msgstr "ビジネス・タイプ(Service id)" -#: settings/serializers/auth/sms.py:73 +#: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "テンプレート" -#: settings/serializers/auth/sms.py:74 +#: settings/serializers/auth/sms.py:76 #, python-brace-format msgid "" "Template need contain {code} and Signature + template length does not exceed " @@ -3932,34 +3912,34 @@ msgstr "" "満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない" "でください。" -#: settings/serializers/auth/sms.py:83 +#: settings/serializers/auth/sms.py:85 #, python-brace-format msgid "The template needs to contain {code}" msgstr "テンプレートには{code}を含める必要があります" -#: settings/serializers/auth/sms.py:86 +#: settings/serializers/auth/sms.py:88 msgid "Signature + Template must not exceed 65 words" msgstr "署名+テンプレートの長さは65文字以内" -#: settings/serializers/auth/sso.py:11 +#: settings/serializers/auth/sso.py:13 msgid "Enable SSO auth" msgstr "SSO Token認証の有効化" -#: settings/serializers/auth/sso.py:12 +#: settings/serializers/auth/sso.py:14 msgid "Other service can using SSO token login to JumpServer without password" msgstr "" "他のサービスはパスワードなしでJumpServerへのSSOトークンログインを使用できます" -#: settings/serializers/auth/sso.py:15 +#: settings/serializers/auth/sso.py:17 msgid "SSO auth key TTL" msgstr "Token有効期間" -#: settings/serializers/auth/sso.py:15 +#: settings/serializers/auth/sso.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "単位: 秒" -#: settings/serializers/auth/wecom.py:13 +#: settings/serializers/auth/wecom.py:15 msgid "Enable WeCom Auth" msgstr "企業微信認証の有効化" @@ -3971,23 +3951,23 @@ msgstr "件名" msgid "More url" msgstr "もっとURL" -#: settings/serializers/basic.py:28 +#: settings/serializers/basic.py:30 msgid "Site url" msgstr "サイトURL" -#: settings/serializers/basic.py:29 +#: settings/serializers/basic.py:31 msgid "eg: http://dev.jumpserver.org:8080" msgstr "例えば: http://dev.jumpserver.org:8080" -#: settings/serializers/basic.py:32 +#: settings/serializers/basic.py:34 msgid "User guide url" msgstr "ユーザーガイドurl" -#: settings/serializers/basic.py:33 +#: settings/serializers/basic.py:35 msgid "User first login update profile done redirect to it" msgstr "ユーザーの最初のログイン更新プロファイルがリダイレクトされました" -#: settings/serializers/basic.py:37 +#: settings/serializers/basic.py:39 msgid "" "The forgot password url on login page, If you use ldap or cas external " "authentication, you can set it" @@ -3995,57 +3975,61 @@ msgstr "" "ログインページでパスワードのURLを忘れてしまいました。ldapまたはcasの外部認証" "を使用している場合は、設定できます。" -#: settings/serializers/basic.py:41 +#: settings/serializers/basic.py:43 msgid "Global organization name" msgstr "グローバル組織名" -#: settings/serializers/basic.py:42 +#: settings/serializers/basic.py:44 msgid "The name of global organization to display" msgstr "表示するグローバル組織の名前" -#: settings/serializers/basic.py:44 +#: settings/serializers/basic.py:46 msgid "Enable announcement" msgstr "アナウンスの有効化" -#: settings/serializers/basic.py:45 +#: settings/serializers/basic.py:47 msgid "Announcement" msgstr "発表" -#: settings/serializers/basic.py:46 +#: settings/serializers/basic.py:48 msgid "Enable tickets" msgstr "チケットを有効にする" -#: settings/serializers/cleaning.py:10 +#: settings/serializers/cleaning.py:8 +msgid "Period clean" +msgstr "定時清掃" + +#: settings/serializers/cleaning.py:12 msgid "Login log keep days" msgstr "ログインログは日数を保持します" -#: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14 -#: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22 -#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35 +#: settings/serializers/cleaning.py:12 settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:20 settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:28 settings/serializers/other.py:37 msgid "Unit: day" msgstr "単位: 日" -#: settings/serializers/cleaning.py:14 +#: settings/serializers/cleaning.py:16 msgid "Task log keep days" msgstr "タスクログは日数を保持します" -#: settings/serializers/cleaning.py:18 +#: settings/serializers/cleaning.py:20 msgid "Operate log keep days" msgstr "ログ管理日を操作する" -#: settings/serializers/cleaning.py:22 +#: settings/serializers/cleaning.py:24 msgid "FTP log keep days" msgstr "ダウンロードのアップロード" -#: settings/serializers/cleaning.py:26 +#: settings/serializers/cleaning.py:28 msgid "Cloud sync record keep days" msgstr "クラウド同期レコードは日数を保持します" -#: settings/serializers/cleaning.py:29 +#: settings/serializers/cleaning.py:31 msgid "Session keep duration" msgstr "セッション維持期間" -#: settings/serializers/cleaning.py:30 +#: settings/serializers/cleaning.py:32 msgid "" "Unit: days, Session, record, command will be delete if more than duration, " "only in database" @@ -4053,65 +4037,65 @@ msgstr "" "単位:日。セッション、録画、コマンドレコードがそれを超えると削除されます(デー" "タベースストレージにのみ影響します。ossなどは影響しません」影響を受ける)" -#: settings/serializers/email.py:20 +#: settings/serializers/email.py:21 msgid "SMTP host" msgstr "SMTPホスト" -#: settings/serializers/email.py:21 +#: settings/serializers/email.py:22 msgid "SMTP port" msgstr "SMTPポート" -#: settings/serializers/email.py:22 +#: settings/serializers/email.py:23 msgid "SMTP account" msgstr "SMTPアカウント" -#: settings/serializers/email.py:24 +#: settings/serializers/email.py:25 msgid "SMTP password" msgstr "SMTPパスワード" -#: settings/serializers/email.py:25 +#: settings/serializers/email.py:26 msgid "Tips: Some provider use token except password" msgstr "ヒント: 一部のプロバイダーはパスワード以外のトークンを使用します" -#: settings/serializers/email.py:28 +#: settings/serializers/email.py:29 msgid "Send user" msgstr "ユーザーを送信" -#: settings/serializers/email.py:29 +#: settings/serializers/email.py:30 msgid "Tips: Send mail account, default SMTP account as the send account" msgstr "" "ヒント: 送信メールアカウント、送信アカウントとしてのデフォルトのSMTPアカウン" "ト" -#: settings/serializers/email.py:32 +#: settings/serializers/email.py:33 msgid "Test recipient" msgstr "テスト受信者" -#: settings/serializers/email.py:33 +#: settings/serializers/email.py:34 msgid "Tips: Used only as a test mail recipient" msgstr "ヒント: テストメールの受信者としてのみ使用" -#: settings/serializers/email.py:37 +#: settings/serializers/email.py:38 msgid "If SMTP port is 465, may be select" msgstr "SMTPポートが465の場合は、" -#: settings/serializers/email.py:40 +#: settings/serializers/email.py:41 msgid "Use TLS" msgstr "TLSの使用" -#: settings/serializers/email.py:41 +#: settings/serializers/email.py:42 msgid "If SMTP port is 587, may be select" msgstr "SMTPポートが587の場合は、" -#: settings/serializers/email.py:44 +#: settings/serializers/email.py:45 msgid "Subject prefix" msgstr "件名プレフィックス" -#: settings/serializers/email.py:51 +#: settings/serializers/email.py:54 msgid "Create user email subject" msgstr "ユーザーメール件名の作成" -#: settings/serializers/email.py:52 +#: settings/serializers/email.py:55 msgid "" "Tips: When creating a user, send the subject of the email (eg:Create account " "successfully)" @@ -4119,20 +4103,20 @@ msgstr "" "ヒント: ユーザーを作成するときに、メールの件名を送信します (例: アカウントを" "正常に作成)" -#: settings/serializers/email.py:56 +#: settings/serializers/email.py:59 msgid "Create user honorific" msgstr "ユーザー敬語の作成" -#: settings/serializers/email.py:57 +#: settings/serializers/email.py:60 msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgstr "" "ヒント: ユーザーを作成するときは、メールの敬語を送信します (例: こんにちは)" -#: settings/serializers/email.py:61 +#: settings/serializers/email.py:64 msgid "Create user email content" msgstr "ユーザーのメールコンテンツを作成する" -#: settings/serializers/email.py:62 +#: settings/serializers/email.py:65 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4141,72 +4125,76 @@ msgstr "" "ヒント:ユーザーの作成時にパスワード設定メールの内容を送信し、{username}{name}" "{email}ラベルをサポートします。" -#: settings/serializers/email.py:66 +#: settings/serializers/email.py:69 msgid "Tips: Email signature (eg:jumpserver)" msgstr "ヒント: メール署名 (例: jumpserver)" -#: settings/serializers/other.py:7 +#: settings/serializers/other.py:6 +msgid "More..." +msgstr "詳細..." + +#: settings/serializers/other.py:9 msgid "Email suffix" msgstr "メールのサフィックス" -#: settings/serializers/other.py:8 +#: settings/serializers/other.py:10 msgid "" "This is used by default if no email is returned during SSO authentication" msgstr "これは、SSO認証中にメールが返されない場合にデフォルトで使用されます。" -#: settings/serializers/other.py:12 +#: settings/serializers/other.py:14 msgid "OTP issuer name" msgstr "OTP発行者名" -#: settings/serializers/other.py:16 +#: settings/serializers/other.py:18 msgid "OTP valid window" msgstr "OTP有効なウィンドウ" -#: settings/serializers/other.py:21 +#: settings/serializers/other.py:23 msgid "CMD" msgstr "CMD" -#: settings/serializers/other.py:22 +#: settings/serializers/other.py:24 msgid "PowerShell" msgstr "PowerShell" -#: settings/serializers/other.py:24 +#: settings/serializers/other.py:26 msgid "Shell (Windows)" msgstr "シェル (Windows)" -#: settings/serializers/other.py:25 +#: settings/serializers/other.py:27 msgid "The shell type used when Windows assets perform ansible tasks" msgstr "" "Windowsアセットが実行可能なタスクを実行するときに使用されるシェルタイプ" -#: settings/serializers/other.py:29 +#: settings/serializers/other.py:31 msgid "Perm ungroup node" msgstr "グループ化されていないノードを表示" -#: settings/serializers/other.py:30 +#: settings/serializers/other.py:32 msgid "Perm single to ungroup node" msgstr "" "グループ化されていないノードに個別に許可された資産を配置し、資産が存在する" "ノードが表示されないようにしますが、そのノードが許可されていないという質問に" "質問" -#: settings/serializers/other.py:35 +#: settings/serializers/other.py:37 msgid "Ticket authorize default time" msgstr "デフォルト製造オーダ承認時間" -#: settings/serializers/other.py:39 +#: settings/serializers/other.py:41 msgid "Help Docs URL" msgstr "ドキュメントリンク" -#: settings/serializers/other.py:40 +#: settings/serializers/other.py:42 msgid "default: http://docs.jumpserver.org" msgstr "デフォルト: http://docs.jumpserver.org" -#: settings/serializers/other.py:44 +#: settings/serializers/other.py:46 msgid "Help Support URL" msgstr "サポートリンク" -#: settings/serializers/other.py:45 +#: settings/serializers/other.py:47 msgid "default: http://www.jumpserver.org/support/" msgstr "デフォルト: http://www.jumpserver.org/support/" @@ -4379,11 +4367,15 @@ msgstr "ログインcaptchaの有効化" msgid "Enable captcha to prevent robot authentication" msgstr "Captchaを有効にしてロボット認証を防止する" -#: settings/serializers/security.py:147 +#: settings/serializers/security.py:146 +msgid "Security" +msgstr "セキュリティ" + +#: settings/serializers/security.py:149 msgid "Enable terminal register" msgstr "ターミナルレジスタの有効化" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:151 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" @@ -4391,64 +4383,64 @@ msgstr "" "ターミナルレジスタを許可し、すべてのターミナルセットアップの後、セキュリティ" "のためにこれを無効にする必要があります" -#: settings/serializers/security.py:153 +#: settings/serializers/security.py:155 msgid "Enable watermark" msgstr "透かしの有効化" -#: settings/serializers/security.py:154 +#: settings/serializers/security.py:156 msgid "Enabled, the web session and replay contains watermark information" msgstr "Webセッションとリプレイには透かし情報が含まれています。" -#: settings/serializers/security.py:158 +#: settings/serializers/security.py:160 msgid "Connection max idle time" msgstr "接続最大アイドル時間" -#: settings/serializers/security.py:159 +#: settings/serializers/security.py:161 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "アイドル時間がそれ以上の場合は、接続単位を切断します: 分" -#: settings/serializers/security.py:162 +#: settings/serializers/security.py:164 msgid "Remember manual auth" msgstr "手動入力パスワードの保存" -#: settings/serializers/security.py:165 +#: settings/serializers/security.py:167 msgid "Enable change auth secure mode" msgstr "安全モードの変更を有効にする" -#: settings/serializers/security.py:168 +#: settings/serializers/security.py:170 msgid "Insecure command alert" msgstr "安全でないコマンドアラート" -#: settings/serializers/security.py:171 +#: settings/serializers/security.py:173 msgid "Email recipient" msgstr "メール受信者" -#: settings/serializers/security.py:172 +#: settings/serializers/security.py:174 msgid "Multiple user using , split" msgstr "複数のユーザーを使用して、分割" -#: settings/serializers/security.py:175 +#: settings/serializers/security.py:177 msgid "Batch command execution" msgstr "バッチコマンドの実行" -#: settings/serializers/security.py:176 +#: settings/serializers/security.py:178 msgid "Allow user run batch command or not using ansible" msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" -#: settings/serializers/security.py:179 +#: settings/serializers/security.py:181 msgid "Session share" msgstr "セッション共有" -#: settings/serializers/security.py:180 +#: settings/serializers/security.py:182 msgid "Enabled, Allows user active session to be shared with other users" msgstr "" "ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。" -#: settings/serializers/security.py:183 +#: settings/serializers/security.py:185 msgid "Remote Login Protection" msgstr "リモートログイン保護" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:187 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 " @@ -4458,19 +4450,28 @@ msgstr "" "します。アカウントが共通のログイン都市からログインしている場合、システムはリ" "モートログインリマインダーを送信します" -#: settings/serializers/terminal.py:13 +#: settings/serializers/settings.py:61 +#: users/templates/users/reset_password.html:29 +msgid "Setting" +msgstr "設定" + +#: settings/serializers/terminal.py:6 terminal/models/terminal.py:185 +msgid "Terminal" +msgstr "ターミナル" + +#: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自動" -#: settings/serializers/terminal.py:19 +#: settings/serializers/terminal.py:21 msgid "Password auth" msgstr "パスワード認証" -#: settings/serializers/terminal.py:21 +#: settings/serializers/terminal.py:23 msgid "Public key auth" msgstr "鍵認証" -#: settings/serializers/terminal.py:22 +#: settings/serializers/terminal.py:24 msgid "" "Tips: If use other auth method, like AD/LDAP, you should disable this to " "avoid being able to log in after deleting" @@ -4478,19 +4479,19 @@ msgstr "" "ヒント: AD/LDAPなどの他の認証方法を使用する場合は、サードパーティ製システムの" "削除後にこの項目を無効にする必要があります, ログインも可能" -#: settings/serializers/terminal.py:26 +#: settings/serializers/terminal.py:28 msgid "List sort by" msgstr "リストの並べ替え" -#: settings/serializers/terminal.py:29 +#: settings/serializers/terminal.py:31 msgid "List page size" msgstr "ページサイズを一覧表示" -#: settings/serializers/terminal.py:32 +#: settings/serializers/terminal.py:34 msgid "Telnet login regex" msgstr "Telnetログインregex" -#: settings/serializers/terminal.py:33 +#: settings/serializers/terminal.py:35 msgid "" "Tips: The login success message varies with devices. if you cannot log in to " "the device through Telnet, set this parameter" @@ -4498,18 +4499,27 @@ msgstr "" "ヒント: ログイン成功メッセージはデバイスによって異なります。Telnet経由でデバ" "イスにログインできない場合は、このパラメーターを設定します。" -#: settings/serializers/terminal.py:36 +#: settings/serializers/terminal.py:38 msgid "Enable database proxy" msgstr "属性マップの有効化" -#: settings/serializers/terminal.py:37 +#: settings/serializers/terminal.py:39 msgid "Enable Razor" msgstr "Razor の有効化" -#: settings/serializers/terminal.py:38 +#: settings/serializers/terminal.py:40 msgid "Enable SSH Client" msgstr "SSH Clientの有効化" +#: settings/serializers/terminal.py:51 +msgid "Default graphics resolution" +msgstr "デフォルトのグラフィック解像度" + +#: settings/serializers/terminal.py:52 +msgid "" +"Tip: Default resolution to use when connecting graphical assets in Luna pages" +msgstr "ヒント: Luna ページでグラフィック アセットを接続するときに使用するデフォルトの解像度" + #: settings/utils/ldap.py:467 msgid "ldap:// or ldaps:// protocol is used." msgstr "ldap:// または ldaps:// プロトコルが使用されます。" @@ -4750,14 +4760,17 @@ msgstr "" " " #: templates/_mfa_login_field.html:28 +#: users/templates/users/forgot_password.html:65 msgid "Send verification code" msgstr "確認コードを送信" #: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:122 msgid "Wait: " msgstr "待つ:" #: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:138 msgid "The verification code has been sent" msgstr "確認コードが送信されました" @@ -4800,13 +4813,21 @@ msgstr "Windowsリモートアプリケーション発行者ツール" #: templates/resource_download.html:43 msgid "" +"OpenSSH is a program used to connect remote applications in the Windows " +"Remote Application Publisher" +msgstr "" +"OpenSSHはリモートアプリケーションをWindowsリモートアプリケーションで接続する" +"プログラムです" + +#: templates/resource_download.html:48 +msgid "" "Jmservisor is the program used to pull up remote applications in Windows " "Remote Application publisher" msgstr "" "Jmservisorはwindowsリモートアプリケーションパブリケーションサーバでリモートア" "プリケーションを引き出すためのプログラムです" -#: templates/resource_download.html:51 +#: templates/resource_download.html:57 msgid "Offline video player" msgstr "オフラインビデオプレーヤー" @@ -4822,10 +4843,6 @@ msgstr "セッションが存在しません: {}" msgid "Session is finished or the protocol not supported" msgstr "セッションが終了したか、プロトコルがサポートされていません" -#: terminal/api/session.py:224 -msgid "User does not exist: {}" -msgstr "ユーザーが存在しない: {}" - #: terminal/api/session.py:232 msgid "User does not have permission" msgstr "ユーザーに権限がありません" @@ -4870,14 +4887,6 @@ msgstr "オンラインセッションを持つ" msgid "Terminals" msgstr "ターミナル管理" -#: terminal/backends/command/es.py:28 -msgid "Invalid elasticsearch config" -msgstr "無効なElasticsearch構成" - -#: terminal/backends/command/es.py:33 -msgid "Not Support Elasticsearch8" -msgstr "サポートされていません Elasticsearch8" - #: terminal/backends/command/models.py:16 msgid "Ordinary" msgstr "普通" @@ -4919,7 +4928,7 @@ msgstr "リスクレベル表示" msgid "Timestamp" msgstr "タイムスタンプ" -#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:105 +#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:106 msgid "Remote Address" msgstr "リモートアドレス" @@ -4955,11 +4964,11 @@ msgstr "コマンドレコード" msgid "HTTPS Port" msgstr "HTTPS ポート" -#: terminal/models/endpoint.py:18 terminal/models/terminal.py:107 +#: terminal/models/endpoint.py:18 terminal/models/terminal.py:108 msgid "HTTP Port" msgstr "HTTP ポート" -#: terminal/models/endpoint.py:19 terminal/models/terminal.py:106 +#: terminal/models/endpoint.py:19 terminal/models/terminal.py:107 msgid "SSH Port" msgstr "SSH ポート" @@ -4994,35 +5003,35 @@ msgstr "セッションのリプレイをアップロードできます" msgid "Can download session replay" msgstr "セッション再生をダウンロードできます" -#: terminal/models/session.py:50 terminal/models/sharing.py:101 +#: terminal/models/session.py:51 terminal/models/sharing.py:101 msgid "Login from" msgstr "ログイン元" -#: terminal/models/session.py:54 +#: terminal/models/session.py:55 msgid "Replay" msgstr "リプレイ" -#: terminal/models/session.py:59 +#: terminal/models/session.py:60 msgid "Date end" msgstr "終了日" -#: terminal/models/session.py:260 +#: terminal/models/session.py:261 msgid "Session record" msgstr "セッション記録" -#: terminal/models/session.py:262 +#: terminal/models/session.py:263 msgid "Can monitor session" msgstr "セッションを監視できます" -#: terminal/models/session.py:263 +#: terminal/models/session.py:264 msgid "Can share session" msgstr "セッションを共有できます" -#: terminal/models/session.py:264 +#: terminal/models/session.py:265 msgid "Can terminate session" msgstr "セッションを終了できます" -#: terminal/models/session.py:265 +#: terminal/models/session.py:266 msgid "Can validate session action perm" msgstr "セッションアクションのパーマを検証できます" @@ -5030,10 +5039,6 @@ msgstr "セッションアクションのパーマを検証できます" msgid "Creator" msgstr "作成者" -#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 -msgid "Verify code" -msgstr "コードの確認" - #: terminal/models/sharing.py:31 msgid "Expired time (min)" msgstr "期限切れ時間 (分)" @@ -5111,15 +5116,15 @@ msgstr "スレッド" msgid "Boot Time" msgstr "ブート時間" -#: terminal/models/storage.py:28 +#: terminal/models/storage.py:29 msgid "Default storage" msgstr "デフォルトのストレージ" -#: terminal/models/storage.py:137 terminal/models/terminal.py:108 +#: terminal/models/storage.py:139 terminal/models/terminal.py:109 msgid "Command storage" msgstr "コマンドストレージ" -#: terminal/models/storage.py:197 terminal/models/terminal.py:109 +#: terminal/models/storage.py:199 terminal/models/terminal.py:110 msgid "Replay storage" msgstr "再生ストレージ" @@ -5131,15 +5136,19 @@ msgstr "アルグ" msgid "Kwargs" msgstr "クワーグ" -#: terminal/models/terminal.py:103 +#: terminal/models/terminal.py:104 msgid "type" msgstr "タイプ" -#: terminal/models/terminal.py:183 -msgid "Terminal" -msgstr "ターミナル" +#: terminal/models/terminal.py:111 +msgid "Application User" +msgstr "ユーザーの適用" -#: terminal/models/terminal.py:185 +#: terminal/models/terminal.py:112 +msgid "Is Accepted" +msgstr "受け入れられる" + +#: terminal/models/terminal.py:187 msgid "Can view terminal config" msgstr "ターミナル構成を表示できます" @@ -5151,11 +5160,11 @@ msgstr "セッション" msgid "Danger command alert" msgstr "危険コマンドアラート" -#: terminal/notifications.py:92 terminal/notifications.py:140 +#: terminal/notifications.py:95 terminal/notifications.py:143 msgid "Level" msgstr "レベル" -#: terminal/notifications.py:110 +#: terminal/notifications.py:113 msgid "Batch danger command alert" msgstr "一括危険コマンド警告" @@ -5238,7 +5247,7 @@ msgstr "アクセスキー" msgid "Access key secret" msgstr "アクセスキーシークレット" -#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:220 +#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:222 msgid "Region" msgstr "リージョン" @@ -5764,100 +5773,109 @@ msgstr "パスワードの確認" msgid "Password does not match" msgstr "パスワードが一致しない" -#: users/forms/profile.py:109 +#: users/forms/profile.py:112 msgid "Old password" msgstr "古いパスワード" -#: users/forms/profile.py:119 +#: users/forms/profile.py:122 msgid "Old password error" msgstr "古いパスワードエラー" -#: users/forms/profile.py:129 +#: users/forms/profile.py:132 msgid "Automatically configure and download the SSH key" msgstr "SSHキーの自動設定とダウンロード" -#: users/forms/profile.py:131 +#: users/forms/profile.py:134 msgid "ssh public key" msgstr "ssh公開キー" -#: users/forms/profile.py:132 +#: users/forms/profile.py:135 msgid "ssh-rsa AAAA..." msgstr "ssh-rsa AAAA.." -#: users/forms/profile.py:133 +#: users/forms/profile.py:136 msgid "Paste your id_rsa.pub here." msgstr "ここにid_rsa.pubを貼り付けます。" -#: users/forms/profile.py:146 +#: users/forms/profile.py:149 msgid "Public key should not be the same as your old one." msgstr "公開鍵は古いものと同じであってはなりません。" -#: users/forms/profile.py:150 users/serializers/profile.py:100 +#: users/forms/profile.py:153 users/serializers/profile.py:100 #: users/serializers/profile.py:183 users/serializers/profile.py:210 msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:161 users/models/user.py:700 +#: users/forms/profile.py:164 users/models/user.py:706 msgid "Public key" msgstr "公開キー" -#: users/models/user.py:558 +#: users/models/user.py:559 msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:625 +#: users/models/user.py:629 msgid "Local" msgstr "ローカル" -#: users/models/user.py:681 users/serializers/user.py:149 +#: users/models/user.py:685 users/serializers/user.py:149 msgid "Is service account" msgstr "サービスアカウントです" -#: users/models/user.py:683 +#: users/models/user.py:687 msgid "Avatar" msgstr "アバター" -#: users/models/user.py:686 +#: users/models/user.py:690 msgid "Wechat" msgstr "微信" -#: users/models/user.py:703 +#: users/models/user.py:699 +msgid "OTP secret key" +msgstr "OTP 秘密" + +#: users/models/user.py:709 msgid "Secret key" msgstr "秘密キー" -#: users/models/user.py:719 +#: users/models/user.py:714 users/serializers/profile.py:149 +#: users/serializers/user.py:146 +msgid "Is first login" +msgstr "最初のログインです" + +#: users/models/user.py:725 msgid "Source" msgstr "ソース" -#: users/models/user.py:723 +#: users/models/user.py:729 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:726 +#: users/models/user.py:732 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:901 +#: users/models/user.py:907 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:902 +#: users/models/user.py:908 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:903 +#: users/models/user.py:909 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:912 +#: users/models/user.py:918 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:915 +#: users/models/user.py:921 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:940 +#: users/models/user.py:946 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -5868,7 +5886,7 @@ msgstr "ユーザーパスワード履歴" msgid "Reset password" msgstr "パスワードのリセット" -#: users/notifications.py:85 users/views/profile/reset.py:127 +#: users/notifications.py:85 users/views/profile/reset.py:139 msgid "Reset password success" msgstr "パスワードのリセット成功" @@ -5908,10 +5926,6 @@ msgstr "新しいパスワードを最後の {} 個のパスワードにする msgid "The newly set password is inconsistent" msgstr "新しく設定されたパスワードが一致しない" -#: users/serializers/profile.py:149 users/serializers/user.py:146 -msgid "Is first login" -msgstr "最初のログインです" - #: users/serializers/user.py:28 msgid "System roles" msgstr "システムの役割" @@ -6064,14 +6078,22 @@ msgstr "あなたのssh公開鍵はサイト管理者によってリセットさ msgid "click here to set your password" msgstr "ここをクリックしてパスワードを設定してください" -#: users/templates/users/forgot_password.html:24 +#: users/templates/users/forgot_password.html:38 msgid "Input your email, that will send a mail to your" msgstr "あなたのメールを入力し、それはあなたにメールを送信します" -#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:68 msgid "Submit" msgstr "送信" +#: users/templates/users/forgot_password.html:71 +msgid "Use the phone number to retrieve the password" +msgstr "携帯電話番号を使ってパスワードを探す" + +#: users/templates/users/forgot_password.html:72 +msgid "Use email to retrieve the password" +msgstr "メールアドレスを使ってパスワードを取り戻す" + #: users/templates/users/mfa_setting.html:24 msgid "Enable MFA" msgstr "MFAの有効化" @@ -6092,10 +6114,6 @@ msgstr "パスワードを満たす必要があります" msgid "Password strength" msgstr "パスワードの強さ" -#: users/templates/users/reset_password.html:29 -msgid "Setting" -msgstr "設定" - #: users/templates/users/reset_password.html:48 msgid "Very weak" msgstr "非常に弱い" @@ -6218,45 +6236,23 @@ msgstr "OTP無効化成功、ログインページを返す" msgid "Password invalid" msgstr "パスワード無効" -#: users/views/profile/reset.py:40 -msgid "Send reset password message" -msgstr "リセットパスワードメッセージを送信" - -#: users/views/profile/reset.py:41 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"リセットパスワードメールの成功を送信し、メールボックスにログインしてそれに従" -"う" - -#: users/views/profile/reset.py:52 -msgid "Email address invalid, please input again" -msgstr "メールアドレスが無効です。再度入力してください" - -#: users/views/profile/reset.py:58 -msgid "" -"The user is from {}, please go to the corresponding system to change the " -"password" -msgstr "" -"ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" -"さい。" - -#: users/views/profile/reset.py:84 users/views/profile/reset.py:95 +#: users/views/profile/reset.py:96 users/views/profile/reset.py:107 msgid "Token invalid or expired" msgstr "トークンが無効または期限切れ" -#: users/views/profile/reset.py:100 +#: users/views/profile/reset.py:112 msgid "User auth from {}, go there change password" msgstr "ユーザー認証ソース {}, 対応するシステムにパスワードを変更してください" -#: users/views/profile/reset.py:107 +#: users/views/profile/reset.py:119 msgid "* Your password does not meet the requirements" msgstr "* パスワードが要件を満たしていない" -#: users/views/profile/reset.py:113 +#: users/views/profile/reset.py:125 msgid "* The new password cannot be the last {} passwords" msgstr "* 新しいパスワードを最後の {} パスワードにすることはできません" -#: users/views/profile/reset.py:128 +#: users/views/profile/reset.py:140 msgid "Reset password success, return to login page" msgstr "パスワードの成功をリセットし、ログインページに戻る" @@ -6476,70 +6472,78 @@ msgid "JD Cloud" msgstr "京東雲" #: xpack/plugins/cloud/const.py:16 +msgid "KingSoft Cloud" +msgstr "金山雲" + +#: xpack/plugins/cloud/const.py:17 msgid "Tencent Cloud" msgstr "テンセント雲" -#: xpack/plugins/cloud/const.py:17 +#: xpack/plugins/cloud/const.py:18 msgid "Tencent Cloud (Lighthouse)" msgstr "テンセント雲(軽量アプリケーション)" -#: xpack/plugins/cloud/const.py:18 +#: xpack/plugins/cloud/const.py:19 msgid "VMware" msgstr "VMware" -#: xpack/plugins/cloud/const.py:19 xpack/plugins/cloud/providers/nutanix.py:13 +#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 msgid "Nutanix" msgstr "Nutanix" -#: xpack/plugins/cloud/const.py:20 +#: xpack/plugins/cloud/const.py:21 msgid "Huawei Private Cloud" msgstr "華為私有雲" -#: xpack/plugins/cloud/const.py:21 +#: xpack/plugins/cloud/const.py:22 msgid "Qingyun Private Cloud" msgstr "青雲私有雲" -#: xpack/plugins/cloud/const.py:22 +#: xpack/plugins/cloud/const.py:23 msgid "CTYun Private Cloud" msgstr "スカイウィング私有雲" -#: xpack/plugins/cloud/const.py:23 +#: xpack/plugins/cloud/const.py:24 msgid "OpenStack" msgstr "OpenStack" -#: xpack/plugins/cloud/const.py:24 +#: xpack/plugins/cloud/const.py:25 msgid "Google Cloud Platform" msgstr "谷歌雲" -#: xpack/plugins/cloud/const.py:25 +#: xpack/plugins/cloud/const.py:26 msgid "Fusion Compute" msgstr "" -#: xpack/plugins/cloud/const.py:30 +#: xpack/plugins/cloud/const.py:31 +msgid "Private IP" +msgstr "プライベートIP" + +#: xpack/plugins/cloud/const.py:36 msgid "Instance name" msgstr "インスタンス名" -#: xpack/plugins/cloud/const.py:31 +#: xpack/plugins/cloud/const.py:37 msgid "Instance name and Partial IP" msgstr "インスタンス名と部分IP" -#: xpack/plugins/cloud/const.py:36 +#: xpack/plugins/cloud/const.py:42 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:40 +#: xpack/plugins/cloud/const.py:46 msgid "Unsync" msgstr "同期していません" -#: xpack/plugins/cloud/const.py:41 +#: xpack/plugins/cloud/const.py:47 msgid "New Sync" msgstr "新しい同期" -#: xpack/plugins/cloud/const.py:42 +#: xpack/plugins/cloud/const.py:48 msgid "Synced" msgstr "同期済み" -#: xpack/plugins/cloud/const.py:43 +#: xpack/plugins/cloud/const.py:49 msgid "Released" msgstr "リリース済み" @@ -6547,75 +6551,79 @@ msgstr "リリース済み" msgid "Cloud center" msgstr "クラウドセンター" -#: xpack/plugins/cloud/models.py:30 +#: xpack/plugins/cloud/models.py:29 msgid "Provider" msgstr "プロバイダー" -#: xpack/plugins/cloud/models.py:39 +#: xpack/plugins/cloud/models.py:38 msgid "Cloud account" msgstr "クラウドアカウント" -#: xpack/plugins/cloud/models.py:41 +#: xpack/plugins/cloud/models.py:40 msgid "Test cloud account" msgstr "クラウドアカウントのテスト" -#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67 +#: xpack/plugins/cloud/models.py:84 xpack/plugins/cloud/serializers/task.py:69 msgid "Account" msgstr "アカウント" -#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38 +#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:39 msgid "Regions" msgstr "リージョン" -#: xpack/plugins/cloud/models.py:91 +#: xpack/plugins/cloud/models.py:90 msgid "Hostname strategy" msgstr "ホスト名戦略" -#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68 +#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:70 msgid "Unix admin user" msgstr "Unix adminユーザー" -#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69 +#: xpack/plugins/cloud/models.py:103 xpack/plugins/cloud/serializers/task.py:71 msgid "Windows admin user" msgstr "Windows管理者" -#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46 +#: xpack/plugins/cloud/models.py:109 xpack/plugins/cloud/serializers/task.py:47 msgid "IP network segment group" msgstr "IPネットワークセグメントグループ" -#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72 +#: xpack/plugins/cloud/models.py:112 +msgid "Sync IP type" +msgstr "同期IPタイプ" + +#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/serializers/task.py:74 msgid "Always update" msgstr "常に更新" -#: xpack/plugins/cloud/models.py:119 +#: xpack/plugins/cloud/models.py:121 msgid "Date last sync" msgstr "最終同期日" -#: xpack/plugins/cloud/models.py:130 xpack/plugins/cloud/models.py:171 +#: xpack/plugins/cloud/models.py:132 xpack/plugins/cloud/models.py:173 msgid "Sync instance task" msgstr "インスタンスの同期タスク" -#: xpack/plugins/cloud/models.py:182 xpack/plugins/cloud/models.py:230 +#: xpack/plugins/cloud/models.py:184 xpack/plugins/cloud/models.py:232 msgid "Date sync" msgstr "日付の同期" -#: xpack/plugins/cloud/models.py:186 +#: xpack/plugins/cloud/models.py:188 msgid "Sync instance task execution" msgstr "インスタンスタスクの同期実行" -#: xpack/plugins/cloud/models.py:210 +#: xpack/plugins/cloud/models.py:212 msgid "Sync task" msgstr "同期タスク" -#: xpack/plugins/cloud/models.py:214 +#: xpack/plugins/cloud/models.py:216 msgid "Sync instance task history" msgstr "インスタンスタスク履歴の同期" -#: xpack/plugins/cloud/models.py:217 +#: xpack/plugins/cloud/models.py:219 msgid "Instance" msgstr "インスタンス" -#: xpack/plugins/cloud/models.py:234 +#: xpack/plugins/cloud/models.py:236 msgid "Sync instance detail" msgstr "同期インスタンスの詳細" @@ -6809,11 +6817,11 @@ msgstr "華南-広州-友好ユーザー環境" msgid "CN East-Suqian" msgstr "華東-宿遷" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:65 msgid "Validity display" msgstr "有効表示" -#: xpack/plugins/cloud/serializers/account.py:65 +#: xpack/plugins/cloud/serializers/account.py:66 msgid "Provider display" msgstr "プロバイダ表示" @@ -6900,7 +6908,7 @@ msgstr "テストポート" msgid "Test timeout" msgstr "テストタイムアウト" -#: xpack/plugins/cloud/serializers/task.py:29 +#: xpack/plugins/cloud/serializers/task.py:30 msgid "" "Only instances matching the IP range will be synced.
If the instance " "contains multiple IP addresses, the first IP address that matches will be " @@ -6914,19 +6922,19 @@ msgstr "" "ドレスをランダムに一致させることを意味します。
形式はコンマ区切りの文字列" "です。例:192.168.1.0/24,10.1.1.1-10.1.1.20" -#: xpack/plugins/cloud/serializers/task.py:36 +#: xpack/plugins/cloud/serializers/task.py:37 msgid "History count" msgstr "実行回数" -#: xpack/plugins/cloud/serializers/task.py:37 +#: xpack/plugins/cloud/serializers/task.py:38 msgid "Instance count" msgstr "インスタンス数" -#: xpack/plugins/cloud/serializers/task.py:66 +#: xpack/plugins/cloud/serializers/task.py:68 msgid "Linux admin user" msgstr "Linux管理者" -#: xpack/plugins/cloud/serializers/task.py:71 +#: xpack/plugins/cloud/serializers/task.py:73 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定期的な表示" @@ -6987,7 +6995,7 @@ msgstr "ログアウトページのロゴ" msgid "Theme" msgstr "テーマ" -#: xpack/plugins/interface/models.py:43 +#: xpack/plugins/interface/models.py:43 xpack/plugins/interface/models.py:84 msgid "Interface setting" msgstr "インターフェイスの設定" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 2529242ac..40b94f6d7 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:a1e0b5e70491c6228017987091e46d14ccde03b6e56bfb330d1604240c6b3d09 -size 109554 +oid sha256:f9dbc8ef62e3746cebd07177934c7d68c373ff0ecceff4a83aeeb9e96829359b +size 108613 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 1dea004b2..dc81821c4 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: 2022-10-20 17:20+0800\n" +"POT-Creation-Date: 2022-11-10 10:55+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -29,20 +29,20 @@ msgstr "访问控制" #: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 -#: terminal/models/storage.py:26 terminal/models/task.py:16 -#: terminal/models/terminal.py:100 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:669 -#: xpack/plugins/cloud/models.py:28 +#: terminal/models/storage.py:27 terminal/models/task.py:16 +#: terminal/models/terminal.py:101 users/forms/profile.py:33 +#: users/models/group.py:15 users/models/user.py:673 +#: xpack/plugins/cloud/models.py:27 msgid "Name" msgstr "名称" #: acls/models/base.py:27 assets/models/cmd_filter.py:88 -#: assets/models/user.py:251 terminal/models/endpoint.py:90 +#: assets/models/user.py:252 terminal/models/endpoint.py:90 msgid "Priority" msgstr "优先级" #: acls/models/base.py:28 assets/models/cmd_filter.py:88 -#: assets/models/user.py:251 terminal/models/endpoint.py:91 +#: assets/models/user.py:252 terminal/models/endpoint.py:91 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -61,11 +61,11 @@ msgstr "激活中" #: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 #: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 -#: terminal/models/storage.py:29 terminal/models/terminal.py:114 +#: terminal/models/storage.py:30 terminal/models/terminal.py:115 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:706 +#: users/models/group.py:16 users/models/user.py:712 #: xpack/plugins/change_auth_plan/models/base.py:44 -#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 +#: xpack/plugins/cloud/models.py:34 xpack/plugins/cloud/models.py:118 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "备注" @@ -85,15 +85,16 @@ msgid "Login confirm" msgstr "登录复核" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37 -#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100 -#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220 -#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41 -#: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44 -#: terminal/models/sharing.py:33 terminal/notifications.py:91 -#: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:899 users/models/user.py:930 +#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:38 +#: audits/models.py:63 audits/models.py:105 audits/serializers.py:106 +#: authentication/models.py:19 authentication/models.py:54 +#: authentication/models.py:78 notifications/models/notification.py:12 +#: orgs/models.py:220 perms/models/base.py:84 rbac/builtin.py:120 +#: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:13 terminal/models/session.py:45 +#: terminal/models/sharing.py:33 terminal/notifications.py:94 +#: terminal/notifications.py:142 tickets/models/comment.py:21 users/const.py:14 +#: users/models/user.py:905 users/models/user.py:936 #: users/serializers/group.py:19 msgid "User" msgstr "用户" @@ -104,7 +105,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:93 audits/models.py:63 audits/serializers.py:51 +#: assets/models/cmd_filter.py:93 audits/models.py:64 audits/serializers.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" msgstr "动作" @@ -129,14 +130,14 @@ msgstr "系统用户" #: assets/models/asset.py:386 assets/models/authbook.py:19 #: assets/models/backup.py:31 assets/models/cmd_filter.py:42 #: assets/models/gathered_user.py:14 assets/serializers/label.py:30 -#: assets/serializers/system_user.py:268 audits/models.py:39 +#: assets/serializers/system_user.py:268 audits/models.py:40 #: authentication/models.py:66 authentication/models.py:90 #: perms/models/asset_permission.py:23 terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:46 -#: terminal/notifications.py:90 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:47 +#: terminal/notifications.py:93 #: xpack/plugins/change_auth_plan/models/asset.py:199 #: xpack/plugins/change_auth_plan/serializers/asset.py:181 -#: xpack/plugins/cloud/models.py:223 +#: xpack/plugins/cloud/models.py:225 msgid "Asset" msgstr "资产" @@ -154,13 +155,15 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:17 #: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 -#: assets/models/gathered_user.py:15 audits/models.py:121 +#: assets/models/gathered_user.py:15 audits/models.py:139 #: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 +#: authentication/models.py:260 authentication/serializers/password_mfa.py:25 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:667 -#: users/templates/users/_msg_user_created.html:12 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:102 +#: users/models/user.py:671 users/templates/users/_msg_user_created.html:12 +#: users/templates/users/forgot_password.html:51 +#: users/templates/users/forgot_password.html:98 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 #: xpack/plugins/cloud/serializers/account_attrs.py:26 @@ -183,13 +186,13 @@ msgstr "" #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 msgid "IP" msgstr "IP" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:211 #: assets/serializers/account.py:14 assets/serializers/gathered_user.py:23 -#: settings/serializers/terminal.py:7 +#: settings/serializers/terminal.py:9 msgid "Hostname" msgstr "主机名" @@ -200,7 +203,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:213 -#: assets/models/domain.py:63 assets/models/user.py:252 +#: assets/models/domain.py:63 assets/models/user.py:253 #: terminal/serializers/session.py:31 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "协议" @@ -219,7 +222,7 @@ msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" #: acls/serializers/rules/rules.py:20 -#: xpack/plugins/cloud/serializers/task.py:23 +#: xpack/plugins/cloud/serializers/task.py:24 msgid "IP address invalid: `{}`" msgstr "IP 地址无效: `{}`" @@ -243,7 +246,6 @@ msgstr "应用管理" #: applications/const.py:8 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:26 -#: xpack/plugins/change_auth_plan/models/app.py:32 msgid "Database" msgstr "数据库" @@ -251,7 +253,7 @@ msgstr "数据库" msgid "Remote app" msgstr "远程应用" -#: applications/const.py:35 +#: applications/const.py:36 msgid "Custom" msgstr "自定义" @@ -259,14 +261,15 @@ msgstr "自定义" #: assets/models/backup.py:32 assets/models/cmd_filter.py:49 #: authentication/models.py:67 authentication/models.py:95 #: perms/models/application_permission.py:28 +#: xpack/plugins/change_auth_plan/models/app.py:32 msgid "Application" msgstr "应用程序" #: applications/models/account.py:15 assets/models/authbook.py:20 -#: assets/models/cmd_filter.py:46 assets/models/user.py:342 audits/models.py:40 +#: assets/models/cmd_filter.py:46 assets/models/user.py:343 audits/models.py:41 #: authentication/models.py:83 perms/models/application_permission.py:33 #: perms/models/asset_permission.py:25 terminal/backends/command/models.py:22 -#: terminal/backends/command/serializers.py:36 terminal/models/session.py:48 +#: terminal/backends/command/serializers.py:36 terminal/models/session.py:49 #: xpack/plugins/change_auth_plan/models/app.py:36 #: xpack/plugins/change_auth_plan/models/app.py:147 #: xpack/plugins/change_auth_plan/serializers/app.py:65 @@ -274,7 +277,7 @@ msgid "System user" msgstr "系统用户" #: applications/models/account.py:17 assets/models/authbook.py:21 -#: settings/serializers/auth/cas.py:18 +#: settings/serializers/auth/cas.py:20 msgid "Version" msgstr "版本" @@ -293,7 +296,7 @@ msgstr "可以查看应用账号密码" #: applications/models/application.py:221 #: applications/serializers/application.py:101 assets/models/label.py:21 #: perms/models/application_permission.py:21 -#: perms/serializers/application/user_permission.py:33 +#: perms/serializers/application/user_permission.py:33 settings/models.py:35 #: tickets/models/ticket/apply_application.py:15 #: xpack/plugins/change_auth_plan/models/app.py:25 msgid "Category" @@ -301,10 +304,10 @@ msgstr "类别" #: applications/models/application.py:224 #: applications/serializers/application.py:103 assets/models/backup.py:49 -#: assets/models/cmd_filter.py:86 assets/models/user.py:250 +#: assets/models/cmd_filter.py:86 assets/models/user.py:251 #: authentication/models.py:70 perms/models/application_permission.py:24 #: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:58 terminal/models/storage.py:143 +#: terminal/models/storage.py:59 terminal/models/storage.py:145 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:18 #: tickets/models/ticket/general.py:273 @@ -318,8 +321,8 @@ msgstr "类型" msgid "Domain" msgstr "网域" -#: applications/models/application.py:230 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:63 +#: applications/models/application.py:230 xpack/plugins/cloud/models.py:32 +#: xpack/plugins/cloud/serializers/account.py:64 msgid "Attrs" msgstr "属性" @@ -348,22 +351,25 @@ msgstr "类型名称" #: applications/serializers/application.py:105 assets/models/asset.py:230 #: assets/models/base.py:181 assets/models/cluster.py:26 -#: assets/models/domain.py:26 assets/models/gathered_user.py:19 -#: assets/models/group.py:22 assets/models/label.py:25 -#: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28 -#: assets/serializers/cmd_filter.py:48 common/db/models.py:114 +#: assets/models/cmd_filter.py:53 assets/models/domain.py:26 +#: assets/models/gathered_user.py:19 assets/models/group.py:22 +#: assets/models/label.py:25 assets/serializers/account.py:18 +#: assets/serializers/cmd_filter.py:28 assets/serializers/cmd_filter.py:48 +#: authentication/models.py:22 common/db/models.py:116 #: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 #: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:931 -#: xpack/plugins/cloud/models.py:125 +#: users/models/group.py:18 users/models/user.py:937 +#: xpack/plugins/change_auth_plan/models/base.py:45 +#: xpack/plugins/cloud/models.py:127 msgid "Date created" msgstr "创建日期" #: applications/serializers/application.py:106 assets/models/base.py:182 -#: assets/models/gathered_user.py:20 assets/serializers/account.py:21 -#: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49 -#: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40 -#: orgs/models.py:224 +#: assets/models/cmd_filter.py:54 assets/models/gathered_user.py:20 +#: assets/serializers/account.py:21 assets/serializers/cmd_filter.py:29 +#: assets/serializers/cmd_filter.py:49 common/db/models.py:117 +#: common/mixins/models.py:51 ops/models/adhoc.py:40 orgs/models.py:224 +#: xpack/plugins/change_auth_plan/models/base.py:46 msgid "Date updated" msgstr "更新日期" @@ -382,13 +388,14 @@ msgid "Cluster" msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:15 +#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:16 +#: settings/serializers/auth/sms.py:67 terminal/models/endpoint.py:15 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" #: applications/serializers/attrs/application_category/db.py:12 +#: applications/serializers/attrs/application_type/clickhouse.py:10 #: applications/serializers/attrs/application_type/mongodb.py:10 #: applications/serializers/attrs/application_type/mysql.py:10 #: applications/serializers/attrs/application_type/mysql_workbench.py:22 @@ -397,13 +404,13 @@ msgstr "主机" #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 #: assets/models/asset.py:214 assets/models/domain.py:62 -#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:66 +#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" #: applications/serializers/attrs/application_category/db.py:16 -#: settings/serializers/email.py:36 +#: settings/serializers/email.py:37 msgid "Use SSL" msgstr "使用 SSL" @@ -598,12 +605,12 @@ msgstr "主机名原始" #: assets/models/asset.py:215 assets/serializers/account.py:16 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41 -#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43 +#: xpack/plugins/cloud/models.py:106 xpack/plugins/cloud/serializers/task.py:44 msgid "Protocols" msgstr "协议组" #: assets/models/asset.py:218 assets/models/cmd_filter.py:38 -#: assets/models/user.py:242 perms/models/asset_permission.py:24 +#: assets/models/user.py:243 perms/models/asset_permission.py:24 #: xpack/plugins/change_auth_plan/models/asset.py:43 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" @@ -616,11 +623,11 @@ msgid "Is active" msgstr "激活" #: assets/models/asset.py:222 assets/models/cluster.py:19 -#: assets/models/user.py:239 assets/models/user.py:394 +#: assets/models/user.py:240 assets/models/user.py:395 msgid "Admin user" msgstr "特权用户" -#: assets/models/asset.py:225 +#: assets/models/asset.py:225 xpack/plugins/cloud/const.py:32 msgid "Public IP" msgstr "公网IP" @@ -635,11 +642,11 @@ msgstr "标签管理" #: assets/models/asset.py:229 assets/models/base.py:183 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:56 #: assets/models/cmd_filter.py:103 assets/models/group.py:21 -#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71 -#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:714 +#: common/db/models.py:114 common/mixins/models.py:49 orgs/models.py:71 +#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:720 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 -#: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30 +#: xpack/plugins/cloud/models.py:124 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -692,7 +699,7 @@ msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" #: assets/models/backup.py:30 perms/models/base.py:54 -#: settings/serializers/terminal.py:12 +#: settings/serializers/terminal.py:14 msgid "All" msgstr "全部" @@ -717,8 +724,8 @@ msgstr "手动触发" msgid "Timing trigger" msgstr "定时触发" -#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31 -#: perms/models/base.py:89 terminal/models/session.py:58 +#: assets/models/backup.py:105 audits/models.py:45 ops/models/command.py:31 +#: perms/models/base.py:89 terminal/models/session.py:59 #: tickets/models/ticket/apply_application.py:29 #: tickets/models/ticket/apply_asset.py:23 #: xpack/plugins/change_auth_plan/models/base.py:112 @@ -746,17 +753,17 @@ msgstr "账号备份快照" msgid "Trigger mode" msgstr "触发模式" -#: assets/models/backup.py:119 audits/models.py:127 +#: assets/models/backup.py:119 audits/models.py:145 #: terminal/models/sharing.py:108 #: xpack/plugins/change_auth_plan/models/base.py:201 #: xpack/plugins/change_auth_plan/serializers/app.py:66 #: xpack/plugins/change_auth_plan/serializers/asset.py:180 -#: xpack/plugins/cloud/models.py:179 +#: xpack/plugins/cloud/models.py:181 msgid "Reason" msgstr "原因" -#: assets/models/backup.py:121 audits/serializers.py:82 -#: audits/serializers.py:97 ops/models/adhoc.py:260 +#: assets/models/backup.py:121 audits/serializers.py:88 +#: audits/serializers.py:103 ops/models/adhoc.py:260 #: terminal/serializers/session.py:36 #: xpack/plugins/change_auth_plan/models/base.py:202 #: xpack/plugins/change_auth_plan/serializers/app.py:67 @@ -778,10 +785,10 @@ msgstr "未知" msgid "Ok" msgstr "成功" -#: assets/models/base.py:32 audits/models.py:118 +#: assets/models/base.py:32 audits/models.py:136 #: xpack/plugins/change_auth_plan/serializers/app.py:88 #: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:35 +#: xpack/plugins/cloud/const.py:41 msgid "Failed" msgstr "失败" @@ -795,10 +802,10 @@ msgstr "校验日期" #: assets/models/base.py:177 assets/serializers/base.py:15 #: assets/serializers/base.py:37 assets/serializers/system_user.py:29 -#: audits/signal_handlers.py:50 authentication/confirm/password.py:9 +#: audits/signal_handlers.py:58 authentication/confirm/password.py:9 #: authentication/forms.py:32 #: authentication/templates/authentication/login.html:228 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:46 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 #: users/forms/profile.py:22 users/serializers/user.py:94 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 @@ -832,7 +839,10 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:689 +#: assets/models/cluster.py:22 authentication/serializers/password_mfa.py:29 +#: users/forms/profile.py:104 users/models/user.py:693 +#: users/templates/users/forgot_password.html:59 +#: users/templates/users/forgot_password.html:103 msgid "Phone" msgstr "手机" @@ -858,7 +868,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:916 +#: users/models/user.py:922 msgid "System" msgstr "系统" @@ -867,7 +877,7 @@ msgid "Default Cluster" msgstr "默认Cluster" #: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:675 +#: users/models/group.py:31 users/models/user.py:679 msgid "User group" msgstr "用户组" @@ -880,7 +890,7 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:72 ops/models/command.py:26 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:55 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:56 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 msgid "Command" @@ -998,7 +1008,7 @@ msgid "Parent key" msgstr "ssh私钥" #: assets/models/node.py:566 assets/serializers/system_user.py:267 -#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70 +#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers/task.py:72 msgid "Node" msgstr "节点" @@ -1006,78 +1016,78 @@ msgstr "节点" msgid "Can match node" msgstr "可以匹配节点" -#: assets/models/user.py:233 +#: assets/models/user.py:234 msgid "Automatic managed" msgstr "托管密码" -#: assets/models/user.py:234 +#: assets/models/user.py:235 msgid "Manually input" msgstr "手动输入" -#: assets/models/user.py:238 +#: assets/models/user.py:239 msgid "Common user" msgstr "普通用户" -#: assets/models/user.py:241 +#: assets/models/user.py:242 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:244 assets/serializers/domain.py:30 +#: assets/models/user.py:245 assets/serializers/domain.py:30 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 #: xpack/plugins/change_auth_plan/models/asset.py:39 msgid "Assets" msgstr "资产" -#: assets/models/user.py:248 users/apps.py:9 +#: assets/models/user.py:249 users/apps.py:9 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:249 +#: assets/models/user.py:250 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:253 +#: assets/models/user.py:254 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:254 +#: assets/models/user.py:255 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:255 +#: assets/models/user.py:256 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:256 +#: assets/models/user.py:257 msgid "Login mode" msgstr "认证方式" -#: assets/models/user.py:257 +#: assets/models/user.py:258 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:258 assets/serializers/system_user.py:37 +#: assets/models/user.py:259 assets/serializers/system_user.py:37 #: authentication/models.py:52 msgid "Token" msgstr "Token" -#: assets/models/user.py:259 +#: assets/models/user.py:260 msgid "Home" msgstr "家目录" -#: assets/models/user.py:260 +#: assets/models/user.py:261 msgid "System groups" msgstr "用户组" -#: assets/models/user.py:263 +#: assets/models/user.py:264 msgid "User switch" msgstr "用户切换" -#: assets/models/user.py:264 +#: assets/models/user.py:265 msgid "Switch from" msgstr "切换自" -#: assets/models/user.py:344 +#: assets/models/user.py:345 msgid "Can match system user" msgstr "可以匹配系统用户" @@ -1152,8 +1162,8 @@ msgstr "CPU信息" msgid "Actions" msgstr "动作" -#: assets/serializers/backup.py:31 ops/mixin.py:106 ops/mixin.py:147 -#: settings/serializers/auth/ldap.py:65 +#: assets/serializers/backup.py:31 ops/mixin.py:26 ops/mixin.py:106 +#: ops/mixin.py:147 settings/serializers/auth/ldap.py:66 #: xpack/plugins/change_auth_plan/serializers/base.py:43 msgid "Periodic perform" msgstr "定时执行" @@ -1163,7 +1173,7 @@ msgstr "定时执行" msgid "Currently only mail sending is supported" msgstr "当前只支持邮件发送" -#: assets/serializers/base.py:16 users/models/user.py:697 +#: assets/serializers/base.py:16 users/models/user.py:703 msgid "Private key" msgstr "ssh私钥" @@ -1411,152 +1421,168 @@ msgstr "没有匹配到资产,结束任务" msgid "Audits" msgstr "日志审计" -#: audits/models.py:27 audits/models.py:59 +#: audits/backends/db.py:12 +msgid "The text content is too long. Use Elasticsearch to store operation logs" +msgstr "文字内容太长。请使用 Elasticsearch 存储操作日志" + +#: audits/backends/db.py:24 audits/backends/db.py:26 +msgid "Tips" +msgstr "提示" + +#: audits/handler.py:126 +msgid "Yes" +msgstr "是" + +#: audits/handler.py:126 +msgid "No" +msgstr "否" + +#: audits/models.py:28 audits/models.py:60 #: authentication/templates/authentication/_access_key_modal.html:65 #: rbac/tree.py:226 msgid "Delete" msgstr "删除" -#: audits/models.py:28 +#: audits/models.py:29 msgid "Upload" msgstr "上传文件" -#: audits/models.py:29 +#: audits/models.py:30 msgid "Download" msgstr "下载文件" -#: audits/models.py:30 +#: audits/models.py:31 msgid "Rmdir" msgstr "删除目录" -#: audits/models.py:31 +#: audits/models.py:32 msgid "Rename" msgstr "重命名" -#: audits/models.py:32 +#: audits/models.py:33 msgid "Mkdir" msgstr "创建目录" -#: audits/models.py:33 +#: audits/models.py:34 msgid "Symlink" msgstr "建立软链接" -#: audits/models.py:38 audits/models.py:66 audits/models.py:89 -#: terminal/models/session.py:51 terminal/models/sharing.py:96 +#: audits/models.py:39 audits/models.py:67 audits/models.py:107 +#: terminal/models/session.py:52 terminal/models/sharing.py:96 msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:41 +#: audits/models.py:42 msgid "Operate" msgstr "操作" -#: audits/models.py:42 +#: audits/models.py:43 msgid "Filename" msgstr "文件名" -#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:104 +#: audits/models.py:44 audits/models.py:135 terminal/models/sharing.py:104 #: tickets/views/approve.py:115 #: xpack/plugins/change_auth_plan/serializers/app.py:87 #: xpack/plugins/change_auth_plan/serializers/asset.py:198 msgid "Success" msgstr "成功" -#: audits/models.py:47 +#: audits/models.py:48 msgid "File transfer log" msgstr "文件管理" -#: audits/models.py:56 +#: audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:223 msgid "Create" msgstr "创建" -#: audits/models.py:57 rbac/tree.py:224 +#: audits/models.py:58 rbac/tree.py:224 msgid "View" msgstr "查看" -#: audits/models.py:58 rbac/tree.py:225 templates/_csv_import_export.html:18 +#: audits/models.py:59 rbac/tree.py:225 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/models.py:64 audits/serializers.py:63 +#: audits/models.py:65 audits/serializers.py:69 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:65 +#: audits/models.py:66 msgid "Resource" msgstr "资源" -#: audits/models.py:67 audits/models.py:90 +#: audits/models.py:68 audits/models.py:108 #: terminal/backends/command/serializers.py:40 msgid "Datetime" msgstr "日期" -#: audits/models.py:82 +#: audits/models.py:100 msgid "Operate log" msgstr "操作日志" -#: audits/models.py:88 +#: audits/models.py:106 msgid "Change by" msgstr "修改者" -#: audits/models.py:96 +#: audits/models.py:114 msgid "Password change log" msgstr "改密日志" -#: audits/models.py:111 +#: audits/models.py:129 msgid "Disabled" msgstr "禁用" -#: audits/models.py:112 settings/models.py:37 +#: audits/models.py:130 settings/models.py:37 msgid "Enabled" msgstr "启用" -#: audits/models.py:113 +#: audits/models.py:131 msgid "-" msgstr "-" -#: audits/models.py:122 +#: audits/models.py:140 msgid "Login type" msgstr "登录方式" -#: audits/models.py:123 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:141 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:124 +#: audits/models.py:142 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "登录城市" -#: audits/models.py:125 audits/serializers.py:44 +#: audits/models.py:143 audits/serializers.py:44 msgid "User agent" msgstr "用户代理" -#: audits/models.py:126 +#: audits/models.py:144 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:692 +#: users/forms/profile.py:65 users/models/user.py:696 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:128 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:175 -#: xpack/plugins/cloud/models.py:227 +#: audits/models.py:146 terminal/models/status.py:33 +#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:177 +#: xpack/plugins/cloud/models.py:229 msgid "Status" msgstr "状态" -#: audits/models.py:129 +#: audits/models.py:147 msgid "Date login" msgstr "登录日期" -#: audits/models.py:130 audits/serializers.py:46 +#: audits/models.py:148 audits/serializers.py:46 msgid "Authentication backend" msgstr "认证方式" -#: audits/models.py:169 +#: audits/models.py:187 msgid "User login log" msgstr "用户登录日志" @@ -1576,234 +1602,67 @@ msgstr "MFA名称" msgid "Reason display" msgstr "原因描述" -#: audits/serializers.py:84 +#: audits/serializers.py:90 msgid "Hosts display" msgstr "主机名称" -#: audits/serializers.py:96 ops/models/command.py:27 -#: xpack/plugins/cloud/models.py:173 +#: audits/serializers.py:102 ops/models/command.py:27 +#: xpack/plugins/cloud/models.py:175 msgid "Result" msgstr "结果" -#: audits/serializers.py:98 terminal/serializers/storage.py:157 +#: audits/serializers.py:104 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:99 +#: audits/serializers.py:105 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:101 +#: audits/serializers.py:107 msgid "Run as display" msgstr "运行用户名称" -#: audits/serializers.py:102 authentication/models.py:81 +#: audits/serializers.py:108 authentication/models.py:81 #: rbac/serializers/rolebinding.py:21 msgid "User display" msgstr "用户名称" -#: audits/signal_handlers.py:49 +#: audits/signal_handlers.py:57 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:59 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:52 +#: audits/signal_handlers.py:60 msgid "Auth Token" msgstr "认证令牌" -#: audits/signal_handlers.py:53 authentication/notifications.py:73 +#: audits/signal_handlers.py:61 authentication/notifications.py:73 #: authentication/views/login.py:73 authentication/views/wecom.py:178 -#: notifications/backends/__init__.py:11 users/models/user.py:728 +#: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 +#: users/models/user.py:734 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers.py:54 authentication/views/feishu.py:144 +#: audits/signal_handlers.py:62 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: users/models/user.py:730 +#: settings/serializers/auth/feishu.py:10 users/models/user.py:736 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 +#: audits/signal_handlers.py:63 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 -#: users/models/user.py:729 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:735 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers.py:56 authentication/models.py:267 +#: audits/signal_handlers.py:64 authentication/models.py:267 msgid "Temporary token" msgstr "临时密码" -#: audits/signal_handlers.py:68 -msgid "User and Group" -msgstr "用户与用户组" - -#: audits/signal_handlers.py:69 -#, python-brace-format -msgid "{User} JOINED {UserGroup}" -msgstr "{User} 加入 {UserGroup}" - -#: audits/signal_handlers.py:70 -#, python-brace-format -msgid "{User} LEFT {UserGroup}" -msgstr "{User} 离开 {UserGroup}" - -#: audits/signal_handlers.py:73 -msgid "Asset and SystemUser" -msgstr "资产与系统用户" - -#: audits/signal_handlers.py:74 -#, python-brace-format -msgid "{Asset} ADD {SystemUser}" -msgstr "{Asset} 添加 {SystemUser}" - -#: audits/signal_handlers.py:75 -#, python-brace-format -msgid "{Asset} REMOVE {SystemUser}" -msgstr "{Asset} 移除 {SystemUser}" - -#: audits/signal_handlers.py:78 -msgid "Node and Asset" -msgstr "节点与资产" - -#: audits/signal_handlers.py:79 -#, python-brace-format -msgid "{Node} ADD {Asset}" -msgstr "{Node} 添加 {Asset}" - -#: audits/signal_handlers.py:80 -#, python-brace-format -msgid "{Node} REMOVE {Asset}" -msgstr "{Node} 移除 {Asset}" - -#: audits/signal_handlers.py:83 -msgid "User asset permissions" -msgstr "用户资产授权" - -#: audits/signal_handlers.py:84 -#, python-brace-format -msgid "{AssetPermission} ADD {User}" -msgstr "{AssetPermission} 添加 {User}" - -#: audits/signal_handlers.py:85 -#, python-brace-format -msgid "{AssetPermission} REMOVE {User}" -msgstr "{AssetPermission} 移除 {User}" - -#: audits/signal_handlers.py:88 -msgid "User group asset permissions" -msgstr "用户组资产授权" - -#: audits/signal_handlers.py:89 -#, python-brace-format -msgid "{AssetPermission} ADD {UserGroup}" -msgstr "{AssetPermission} 添加 {UserGroup}" - -#: audits/signal_handlers.py:90 -#, python-brace-format -msgid "{AssetPermission} REMOVE {UserGroup}" -msgstr "{AssetPermission} 移除 {UserGroup}" - -#: audits/signal_handlers.py:93 perms/models/asset_permission.py:29 -msgid "Asset permission" -msgstr "资产授权" - -#: audits/signal_handlers.py:94 -#, python-brace-format -msgid "{AssetPermission} ADD {Asset}" -msgstr "{AssetPermission} 添加 {Asset}" - -#: audits/signal_handlers.py:95 -#, python-brace-format -msgid "{AssetPermission} REMOVE {Asset}" -msgstr "{AssetPermission} 移除 {Asset}" - -#: audits/signal_handlers.py:98 -msgid "Node permission" -msgstr "节点授权" - -#: audits/signal_handlers.py:99 -#, python-brace-format -msgid "{AssetPermission} ADD {Node}" -msgstr "{AssetPermission} 添加 {Node}" - -#: audits/signal_handlers.py:100 -#, python-brace-format -msgid "{AssetPermission} REMOVE {Node}" -msgstr "{AssetPermission} 移除 {Node}" - -#: audits/signal_handlers.py:103 -msgid "Asset permission and SystemUser" -msgstr "资产授权与系统用户" - -#: audits/signal_handlers.py:104 -#, python-brace-format -msgid "{AssetPermission} ADD {SystemUser}" -msgstr "{AssetPermission} 添加 {SystemUser}" - -#: audits/signal_handlers.py:105 -#, python-brace-format -msgid "{AssetPermission} REMOVE {SystemUser}" -msgstr "{AssetPermission} 移除 {SystemUser}" - -#: audits/signal_handlers.py:108 -msgid "User application permissions" -msgstr "用户应用授权" - -#: audits/signal_handlers.py:109 -#, python-brace-format -msgid "{ApplicationPermission} ADD {User}" -msgstr "{ApplicationPermission} 添加 {User}" - -#: audits/signal_handlers.py:110 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {User}" -msgstr "{ApplicationPermission} 移除 {User}" - -#: audits/signal_handlers.py:113 -msgid "User group application permissions" -msgstr "用户组应用授权" - -#: audits/signal_handlers.py:114 -#, python-brace-format -msgid "{ApplicationPermission} ADD {UserGroup}" -msgstr "{ApplicationPermission} 添加 {UserGroup}" - -#: audits/signal_handlers.py:115 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {UserGroup}" -msgstr "{ApplicationPermission} 移除 {UserGroup}" - -#: audits/signal_handlers.py:118 perms/models/application_permission.py:38 -msgid "Application permission" -msgstr "应用授权" - -#: audits/signal_handlers.py:119 -#, python-brace-format -msgid "{ApplicationPermission} ADD {Application}" -msgstr "{ApplicationPermission} 添加 {Application}" - -#: audits/signal_handlers.py:120 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {Application}" -msgstr "{ApplicationPermission} 移除 {Application}" - -#: audits/signal_handlers.py:123 -msgid "Application permission and SystemUser" -msgstr "应用授权与系统用户" - -#: audits/signal_handlers.py:124 -#, python-brace-format -msgid "{ApplicationPermission} ADD {SystemUser}" -msgstr "{ApplicationPermission} 添加 {SystemUser}" - -#: audits/signal_handlers.py:125 -#, python-brace-format -msgid "{ApplicationPermission} REMOVE {SystemUser}" -msgstr "{ApplicationPermission} 移除 {SystemUser}" - #: authentication/api/confirm.py:40 msgid "This action require verify your MFA" msgstr "此操作需要验证您的 MFA" @@ -1812,7 +1671,34 @@ msgstr "此操作需要验证您的 MFA" msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" -#: authentication/apps.py:7 +#: authentication/api/password.py:29 terminal/api/session.py:224 +#: users/views/profile/reset.py:74 +msgid "User does not exist: {}" +msgstr "用户不存在: {}" + +#: authentication/api/password.py:29 +msgid "No user matched" +msgstr "没有匹配到用户" + +#: authentication/api/password.py:33 +msgid "" +"The user is from {}, please go to the corresponding system to change the " +"password" +msgstr "用户来自 {} 请去相应系统修改密码" + +#: authentication/api/password.py:59 +#: authentication/templates/authentication/login.html:256 +#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:34 +msgid "Forgot password" +msgstr "忘记密码" + +#: authentication/apps.py:7 settings/serializers/auth/base.py:10 +#: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 +#: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 +#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 +#: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" msgstr "认证" @@ -2060,6 +1946,18 @@ msgstr "动态码" msgid "Please input security code" msgstr "请输入动态安全码" +#: authentication/mfa/custom.py:20 +msgid "MFA Custom code invalid" +msgstr "自定义 MFA 验证码校验失败" + +#: authentication/mfa/custom.py:26 +msgid "MFA custom verification code" +msgstr "自定义 MFA 验证码" + +#: authentication/mfa/custom.py:56 +msgid "MFA custom global enabled, cannot disable" +msgstr "自定义 MFA 全局开启,无法被禁用" + #: authentication/mfa/otp.py:7 msgid "OTP code invalid, or server time error" msgstr "虚拟 MFA 验证码错误,或者服务器端时间不对" @@ -2092,7 +1990,7 @@ msgstr "Radius MFA 全局开启,无法被禁用" msgid "SMS verify code invalid" msgstr "短信验证码校验失败" -#: authentication/mfa/sms.py:12 +#: authentication/mfa/sms.py:12 settings/serializers/auth/sms.py:27 msgid "SMS" msgstr "短信" @@ -2138,13 +2036,13 @@ msgstr "SSO token" #: authentication/models.py:72 authentication/models.py:261 #: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:17 +#: settings/serializers/auth/radius.py:19 msgid "Secret" msgstr "密钥" #: authentication/models.py:74 authentication/models.py:264 #: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:711 +#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:717 msgid "Date expired" msgstr "失效日期" @@ -2209,7 +2107,7 @@ msgid "binding reminder" msgstr "绑定提醒" #: authentication/serializers/connection_token.py:23 -#: xpack/plugins/cloud/models.py:34 +#: xpack/plugins/cloud/models.py:33 msgid "Validity" msgstr "有效" @@ -2221,6 +2119,21 @@ msgstr "过期时间" msgid "Asset or application required" msgstr "资产或应用必填" +#: authentication/serializers/password_mfa.py:25 +#: authentication/serializers/password_mfa.py:29 +#: authentication/serializers/password_mfa.py:33 +#: users/templates/users/forgot_password.html:95 +msgid "The {} cannot be empty" +msgstr "{} 不能为空" + +#: authentication/serializers/password_mfa.py:33 +#: notifications/backends/__init__.py:10 settings/serializers/email.py:19 +#: settings/serializers/email.py:50 users/forms/profile.py:103 +#: users/models/user.py:675 users/templates/users/forgot_password.html:55 +#: users/templates/users/forgot_password.html:108 +msgid "Email" +msgstr "邮件" + #: authentication/serializers/token.py:79 #: perms/serializers/application/permission.py:20 #: perms/serializers/application/permission.py:41 @@ -2247,7 +2160,7 @@ msgid "ID" msgstr "ID" #: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:93 terminal/notifications.py:141 +#: terminal/notifications.py:96 terminal/notifications.py:144 msgid "Date" msgstr "日期" @@ -2256,14 +2169,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: settings/serializers/security.py:39 users/models/user.py:556 +#: settings/serializers/security.py:39 users/models/user.py:557 #: users/serializers/profile.py:116 users/templates/users/mfa_setting.html:61 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:557 users/serializers/profile.py:117 +#: users/models/user.py:558 users/serializers/profile.py:117 #: users/templates/users/mfa_setting.html:26 #: users/templates/users/mfa_setting.html:68 msgid "Enable" @@ -2284,7 +2197,6 @@ msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" #: authentication/templates/authentication/_captcha_field.html:15 -#: users/forms/profile.py:103 msgid "Captcha" msgstr "验证码" @@ -2310,9 +2222,10 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_different_city.html:3 #: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3 +#: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:402 ops/tasks.py:145 ops/tasks.py:148 +#: jumpserver/conf.py:406 ops/tasks.py:145 ops/tasks.py:148 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2365,6 +2278,21 @@ msgstr "这个链接有效期1小时, 超过时间您可以" msgid "request new one" msgstr "重新申请" +#: authentication/templates/authentication/_msg_reset_password_code.html:12 +#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 +#: users/forms/profile.py:105 users/templates/users/forgot_password.html:63 +msgid "Verify code" +msgstr "验证码" + +#: authentication/templates/authentication/_msg_reset_password_code.html:15 +msgid "" +"Copy the verification code to the Reset Password page to reset the password." +msgstr "将验证码复制到重置密码页面,重置密码。" + +#: authentication/templates/authentication/_msg_reset_password_code.html:18 +msgid "The validity period of the verification code is one minute" +msgstr "验证码有效期为 1 分钟" + #: authentication/templates/authentication/_msg_rest_password_success.html:5 msgid "Your password has just been successfully updated" msgstr "你的密码刚刚成功更新" @@ -2404,12 +2332,6 @@ msgstr "取消" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:16 -#: users/templates/users/forgot_password.html:17 -msgid "Forgot password" -msgstr "忘记密码" - #: authentication/templates/authentication/login.html:264 #: templates/_header_bar.html:89 msgid "Login" @@ -2452,7 +2374,7 @@ msgid "Copy success" msgstr "复制成功" #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 -#: xpack/plugins/cloud/const.py:26 +#: xpack/plugins/cloud/const.py:27 msgid "LAN" msgstr "局域网" @@ -2600,35 +2522,35 @@ msgstr "%(name)s 更新成功" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:80 +#: common/db/fields.py:81 msgid "Marshal dict data to char field" msgstr "编码 dict 为 char" -#: common/db/fields.py:84 +#: common/db/fields.py:85 msgid "Marshal dict data to text field" msgstr "编码 dict 为 text" -#: common/db/fields.py:96 +#: common/db/fields.py:97 msgid "Marshal list data to char field" msgstr "编码 list 为 char" -#: common/db/fields.py:100 +#: common/db/fields.py:101 msgid "Marshal list data to text field" msgstr "编码 list 为 text" -#: common/db/fields.py:104 +#: common/db/fields.py:105 msgid "Marshal data to char field" msgstr "编码数据为 char" -#: common/db/fields.py:108 +#: common/db/fields.py:109 msgid "Marshal data to text field" msgstr "编码数据为 text" -#: common/db/fields.py:150 +#: common/db/fields.py:151 msgid "Encrypt field using Secret Key" msgstr "加密的字段" -#: common/db/models.py:113 +#: common/db/models.py:115 msgid "Updated by" msgstr "更新人" @@ -2698,6 +2620,14 @@ msgstr "仅导出选择项" msgid "Export filtered: %s" msgstr "导出搜素: %s" +#: common/plugins/es.py:28 +msgid "Invalid elasticsearch config" +msgstr "无效的 Elasticsearch 配置" + +#: common/plugins/es.py:33 +msgid "Not Support Elasticsearch8" +msgstr "不支持 Elasticsearch8" + #: common/sdk/im/exceptions.py:23 msgid "Network error, please contact system administrator" msgstr "网络错误,请联系系统管理员" @@ -2742,15 +2672,15 @@ msgstr "短信服务商不支持:{}" msgid "SMS verification code signature or template invalid" msgstr "短信验证码签名或模版无效" -#: common/sdk/sms/utils.py:15 +#: common/sdk/sms/exceptions.py:8 msgid "The verification code has expired. Please resend it" msgstr "验证码已过期,请重新发送" -#: common/sdk/sms/utils.py:20 +#: common/sdk/sms/exceptions.py:13 msgid "The verification code is incorrect" msgstr "验证码错误" -#: common/sdk/sms/utils.py:25 +#: common/sdk/sms/exceptions.py:18 msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" @@ -2770,11 +2700,11 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: jumpserver/conf.py:401 +#: jumpserver/conf.py:405 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:403 +#: jumpserver/conf.py:407 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -2813,15 +2743,26 @@ msgstr "" msgid "Notifications" msgstr "通知" -#: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:671 -msgid "Email" -msgstr "邮件" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "站内信" +#: notifications/models/notification.py:14 +msgid "receive backend" +msgstr "消息后端" + +#: notifications/models/notification.py:17 +msgid "User message" +msgstr "用户消息" + +#: notifications/models/notification.py:20 +msgid "{} subscription" +msgstr "{} 订阅" + +#: notifications/models/notification.py:32 +msgid "System message" +msgstr "系统信息" + #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -2835,12 +2776,12 @@ msgid "App ops" msgstr "作业中心" #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 -#: settings/serializers/auth/ldap.py:72 +#: settings/serializers/auth/ldap.py:73 msgid "Cycle perform" msgstr "周期执行" #: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150 -#: settings/serializers/auth/ldap.py:69 +#: settings/serializers/auth/ldap.py:70 msgid "Regularly perform" msgstr "定期执行" @@ -3029,7 +2970,7 @@ msgstr "组织管理" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85 #: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48 -#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 +#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 msgid "Organization" msgstr "组织" @@ -3047,7 +2988,7 @@ msgid "Can view all joined org" msgstr "可以查看所有加入的组织" #: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:679 +#: users/models/user.py:683 msgid "Role" msgstr "角色" @@ -3063,6 +3004,10 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" +#: perms/models/application_permission.py:38 +msgid "Application permission" +msgstr "应用授权" + #: perms/models/application_permission.py:111 msgid "Permed application" msgstr "授权的应用" @@ -3079,6 +3024,10 @@ msgstr "可以查看用户授权的应用" msgid "Can view usergroup apps" msgstr "可以查看用户组授权的应用" +#: perms/models/asset_permission.py:29 +msgid "Asset permission" +msgstr "资产授权" + #: perms/models/asset_permission.py:134 msgid "Ungrouped" msgstr "未分组" @@ -3318,19 +3267,15 @@ msgstr "Web终端" msgid "Can view file manager" msgstr "文件管理" -#: rbac/models/permission.py:26 -msgid "Permission" -msgstr "权限" - -#: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: settings/serializers/auth/oauth2.py:35 -msgid "Scope" -msgstr "范围" - -#: rbac/models/role.py:34 +#: rbac/models/permission.py:26 rbac/models/role.py:34 msgid "Permissions" msgstr "授权" +#: rbac/models/role.py:31 rbac/models/rolebinding.py:38 +#: settings/serializers/auth/oauth2.py:37 +msgid "Scope" +msgstr "范围" + #: rbac/models/role.py:36 msgid "Built-in" msgstr "内置" @@ -3506,6 +3451,10 @@ msgstr "测试手机号 该字段是必填项。" msgid "Settings" msgstr "系统设置" +#: settings/models.py:36 +msgid "Encrypted" +msgstr "加密的" + #: settings/models.py:158 msgid "Can change email setting" msgstr "邮件设置" @@ -3546,122 +3495,134 @@ msgstr "终端设置" msgid "Can change other setting" msgstr "其它设置" -#: settings/serializers/auth/base.py:10 +#: settings/serializers/auth/base.py:10 settings/serializers/basic.py:27 +msgid "Basic" +msgstr "基本" + +#: settings/serializers/auth/base.py:12 msgid "CAS Auth" msgstr "CAS 认证" -#: settings/serializers/auth/base.py:11 +#: settings/serializers/auth/base.py:13 msgid "OPENID Auth" msgstr "OIDC 认证" -#: settings/serializers/auth/base.py:12 +#: settings/serializers/auth/base.py:14 msgid "RADIUS Auth" msgstr "RADIUS 认证" -#: settings/serializers/auth/base.py:13 +#: settings/serializers/auth/base.py:15 msgid "DingTalk Auth" msgstr "钉钉 认证" -#: settings/serializers/auth/base.py:14 +#: settings/serializers/auth/base.py:16 msgid "FeiShu Auth" msgstr "飞书 认证" -#: settings/serializers/auth/base.py:15 +#: settings/serializers/auth/base.py:17 msgid "WeCom Auth" msgstr "企业微信 认证" -#: settings/serializers/auth/base.py:16 +#: settings/serializers/auth/base.py:18 msgid "SSO Auth" msgstr "SSO Token 认证" -#: settings/serializers/auth/base.py:17 +#: settings/serializers/auth/base.py:19 msgid "SAML2 Auth" msgstr "SAML2 认证" -#: settings/serializers/auth/base.py:20 settings/serializers/basic.py:36 +#: settings/serializers/auth/base.py:22 settings/serializers/basic.py:38 msgid "Forgot password url" msgstr "忘记密码 URL" -#: settings/serializers/auth/base.py:26 +#: settings/serializers/auth/base.py:28 msgid "Enable login redirect msg" msgstr "启用登录跳转提示" #: settings/serializers/auth/cas.py:10 +msgid "CAS" +msgstr "CAS" + +#: settings/serializers/auth/cas.py:12 msgid "Enable CAS Auth" msgstr "启用 CAS 认证" -#: settings/serializers/auth/cas.py:11 settings/serializers/auth/oidc.py:48 +#: settings/serializers/auth/cas.py:13 settings/serializers/auth/oidc.py:49 msgid "Server url" msgstr "服务端地址" -#: settings/serializers/auth/cas.py:14 +#: settings/serializers/auth/cas.py:16 msgid "Proxy server url" msgstr "回调地址" -#: settings/serializers/auth/cas.py:16 settings/serializers/auth/oauth2.py:53 -#: settings/serializers/auth/saml2.py:32 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同步注销" -#: settings/serializers/auth/cas.py:21 +#: settings/serializers/auth/cas.py:23 msgid "Username attr" msgstr "用户名属性" -#: settings/serializers/auth/cas.py:24 +#: settings/serializers/auth/cas.py:26 msgid "Enable attributes map" msgstr "启用属性映射" -#: settings/serializers/auth/cas.py:26 settings/serializers/auth/saml2.py:31 +#: settings/serializers/auth/cas.py:28 settings/serializers/auth/saml2.py:33 msgid "Rename attr" msgstr "映射属性" -#: settings/serializers/auth/cas.py:27 +#: settings/serializers/auth/cas.py:29 msgid "Create user if not" msgstr "创建用户(如果不存在)" -#: settings/serializers/auth/dingtalk.py:13 +#: settings/serializers/auth/dingtalk.py:15 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" -#: settings/serializers/auth/feishu.py:12 +#: settings/serializers/auth/feishu.py:14 msgid "Enable FeiShu Auth" msgstr "启用飞书认证" -#: settings/serializers/auth/ldap.py:41 +#: settings/serializers/auth/ldap.py:39 +msgid "LDAP" +msgstr "LDAP" + +#: settings/serializers/auth/ldap.py:42 msgid "LDAP server" msgstr "LDAP 地址" -#: settings/serializers/auth/ldap.py:42 +#: settings/serializers/auth/ldap.py:43 msgid "eg: ldap://localhost:389" msgstr "如: ldap://localhost:389" -#: settings/serializers/auth/ldap.py:44 +#: settings/serializers/auth/ldap.py:45 msgid "Bind DN" msgstr "绑定 DN" -#: settings/serializers/auth/ldap.py:49 +#: settings/serializers/auth/ldap.py:50 msgid "User OU" msgstr "用户 OU" -#: settings/serializers/auth/ldap.py:50 +#: settings/serializers/auth/ldap.py:51 msgid "Use | split multi OUs" msgstr "多个 OU 使用 | 分割" -#: settings/serializers/auth/ldap.py:53 +#: settings/serializers/auth/ldap.py:54 msgid "User search filter" msgstr "用户过滤器" -#: settings/serializers/auth/ldap.py:54 +#: settings/serializers/auth/ldap.py:55 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:55 -#: settings/serializers/auth/oidc.py:36 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "用户属性映射" -#: settings/serializers/auth/ldap.py:58 +#: settings/serializers/auth/ldap.py:59 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -3669,77 +3630,85 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的用户需要属性" -#: settings/serializers/auth/ldap.py:76 +#: settings/serializers/auth/ldap.py:77 msgid "Connect timeout" msgstr "连接超时时间" -#: settings/serializers/auth/ldap.py:78 +#: settings/serializers/auth/ldap.py:79 msgid "Search paged size" msgstr "搜索分页数量" -#: settings/serializers/auth/ldap.py:80 +#: settings/serializers/auth/ldap.py:81 msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/auth/oauth2.py:20 +#: settings/serializers/auth/oauth2.py:19 +msgid "OAuth2" +msgstr "OAuth2" + +#: settings/serializers/auth/oauth2.py:22 msgid "Enable OAuth2 Auth" msgstr "启用 OAuth2 认证" -#: settings/serializers/auth/oauth2.py:23 +#: settings/serializers/auth/oauth2.py:25 msgid "Logo" msgstr "图标" -#: settings/serializers/auth/oauth2.py:26 +#: settings/serializers/auth/oauth2.py:28 msgid "Service provider" msgstr "服务提供商" -#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18 +#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "客户端 ID" -#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21 +#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "客户端密钥" -#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62 +#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:63 msgid "Provider auth endpoint" msgstr "授权端点地址" -#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65 +#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:66 msgid "Provider token endpoint" msgstr "token 端点地址" -#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29 +#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "客户端认证方式" -#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:72 msgid "Provider userinfo endpoint" msgstr "用户信息端点地址" -#: settings/serializers/auth/oauth2.py:51 settings/serializers/auth/oidc.py:74 +#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:75 msgid "Provider end session endpoint" msgstr "注销会话端点地址" -#: settings/serializers/auth/oauth2.py:58 settings/serializers/auth/oidc.py:92 -#: settings/serializers/auth/saml2.py:33 +#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:93 +#: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "总是更新用户信息" -#: settings/serializers/auth/oidc.py:15 +#: settings/serializers/auth/oidc.py:12 +msgid "OIDC" +msgstr "OIDC" + +#: settings/serializers/auth/oidc.py:16 msgid "Base site url" msgstr "JumpServer 地址" -#: settings/serializers/auth/oidc.py:31 +#: settings/serializers/auth/oidc.py:32 msgid "Share session" msgstr "共享会话" -#: settings/serializers/auth/oidc.py:33 +#: settings/serializers/auth/oidc.py:34 msgid "Ignore ssl verification" msgstr "忽略 SSL 证书验证" -#: settings/serializers/auth/oidc.py:37 +#: settings/serializers/auth/oidc.py:38 msgid "" "User attr map present how to map OpenID user attr to jumpserver, username," "name,email is jumpserver attr" @@ -3747,83 +3716,91 @@ msgstr "" "用户属性映射代表怎样将OpenID中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的用户需要属性" -#: settings/serializers/auth/oidc.py:45 +#: settings/serializers/auth/oidc.py:46 msgid "Use Keycloak" msgstr "使用 Keycloak" -#: settings/serializers/auth/oidc.py:51 +#: settings/serializers/auth/oidc.py:52 msgid "Realm name" msgstr "域" -#: settings/serializers/auth/oidc.py:57 +#: settings/serializers/auth/oidc.py:58 msgid "Enable OPENID Auth" msgstr "启用 OIDC 认证" -#: settings/serializers/auth/oidc.py:59 +#: settings/serializers/auth/oidc.py:60 msgid "Provider endpoint" msgstr "端点地址" -#: settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oidc.py:69 msgid "Provider jwks endpoint" msgstr "jwks 端点地址" -#: settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oidc.py:78 msgid "Provider sign alg" msgstr "签名算法" -#: settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oidc.py:81 msgid "Provider sign key" msgstr "签名 Key" -#: settings/serializers/auth/oidc.py:82 +#: settings/serializers/auth/oidc.py:83 msgid "Scopes" msgstr "连接范围" -#: settings/serializers/auth/oidc.py:84 +#: settings/serializers/auth/oidc.py:85 msgid "Id token max age" msgstr "令牌有效时间" -#: settings/serializers/auth/oidc.py:87 +#: settings/serializers/auth/oidc.py:88 msgid "Id token include claims" msgstr "声明" -#: settings/serializers/auth/oidc.py:89 +#: settings/serializers/auth/oidc.py:90 msgid "Use state" msgstr "使用状态" -#: settings/serializers/auth/oidc.py:90 +#: settings/serializers/auth/oidc.py:91 msgid "Use nonce" msgstr "临时使用" #: settings/serializers/auth/radius.py:13 +msgid "Radius" +msgstr "Radius" + +#: settings/serializers/auth/radius.py:15 msgid "Enable Radius Auth" msgstr "启用 Radius 认证" -#: settings/serializers/auth/radius.py:19 +#: settings/serializers/auth/radius.py:21 msgid "OTP in Radius" msgstr "使用 Radius OTP" -#: settings/serializers/auth/saml2.py:12 +#: settings/serializers/auth/saml2.py:11 +msgid "SAML2" +msgstr "SAML2" + +#: settings/serializers/auth/saml2.py:14 msgid "Enable SAML2 Auth" msgstr "启用 SAML2 认证" -#: settings/serializers/auth/saml2.py:15 +#: settings/serializers/auth/saml2.py:17 msgid "IDP metadata URL" msgstr "IDP metadata 地址" -#: settings/serializers/auth/saml2.py:18 +#: settings/serializers/auth/saml2.py:20 msgid "IDP metadata XML" msgstr "IDP metadata XML" -#: settings/serializers/auth/saml2.py:21 +#: settings/serializers/auth/saml2.py:23 msgid "SP advanced settings" msgstr "高级设置" -#: settings/serializers/auth/saml2.py:25 +#: settings/serializers/auth/saml2.py:27 msgid "SP private key" msgstr "SP 密钥" -#: settings/serializers/auth/saml2.py:29 +#: settings/serializers/auth/saml2.py:31 msgid "SP cert" msgstr "SP 证书" @@ -3835,50 +3812,50 @@ msgstr "启用 SMS" msgid "SMS provider / Protocol" msgstr "短信服务商 / 协议" -#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:43 -#: settings/serializers/auth/sms.py:51 settings/serializers/auth/sms.py:60 -#: settings/serializers/auth/sms.py:71 settings/serializers/email.py:65 +#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 +#: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 msgid "Signature" msgstr "签名" -#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:44 -#: settings/serializers/auth/sms.py:52 settings/serializers/auth/sms.py:61 +#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:46 +#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 msgid "Template code" msgstr "模板" -#: settings/serializers/auth/sms.py:29 +#: settings/serializers/auth/sms.py:31 msgid "Test phone" msgstr "测试手机号" -#: settings/serializers/auth/sms.py:58 +#: settings/serializers/auth/sms.py:60 msgid "App Access Address" msgstr "应用地址" -#: settings/serializers/auth/sms.py:59 +#: settings/serializers/auth/sms.py:61 msgid "Signature channel number" msgstr "签名通道号" -#: settings/serializers/auth/sms.py:67 +#: settings/serializers/auth/sms.py:69 msgid "Enterprise code(SP id)" msgstr "企业代码(SP id)" -#: settings/serializers/auth/sms.py:68 +#: settings/serializers/auth/sms.py:70 msgid "Shared secret(Shared secret)" msgstr "共享密码(Shared secret)" -#: settings/serializers/auth/sms.py:69 +#: settings/serializers/auth/sms.py:71 msgid "Original number(Src id)" msgstr "原始号码(Src id)" -#: settings/serializers/auth/sms.py:70 +#: settings/serializers/auth/sms.py:72 msgid "Business type(Service id)" msgstr "业务类型(Service id)" -#: settings/serializers/auth/sms.py:73 +#: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "模板" -#: settings/serializers/auth/sms.py:74 +#: settings/serializers/auth/sms.py:76 #, python-brace-format msgid "" "Template need contain {code} and Signature + template length does not exceed " @@ -3888,33 +3865,33 @@ msgstr "" "模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 " "{code}, 有效期为5分钟。请不要泄露给其他人。" -#: settings/serializers/auth/sms.py:83 +#: settings/serializers/auth/sms.py:85 #, python-brace-format msgid "The template needs to contain {code}" msgstr "模板需要包含 {code}" -#: settings/serializers/auth/sms.py:86 +#: settings/serializers/auth/sms.py:88 msgid "Signature + Template must not exceed 65 words" msgstr "模板+签名不能超过65个字" -#: settings/serializers/auth/sso.py:11 +#: settings/serializers/auth/sso.py:13 msgid "Enable SSO auth" msgstr "启用 SSO Token 认证" -#: settings/serializers/auth/sso.py:12 +#: settings/serializers/auth/sso.py:14 msgid "Other service can using SSO token login to JumpServer without password" msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过程" -#: settings/serializers/auth/sso.py:15 +#: settings/serializers/auth/sso.py:17 msgid "SSO auth key TTL" msgstr "Token 有效期" -#: settings/serializers/auth/sso.py:15 +#: settings/serializers/auth/sso.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "单位: 秒" -#: settings/serializers/auth/wecom.py:13 +#: settings/serializers/auth/wecom.py:15 msgid "Enable WeCom Auth" msgstr "启用企业微信认证" @@ -3926,23 +3903,23 @@ msgstr "主题" msgid "More url" msgstr "更多信息 URL" -#: settings/serializers/basic.py:28 +#: settings/serializers/basic.py:30 msgid "Site url" msgstr "当前站点URL" -#: settings/serializers/basic.py:29 +#: settings/serializers/basic.py:31 msgid "eg: http://dev.jumpserver.org:8080" msgstr "如: http://dev.jumpserver.org:8080" -#: settings/serializers/basic.py:32 +#: settings/serializers/basic.py:34 msgid "User guide url" msgstr "用户向导URL" -#: settings/serializers/basic.py:33 +#: settings/serializers/basic.py:35 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" -#: settings/serializers/basic.py:37 +#: settings/serializers/basic.py:39 msgid "" "The forgot password url on login page, If you use ldap or cas external " "authentication, you can set it" @@ -3950,57 +3927,61 @@ msgstr "" "登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重" "置密码访问的地址" -#: settings/serializers/basic.py:41 +#: settings/serializers/basic.py:43 msgid "Global organization name" msgstr "全局组织名" -#: settings/serializers/basic.py:42 +#: settings/serializers/basic.py:44 msgid "The name of global organization to display" msgstr "全局组织的显示名称,默认为 全局组织" -#: settings/serializers/basic.py:44 +#: settings/serializers/basic.py:46 msgid "Enable announcement" msgstr "启用公告" -#: settings/serializers/basic.py:45 +#: settings/serializers/basic.py:47 msgid "Announcement" msgstr "公告" -#: settings/serializers/basic.py:46 +#: settings/serializers/basic.py:48 msgid "Enable tickets" msgstr "启用工单" -#: settings/serializers/cleaning.py:10 +#: settings/serializers/cleaning.py:8 +msgid "Period clean" +msgstr "定時清掃" + +#: settings/serializers/cleaning.py:12 msgid "Login log keep days" msgstr "登录日志" -#: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14 -#: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22 -#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35 +#: settings/serializers/cleaning.py:12 settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:20 settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:28 settings/serializers/other.py:37 msgid "Unit: day" msgstr "单位: 天" -#: settings/serializers/cleaning.py:14 +#: settings/serializers/cleaning.py:16 msgid "Task log keep days" msgstr "任务日志" -#: settings/serializers/cleaning.py:18 +#: settings/serializers/cleaning.py:20 msgid "Operate log keep days" msgstr "操作日志" -#: settings/serializers/cleaning.py:22 +#: settings/serializers/cleaning.py:24 msgid "FTP log keep days" msgstr "上传下载" -#: settings/serializers/cleaning.py:26 +#: settings/serializers/cleaning.py:28 msgid "Cloud sync record keep days" msgstr "云同步记录" -#: settings/serializers/cleaning.py:29 +#: settings/serializers/cleaning.py:31 msgid "Session keep duration" msgstr "会话日志保存时间" -#: settings/serializers/cleaning.py:30 +#: settings/serializers/cleaning.py:32 msgid "" "Unit: days, Session, record, command will be delete if more than duration, " "only in database" @@ -4008,81 +3989,81 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/serializers/email.py:20 +#: settings/serializers/email.py:21 msgid "SMTP host" msgstr "SMTP 主机" -#: settings/serializers/email.py:21 +#: settings/serializers/email.py:22 msgid "SMTP port" msgstr "SMTP 端口" -#: settings/serializers/email.py:22 +#: settings/serializers/email.py:23 msgid "SMTP account" msgstr "SMTP 账号" -#: settings/serializers/email.py:24 +#: settings/serializers/email.py:25 msgid "SMTP password" msgstr "SMTP 密码" -#: settings/serializers/email.py:25 +#: settings/serializers/email.py:26 msgid "Tips: Some provider use token except password" msgstr "提示:一些邮件提供商需要输入的是授权码" -#: settings/serializers/email.py:28 +#: settings/serializers/email.py:29 msgid "Send user" msgstr "发件人" -#: settings/serializers/email.py:29 +#: settings/serializers/email.py:30 msgid "Tips: Send mail account, default SMTP account as the send account" msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" -#: settings/serializers/email.py:32 +#: settings/serializers/email.py:33 msgid "Test recipient" msgstr "测试收件人" -#: settings/serializers/email.py:33 +#: settings/serializers/email.py:34 msgid "Tips: Used only as a test mail recipient" msgstr "提示:仅用来作为测试邮件收件人" -#: settings/serializers/email.py:37 +#: settings/serializers/email.py:38 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用 SSL" -#: settings/serializers/email.py:40 +#: settings/serializers/email.py:41 msgid "Use TLS" msgstr "使用 TLS" -#: settings/serializers/email.py:41 +#: settings/serializers/email.py:42 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用 TLS" -#: settings/serializers/email.py:44 +#: settings/serializers/email.py:45 msgid "Subject prefix" msgstr "主题前缀" -#: settings/serializers/email.py:51 +#: settings/serializers/email.py:54 msgid "Create user email subject" msgstr "邮件主题" -#: settings/serializers/email.py:52 +#: settings/serializers/email.py:55 msgid "" "Tips: When creating a user, send the subject of the email (eg:Create account " "successfully)" msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" -#: settings/serializers/email.py:56 +#: settings/serializers/email.py:59 msgid "Create user honorific" msgstr "邮件问候语" -#: settings/serializers/email.py:57 +#: settings/serializers/email.py:60 msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 你好)" -#: settings/serializers/email.py:61 +#: settings/serializers/email.py:64 msgid "Create user email content" msgstr "邮件的内容" -#: settings/serializers/email.py:62 +#: settings/serializers/email.py:65 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4090,70 +4071,74 @@ msgid "" msgstr "" "提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" -#: settings/serializers/email.py:66 +#: settings/serializers/email.py:69 msgid "Tips: Email signature (eg:jumpserver)" msgstr "邮件署名 (如:jumpserver)" -#: settings/serializers/other.py:7 +#: settings/serializers/other.py:6 +msgid "More..." +msgstr "更多..." + +#: settings/serializers/other.py:9 msgid "Email suffix" msgstr "邮件后缀" -#: settings/serializers/other.py:8 +#: settings/serializers/other.py:10 msgid "" "This is used by default if no email is returned during SSO authentication" msgstr "SSO认证时,如果没有返回邮件地址,将使用该后缀" -#: settings/serializers/other.py:12 +#: settings/serializers/other.py:14 msgid "OTP issuer name" msgstr "OTP 扫描后的名称" -#: settings/serializers/other.py:16 +#: settings/serializers/other.py:18 msgid "OTP valid window" msgstr "OTP 延迟有效次数" -#: settings/serializers/other.py:21 +#: settings/serializers/other.py:23 msgid "CMD" msgstr "CMD" -#: settings/serializers/other.py:22 +#: settings/serializers/other.py:24 msgid "PowerShell" msgstr "PowerShell" -#: settings/serializers/other.py:24 +#: settings/serializers/other.py:26 msgid "Shell (Windows)" msgstr "Windows shell" -#: settings/serializers/other.py:25 +#: settings/serializers/other.py:27 msgid "The shell type used when Windows assets perform ansible tasks" msgstr "windows 资产执行 Ansible 任务时,使用的 Shell 类型。" -#: settings/serializers/other.py:29 +#: settings/serializers/other.py:31 msgid "Perm ungroup node" msgstr "显示未分组节点" -#: settings/serializers/other.py:30 +#: settings/serializers/other.py:32 msgid "Perm single to ungroup node" msgstr "" "放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问" "题" -#: settings/serializers/other.py:35 +#: settings/serializers/other.py:37 msgid "Ticket authorize default time" msgstr "默认工单授权时间" -#: settings/serializers/other.py:39 +#: settings/serializers/other.py:41 msgid "Help Docs URL" msgstr "文档链接" -#: settings/serializers/other.py:40 +#: settings/serializers/other.py:42 msgid "default: http://docs.jumpserver.org" msgstr "默认: http://dev.jumpserver.org:8080" -#: settings/serializers/other.py:44 +#: settings/serializers/other.py:46 msgid "Help Support URL" msgstr "支持链接" -#: settings/serializers/other.py:45 +#: settings/serializers/other.py:47 msgid "default: http://www.jumpserver.org/support/" msgstr "默认: http://www.jumpserver.org/support/" @@ -4317,73 +4302,77 @@ msgstr "启用登录验证码" msgid "Enable captcha to prevent robot authentication" msgstr "开启验证码,防止机器人登录" -#: settings/serializers/security.py:147 +#: settings/serializers/security.py:146 +msgid "Security" +msgstr "安全" + +#: settings/serializers/security.py:149 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:151 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/security.py:153 +#: settings/serializers/security.py:155 msgid "Enable watermark" msgstr "开启水印" -#: settings/serializers/security.py:154 +#: settings/serializers/security.py:156 msgid "Enabled, the web session and replay contains watermark information" msgstr "启用后,Web 会话和录像将包含水印信息" -#: settings/serializers/security.py:158 +#: settings/serializers/security.py:160 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/security.py:159 +#: settings/serializers/security.py:161 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/security.py:162 +#: settings/serializers/security.py:164 msgid "Remember manual auth" msgstr "保存手动输入密码" -#: settings/serializers/security.py:165 +#: settings/serializers/security.py:167 msgid "Enable change auth secure mode" msgstr "启用改密安全模式" -#: settings/serializers/security.py:168 +#: settings/serializers/security.py:170 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:171 +#: settings/serializers/security.py:173 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:172 +#: settings/serializers/security.py:174 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:175 +#: settings/serializers/security.py:177 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/security.py:176 +#: settings/serializers/security.py:178 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:179 +#: settings/serializers/security.py:181 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:180 +#: settings/serializers/security.py:182 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" -#: settings/serializers/security.py:183 +#: settings/serializers/security.py:185 msgid "Remote Login Protection" msgstr "异地登录保护" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:187 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 " @@ -4392,19 +4381,28 @@ msgstr "" "根据登录 IP 是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地" "登录提醒" -#: settings/serializers/terminal.py:13 +#: settings/serializers/settings.py:61 +#: users/templates/users/reset_password.html:29 +msgid "Setting" +msgstr "设置" + +#: settings/serializers/terminal.py:6 terminal/models/terminal.py:185 +msgid "Terminal" +msgstr "终端" + +#: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自动" -#: settings/serializers/terminal.py:19 +#: settings/serializers/terminal.py:21 msgid "Password auth" msgstr "密码认证" -#: settings/serializers/terminal.py:21 +#: settings/serializers/terminal.py:23 msgid "Public key auth" msgstr "密钥认证" -#: settings/serializers/terminal.py:22 +#: settings/serializers/terminal.py:24 msgid "" "Tips: If use other auth method, like AD/LDAP, you should disable this to " "avoid being able to log in after deleting" @@ -4412,37 +4410,46 @@ msgstr "" "提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" "除后,还可以登录" -#: settings/serializers/terminal.py:26 +#: settings/serializers/terminal.py:28 msgid "List sort by" msgstr "资产列表排序" -#: settings/serializers/terminal.py:29 +#: settings/serializers/terminal.py:31 msgid "List page size" msgstr "资产列表每页数量" -#: settings/serializers/terminal.py:32 +#: settings/serializers/terminal.py:34 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/serializers/terminal.py:33 +#: settings/serializers/terminal.py:35 msgid "" "Tips: The login success message varies with devices. if you cannot log in to " "the device through Telnet, set this parameter" msgstr "" "提示: 不同设备登录成功提示不一样,所以如果 telnet 不能正常登录,可以这里设置" -#: settings/serializers/terminal.py:36 +#: settings/serializers/terminal.py:38 msgid "Enable database proxy" msgstr "启用数据库组件" -#: settings/serializers/terminal.py:37 +#: settings/serializers/terminal.py:39 msgid "Enable Razor" msgstr "启用 Razor 服务" -#: settings/serializers/terminal.py:38 +#: settings/serializers/terminal.py:40 msgid "Enable SSH Client" msgstr "启用 SSH Client" +#: settings/serializers/terminal.py:51 +msgid "Default graphics resolution" +msgstr "默认图形化分辨率" + +#: settings/serializers/terminal.py:52 +msgid "" +"Tip: Default resolution to use when connecting graphical assets in Luna pages" +msgstr "提示:在Luna 页面中连接图形化资产时默认使用的分辨率" + #: settings/utils/ldap.py:467 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" @@ -4678,14 +4685,17 @@ msgstr "" " " #: templates/_mfa_login_field.html:28 +#: users/templates/users/forgot_password.html:65 msgid "Send verification code" msgstr "发送验证码" #: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:122 msgid "Wait: " msgstr "等待:" #: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:138 msgid "The verification code has been sent" msgstr "验证码已发送" @@ -4725,11 +4735,17 @@ msgstr "Windows 远程应用发布服务器工具" #: templates/resource_download.html:43 msgid "" +"OpenSSH is a program used to connect remote applications in the Windows " +"Remote Application Publisher" +msgstr "OpenSSH 是在 windows 远程应用发布服务器中用来连接远程应用的程序" + +#: templates/resource_download.html:48 +msgid "" "Jmservisor is the program used to pull up remote applications in Windows " "Remote Application publisher" msgstr "Jmservisor 是在 windows 远程应用发布服务器中用来拉起远程应用的程序" -#: templates/resource_download.html:51 +#: templates/resource_download.html:57 msgid "Offline video player" msgstr "离线录像播放器" @@ -4745,10 +4761,6 @@ msgstr "会话不存在: {}" msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:224 -msgid "User does not exist: {}" -msgstr "用户不存在: {}" - #: terminal/api/session.py:232 msgid "User does not have permission" msgstr "用户没有权限" @@ -4793,14 +4805,6 @@ msgstr "有在线会话" msgid "Terminals" msgstr "终端管理" -#: terminal/backends/command/es.py:28 -msgid "Invalid elasticsearch config" -msgstr "无效的 Elasticsearch 配置" - -#: terminal/backends/command/es.py:33 -msgid "Not Support Elasticsearch8" -msgstr "不支持 Elasticsearch8" - #: terminal/backends/command/models.py:16 msgid "Ordinary" msgstr "普通" @@ -4842,7 +4846,7 @@ msgstr "风险等级名称" msgid "Timestamp" msgstr "时间戳" -#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:105 +#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:106 msgid "Remote Address" msgstr "远端地址" @@ -4878,11 +4882,11 @@ msgstr "命令记录" msgid "HTTPS Port" msgstr "HTTPS 端口" -#: terminal/models/endpoint.py:18 terminal/models/terminal.py:107 +#: terminal/models/endpoint.py:18 terminal/models/terminal.py:108 msgid "HTTP Port" msgstr "HTTP 端口" -#: terminal/models/endpoint.py:19 terminal/models/terminal.py:106 +#: terminal/models/endpoint.py:19 terminal/models/terminal.py:107 msgid "SSH Port" msgstr "SSH 端口" @@ -4917,35 +4921,35 @@ msgstr "可以上传会话录像" msgid "Can download session replay" msgstr "可以下载会话录像" -#: terminal/models/session.py:50 terminal/models/sharing.py:101 +#: terminal/models/session.py:51 terminal/models/sharing.py:101 msgid "Login from" msgstr "登录来源" -#: terminal/models/session.py:54 +#: terminal/models/session.py:55 msgid "Replay" msgstr "回放" -#: terminal/models/session.py:59 +#: terminal/models/session.py:60 msgid "Date end" msgstr "结束日期" -#: terminal/models/session.py:260 +#: terminal/models/session.py:261 msgid "Session record" msgstr "会话记录" -#: terminal/models/session.py:262 +#: terminal/models/session.py:263 msgid "Can monitor session" msgstr "可以监控会话" -#: terminal/models/session.py:263 +#: terminal/models/session.py:264 msgid "Can share session" msgstr "可以分享会话" -#: terminal/models/session.py:264 +#: terminal/models/session.py:265 msgid "Can terminate session" msgstr "可以终断会话" -#: terminal/models/session.py:265 +#: terminal/models/session.py:266 msgid "Can validate session action perm" msgstr "可以验证会话动作权限" @@ -4953,10 +4957,6 @@ msgstr "可以验证会话动作权限" msgid "Creator" msgstr "创建者" -#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 -msgid "Verify code" -msgstr "验证码" - #: terminal/models/sharing.py:31 msgid "Expired time (min)" msgstr "过期时间 (分)" @@ -5034,15 +5034,15 @@ msgstr "线程数" msgid "Boot Time" msgstr "运行时间" -#: terminal/models/storage.py:28 +#: terminal/models/storage.py:29 msgid "Default storage" msgstr "默认存储" -#: terminal/models/storage.py:137 terminal/models/terminal.py:108 +#: terminal/models/storage.py:139 terminal/models/terminal.py:109 msgid "Command storage" msgstr "命令存储" -#: terminal/models/storage.py:197 terminal/models/terminal.py:109 +#: terminal/models/storage.py:199 terminal/models/terminal.py:110 msgid "Replay storage" msgstr "录像存储" @@ -5054,15 +5054,19 @@ msgstr "参数" msgid "Kwargs" msgstr "其它参数" -#: terminal/models/terminal.py:103 +#: terminal/models/terminal.py:104 msgid "type" msgstr "类型" -#: terminal/models/terminal.py:183 -msgid "Terminal" -msgstr "终端" +#: terminal/models/terminal.py:111 +msgid "Application User" +msgstr "应用用户" -#: terminal/models/terminal.py:185 +#: terminal/models/terminal.py:112 +msgid "Is Accepted" +msgstr "被接受" + +#: terminal/models/terminal.py:187 msgid "Can view terminal config" msgstr "可以查看终端配置" @@ -5074,11 +5078,11 @@ msgstr "会话管理" msgid "Danger command alert" msgstr "危险命令告警" -#: terminal/notifications.py:92 terminal/notifications.py:140 +#: terminal/notifications.py:95 terminal/notifications.py:143 msgid "Level" msgstr "级别" -#: terminal/notifications.py:110 +#: terminal/notifications.py:113 msgid "Batch danger command alert" msgstr "批量危险命令告警" @@ -5159,7 +5163,7 @@ msgstr "访问密钥 ID(AK)" msgid "Access key secret" msgstr "访问密钥密文(SK)" -#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:220 +#: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:222 msgid "Region" msgstr "地域" @@ -5678,100 +5682,109 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:109 +#: users/forms/profile.py:112 msgid "Old password" msgstr "原来密码" -#: users/forms/profile.py:119 +#: users/forms/profile.py:122 msgid "Old password error" msgstr "原来密码错误" -#: users/forms/profile.py:129 +#: users/forms/profile.py:132 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms/profile.py:131 +#: users/forms/profile.py:134 msgid "ssh public key" msgstr "SSH公钥" -#: users/forms/profile.py:132 +#: users/forms/profile.py:135 msgid "ssh-rsa AAAA..." msgstr "ssh-rsa AAAA..." -#: users/forms/profile.py:133 +#: users/forms/profile.py:136 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms/profile.py:146 +#: users/forms/profile.py:149 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms/profile.py:150 users/serializers/profile.py:100 +#: users/forms/profile.py:153 users/serializers/profile.py:100 #: users/serializers/profile.py:183 users/serializers/profile.py:210 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:161 users/models/user.py:700 +#: users/forms/profile.py:164 users/models/user.py:706 msgid "Public key" msgstr "SSH公钥" -#: users/models/user.py:558 +#: users/models/user.py:559 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:625 +#: users/models/user.py:629 msgid "Local" msgstr "数据库" -#: users/models/user.py:681 users/serializers/user.py:149 +#: users/models/user.py:685 users/serializers/user.py:149 msgid "Is service account" msgstr "服务账号" -#: users/models/user.py:683 +#: users/models/user.py:687 msgid "Avatar" msgstr "头像" -#: users/models/user.py:686 +#: users/models/user.py:690 msgid "Wechat" msgstr "微信" -#: users/models/user.py:703 +#: users/models/user.py:699 +msgid "OTP secret key" +msgstr "OTP 秘钥" + +#: users/models/user.py:709 msgid "Secret key" msgstr "Secret key" -#: users/models/user.py:719 +#: users/models/user.py:714 users/serializers/profile.py:149 +#: users/serializers/user.py:146 +msgid "Is first login" +msgstr "首次登录" + +#: users/models/user.py:725 msgid "Source" msgstr "来源" -#: users/models/user.py:723 +#: users/models/user.py:729 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:726 +#: users/models/user.py:732 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:901 +#: users/models/user.py:907 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:902 +#: users/models/user.py:908 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:903 +#: users/models/user.py:909 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:912 +#: users/models/user.py:918 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:915 +#: users/models/user.py:921 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:940 +#: users/models/user.py:946 msgid "User password history" msgstr "用户密码历史" @@ -5782,7 +5795,7 @@ msgstr "用户密码历史" msgid "Reset password" msgstr "重置密码" -#: users/notifications.py:85 users/views/profile/reset.py:127 +#: users/notifications.py:85 users/views/profile/reset.py:139 msgid "Reset password success" msgstr "重置密码成功" @@ -5822,10 +5835,6 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:149 users/serializers/user.py:146 -msgid "Is first login" -msgstr "首次登录" - #: users/serializers/user.py:28 msgid "System roles" msgstr "系统角色" @@ -5976,14 +5985,22 @@ msgstr "你的 SSH 密钥已经被管理员重置" msgid "click here to set your password" msgstr "点击这里设置密码" -#: users/templates/users/forgot_password.html:24 +#: users/templates/users/forgot_password.html:38 msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:68 msgid "Submit" msgstr "提交" +#: users/templates/users/forgot_password.html:71 +msgid "Use the phone number to retrieve the password" +msgstr "使用手机号找回密码" + +#: users/templates/users/forgot_password.html:72 +msgid "Use email to retrieve the password" +msgstr "使用邮箱找回密码" + #: users/templates/users/mfa_setting.html:24 msgid "Enable MFA" msgstr "启用 MFA 多因子认证" @@ -6004,10 +6021,6 @@ msgstr "您的密码必须满足:" msgid "Password strength" msgstr "密码强度:" -#: users/templates/users/reset_password.html:29 -msgid "Setting" -msgstr "设置" - #: users/templates/users/reset_password.html:48 msgid "Very weak" msgstr "很弱" @@ -6122,42 +6135,23 @@ msgstr "MFA(OTP) 禁用成功,返回登录页面" msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/profile/reset.py:40 -msgid "Send reset password message" -msgstr "发送重置密码邮件" - -#: users/views/profile/reset.py:41 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" - -#: users/views/profile/reset.py:52 -msgid "Email address invalid, please input again" -msgstr "邮箱地址错误,重新输入" - -#: users/views/profile/reset.py:58 -msgid "" -"The user is from {}, please go to the corresponding system to change the " -"password" -msgstr "用户来自 {} 请去相应系统修改密码" - -#: users/views/profile/reset.py:84 users/views/profile/reset.py:95 +#: users/views/profile/reset.py:96 users/views/profile/reset.py:107 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/profile/reset.py:100 +#: users/views/profile/reset.py:112 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/views/profile/reset.py:107 +#: users/views/profile/reset.py:119 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/views/profile/reset.py:113 +#: users/views/profile/reset.py:125 msgid "* The new password cannot be the last {} passwords" msgstr "* 新密码不能是最近 {} 次的密码" -#: users/views/profile/reset.py:128 +#: users/views/profile/reset.py:140 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" @@ -6377,70 +6371,78 @@ msgid "JD Cloud" msgstr "京东云" #: xpack/plugins/cloud/const.py:16 +msgid "KingSoft Cloud" +msgstr "金山云" + +#: xpack/plugins/cloud/const.py:17 msgid "Tencent Cloud" msgstr "腾讯云" -#: xpack/plugins/cloud/const.py:17 +#: xpack/plugins/cloud/const.py:18 msgid "Tencent Cloud (Lighthouse)" msgstr "腾讯云(轻量服务器应用)" -#: xpack/plugins/cloud/const.py:18 +#: xpack/plugins/cloud/const.py:19 msgid "VMware" msgstr "VMware" -#: xpack/plugins/cloud/const.py:19 xpack/plugins/cloud/providers/nutanix.py:13 +#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 msgid "Nutanix" msgstr "Nutanix" -#: xpack/plugins/cloud/const.py:20 +#: xpack/plugins/cloud/const.py:21 msgid "Huawei Private Cloud" msgstr "华为私有云" -#: xpack/plugins/cloud/const.py:21 +#: xpack/plugins/cloud/const.py:22 msgid "Qingyun Private Cloud" msgstr "青云私有云" -#: xpack/plugins/cloud/const.py:22 +#: xpack/plugins/cloud/const.py:23 msgid "CTYun Private Cloud" msgstr "天翼私有云" -#: xpack/plugins/cloud/const.py:23 +#: xpack/plugins/cloud/const.py:24 msgid "OpenStack" msgstr "OpenStack" -#: xpack/plugins/cloud/const.py:24 +#: xpack/plugins/cloud/const.py:25 msgid "Google Cloud Platform" msgstr "谷歌云" -#: xpack/plugins/cloud/const.py:25 +#: xpack/plugins/cloud/const.py:26 msgid "Fusion Compute" msgstr "" -#: xpack/plugins/cloud/const.py:30 +#: xpack/plugins/cloud/const.py:31 +msgid "Private IP" +msgstr "私有IP" + +#: xpack/plugins/cloud/const.py:36 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/const.py:31 +#: xpack/plugins/cloud/const.py:37 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/const.py:36 +#: xpack/plugins/cloud/const.py:42 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:40 +#: xpack/plugins/cloud/const.py:46 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/const.py:41 +#: xpack/plugins/cloud/const.py:47 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/const.py:42 +#: xpack/plugins/cloud/const.py:48 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/const.py:43 +#: xpack/plugins/cloud/const.py:49 msgid "Released" msgstr "已释放" @@ -6448,75 +6450,79 @@ msgstr "已释放" msgid "Cloud center" msgstr "云管中心" -#: xpack/plugins/cloud/models.py:30 +#: xpack/plugins/cloud/models.py:29 msgid "Provider" msgstr "云服务商" -#: xpack/plugins/cloud/models.py:39 +#: xpack/plugins/cloud/models.py:38 msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:41 +#: xpack/plugins/cloud/models.py:40 msgid "Test cloud account" msgstr "测试云账号" -#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67 +#: xpack/plugins/cloud/models.py:84 xpack/plugins/cloud/serializers/task.py:69 msgid "Account" msgstr "账号" -#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38 +#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:39 msgid "Regions" msgstr "地域" -#: xpack/plugins/cloud/models.py:91 +#: xpack/plugins/cloud/models.py:90 msgid "Hostname strategy" msgstr "主机名策略" -#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68 +#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:70 msgid "Unix admin user" msgstr "Unix 管理员" -#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69 +#: xpack/plugins/cloud/models.py:103 xpack/plugins/cloud/serializers/task.py:71 msgid "Windows admin user" msgstr "Windows 管理员" -#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46 +#: xpack/plugins/cloud/models.py:109 xpack/plugins/cloud/serializers/task.py:47 msgid "IP network segment group" msgstr "IP网段组" -#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72 +#: xpack/plugins/cloud/models.py:112 +msgid "Sync IP type" +msgstr "同步IP类型" + +#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/serializers/task.py:74 msgid "Always update" msgstr "总是更新" -#: xpack/plugins/cloud/models.py:119 +#: xpack/plugins/cloud/models.py:121 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:130 xpack/plugins/cloud/models.py:171 +#: xpack/plugins/cloud/models.py:132 xpack/plugins/cloud/models.py:173 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:182 xpack/plugins/cloud/models.py:230 +#: xpack/plugins/cloud/models.py:184 xpack/plugins/cloud/models.py:232 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:186 +#: xpack/plugins/cloud/models.py:188 msgid "Sync instance task execution" msgstr "同步实例任务执行" -#: xpack/plugins/cloud/models.py:210 +#: xpack/plugins/cloud/models.py:212 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:214 +#: xpack/plugins/cloud/models.py:216 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:217 +#: xpack/plugins/cloud/models.py:219 msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/models.py:234 +#: xpack/plugins/cloud/models.py:236 msgid "Sync instance detail" msgstr "同步实例详情" @@ -6710,11 +6716,11 @@ msgstr "华南-广州-友好用户环境" msgid "CN East-Suqian" msgstr "华东-宿迁" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:65 msgid "Validity display" msgstr "有效性显示" -#: xpack/plugins/cloud/serializers/account.py:65 +#: xpack/plugins/cloud/serializers/account.py:66 msgid "Provider display" msgstr "服务商显示" @@ -6800,7 +6806,7 @@ msgstr "测试端口" msgid "Test timeout" msgstr "测试超时时间" -#: xpack/plugins/cloud/serializers/task.py:29 +#: xpack/plugins/cloud/serializers/task.py:30 msgid "" "Only instances matching the IP range will be synced.
If the instance " "contains multiple IP addresses, the first IP address that matches will be " @@ -6812,19 +6818,19 @@ msgstr "" "到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 " "IP 地址。
格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20" -#: xpack/plugins/cloud/serializers/task.py:36 +#: xpack/plugins/cloud/serializers/task.py:37 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers/task.py:37 +#: xpack/plugins/cloud/serializers/task.py:38 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers/task.py:66 +#: xpack/plugins/cloud/serializers/task.py:68 msgid "Linux admin user" msgstr "Linux 管理员" -#: xpack/plugins/cloud/serializers/task.py:71 +#: xpack/plugins/cloud/serializers/task.py:73 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" @@ -6885,7 +6891,7 @@ msgstr "退出页面logo" msgid "Theme" msgstr "主题" -#: xpack/plugins/interface/models.py:43 +#: xpack/plugins/interface/models.py:43 xpack/plugins/interface/models.py:84 msgid "Interface setting" msgstr "界面设置" diff --git a/apps/notifications/backends/sms.py b/apps/notifications/backends/sms.py index 860db437d..18836f421 100644 --- a/apps/notifications/backends/sms.py +++ b/apps/notifications/backends/sms.py @@ -1,6 +1,4 @@ -from django.conf import settings - -from common.sdk.sms.alibaba import AlibabaSMS as Client +from common.sdk.sms.endpoint import SMS from .base import BackendBase @@ -9,13 +7,7 @@ class SMS(BackendBase): is_enable_field_in_settings = 'SMS_ENABLED' def __init__(self): - """ - 暂时只对接阿里,之后再扩展 - """ - self.client = Client( - access_key_id=settings.ALIBABA_ACCESS_KEY_ID, - access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET - ) + self.client = SMS() def send_msg(self, users, sign_name: str, template_code: str, template_param: dict): accounts, __, __ = self.get_accounts(users) diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py index 8b52fb84f..e63e4f36a 100644 --- a/apps/notifications/migrations/0001_initial.py +++ b/apps/notifications/migrations/0001_initial.py @@ -43,11 +43,11 @@ class Migration(migrations.Migration): ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('message_type', models.CharField(max_length=128)), - ('receive_backends', models.JSONField(default=list)), + ('receive_backends', models.JSONField(default=list, verbose_name='receive backend')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL)), ], options={ - 'abstract': False, + 'abstract': False, 'verbose_name': 'User message' }, ), migrations.CreateModel( @@ -64,7 +64,7 @@ class Migration(migrations.Migration): ('users', models.ManyToManyField(related_name='system_msg_subscriptions', to=settings.AUTH_USER_MODEL)), ], options={ - 'abstract': False, + 'abstract': False, 'verbose_name': 'System message' }, ), migrations.CreateModel( diff --git a/apps/notifications/migrations/0002_auto_20210909_1946.py b/apps/notifications/migrations/0002_auto_20210909_1946.py index 75f71fad7..fb48ced63 100644 --- a/apps/notifications/migrations/0002_auto_20210909_1946.py +++ b/apps/notifications/migrations/0002_auto_20210909_1946.py @@ -1,8 +1,8 @@ # Generated by Django 3.1.12 on 2021-09-09 11:46 +import common.db.models from django.conf import settings from django.db import migrations, models -import django.db.models.deletion def init_user_msg_subscription(apps, schema_editor): @@ -49,7 +49,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='usermsgsubscription', name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL), + field=models.OneToOneField(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL, verbose_name='User'), ), migrations.RunPython(init_user_msg_subscription) ] diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py index d50168576..0d6a8b8a4 100644 --- a/apps/notifications/models/notification.py +++ b/apps/notifications/models/notification.py @@ -1,16 +1,23 @@ from django.db import models +from django.utils.translation import ugettext_lazy as _ -from common.db.models import JMSModel +from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP __all__ = ('SystemMsgSubscription', 'UserMsgSubscription') class UserMsgSubscription(JMSModel): - user = models.OneToOneField('users.User', related_name='user_msg_subscription', on_delete=models.CASCADE) - receive_backends = models.JSONField(default=list) + user = models.OneToOneField( + 'users.User', related_name='user_msg_subscription', on_delete=CASCADE_SIGNAL_SKIP, + verbose_name=_('User') + ) + receive_backends = models.JSONField(default=list, verbose_name=_('receive backend')) + + class Meta: + verbose_name = _('User message') def __str__(self): - return f'{self.user} subscription: {self.receive_backends}' + return _('{} subscription').format(self.user) class SystemMsgSubscription(JMSModel): @@ -21,11 +28,19 @@ class SystemMsgSubscription(JMSModel): message_type_label = '' - def __str__(self): - return f'{self.message_type}' + class Meta: + verbose_name = _('System message') - def __repr__(self): - return self.__str__() + def set_message_type_label(self): + # 采用手动调用,没设置成 property 的方式 + # 因为目前只有界面修改时会用到这个属性,避免实例化时占用资源计算 + from ..notifications import system_msgs + msg_label = '' + for msg in system_msgs: + if msg.get('message_type') == self.message_type: + msg_label = msg.get('message_type_label', '') + break + self.message_type_label = msg_label @property def receivers(self): @@ -47,3 +62,9 @@ class SystemMsgSubscription(JMSModel): receviers.append(recevier) return receviers + + def __str__(self): + return f'{self.message_type_label}' or f'{self.message_type}' + + def __repr__(self): + return self.__str__() diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py index 191d90a77..fcf8c811c 100644 --- a/apps/notifications/serializers/notifications.py +++ b/apps/notifications/serializers/notifications.py @@ -22,6 +22,10 @@ class SystemMsgSubscriptionSerializer(BulkModelSerializer): 'receive_backends': {'required': True} } + def update(self, instance, validated_data): + instance.set_message_type_label() + return super().update(instance, validated_data) + class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer): category = serializers.CharField() diff --git a/apps/ops/migrations/0001_initial.py b/apps/ops/migrations/0001_initial.py index 13adf43a8..94643ffa0 100644 --- a/apps/ops/migrations/0001_initial.py +++ b/apps/ops/migrations/0001_initial.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('interval', models.IntegerField(blank=True, help_text='Units: seconds', null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, help_text='5 * * * *', max_length=128, null=True, verbose_name='Crontab')), - ('is_periodic', models.BooleanField(default=False)), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), ('callback', models.CharField(blank=True, max_length=128, null=True, verbose_name='Callback')), ('is_deleted', models.BooleanField(default=False)), ('comment', models.TextField(blank=True, verbose_name='Comment')), diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index e64a763fc..da77bacae 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -23,7 +23,7 @@ class PeriodTaskModelMixin(models.Model): name = models.CharField( max_length=128, unique=False, verbose_name=_("Name") ) - is_periodic = models.BooleanField(default=False) + is_periodic = models.BooleanField(default=False, verbose_name=_("Periodic perform")) interval = models.IntegerField( default=24, null=True, blank=True, verbose_name=_("Cycle perform"), diff --git a/apps/perms/migrations/0029_alter_applicationpermission_type.py b/apps/perms/migrations/0029_alter_applicationpermission_type.py new file mode 100644 index 000000000..a42fd1e71 --- /dev/null +++ b/apps/perms/migrations/0029_alter_applicationpermission_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-04 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0028_auto_20220316_2028'), + ] + + operations = [ + migrations.AlterField( + model_name='applicationpermission', + name='type', + field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('clickhouse', 'ClickHouse'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index b2d388717..7132a3475 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -6,11 +6,11 @@ from django.utils.translation import ugettext_lazy as _ from django.db import models from django.db.models import Q from django.utils import timezone -from orgs.mixins.models import OrgModelMixin +from orgs.mixins.models import OrgModelMixin, OrgManager from common.db.models import UnionQuerySet, BitOperationChoice from common.utils import date_expired_default, lazyproperty -from orgs.mixins.models import OrgManager + __all__ = [ 'BasePermission', 'BasePermissionQuerySet', 'Action' diff --git a/apps/rbac/migrations/0001_initial.py b/apps/rbac/migrations/0001_initial.py index d3f94f6f2..8be92f6d3 100644 --- a/apps/rbac/migrations/0001_initial.py +++ b/apps/rbac/migrations/0001_initial.py @@ -1,5 +1,6 @@ # Generated by Django 3.1.13 on 2021-11-19 08:29 +import common.db.models from django.conf import settings import django.contrib.auth.models import django.contrib.contenttypes.models @@ -84,7 +85,7 @@ class Migration(migrations.Migration): ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='orgs.organization', verbose_name='Organization')), ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='rbac.role', verbose_name='Role')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')), ], options={ 'verbose_name': 'Role binding', diff --git a/apps/rbac/migrations/0002_auto_20210929_1409.py b/apps/rbac/migrations/0002_auto_20210929_1409.py index e280d4f20..514a2d78f 100644 --- a/apps/rbac/migrations/0002_auto_20210929_1409.py +++ b/apps/rbac/migrations/0002_auto_20210929_1409.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='permission', - options={'verbose_name': 'Permission'}, + options={'verbose_name': 'Permissions'}, ), migrations.AlterModelOptions( name='role', diff --git a/apps/rbac/models/permission.py b/apps/rbac/models/permission.py index 5b98b6045..0cb29f73a 100644 --- a/apps/rbac/models/permission.py +++ b/apps/rbac/models/permission.py @@ -23,7 +23,7 @@ class Permission(DjangoPermission): """ 权限类 """ class Meta: proxy = True - verbose_name = _('Permission') + verbose_name = _('Permissions') @classmethod def to_perms(cls, queryset): diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py index 9b2256332..c1debdbfe 100644 --- a/apps/rbac/models/rolebinding.py +++ b/apps/rbac/models/rolebinding.py @@ -5,7 +5,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from rest_framework.serializers import ValidationError -from common.db.models import JMSModel +from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP from common.utils import lazyproperty from orgs.utils import current_org, tmp_to_root_org from .role import Role @@ -38,7 +38,7 @@ class RoleBinding(JMSModel): verbose_name=_('Scope') ) user = models.ForeignKey( - 'users.User', related_name='role_bindings', on_delete=models.CASCADE, verbose_name=_('User') + 'users.User', related_name='role_bindings', on_delete=CASCADE_SIGNAL_SKIP, verbose_name=_('User') ) role = models.ForeignKey( Role, related_name='role_bindings', on_delete=models.CASCADE, verbose_name=_('Role') @@ -56,7 +56,7 @@ class RoleBinding(JMSModel): ] def __str__(self): - display = '{user} & {role}'.format(user=self.user, role=self.role) + display = '{role} -> {user}'.format(user=self.user, role=self.role) if self.org: display += ' | {org}'.format(org=self.org) return display diff --git a/apps/settings/migrations/0001_initial.py b/apps/settings/migrations/0001_initial.py index c5d8c4c36..26ba90b00 100644 --- a/apps/settings/migrations/0001_initial.py +++ b/apps/settings/migrations/0001_initial.py @@ -21,8 +21,8 @@ class Migration(migrations.Migration): verbose_name='Name')), ('value', models.TextField(verbose_name='Value')), ('category', - models.CharField(default='default', max_length=128)), - ('encrypted', models.BooleanField(default=False)), + models.CharField(default='default', max_length=128, verbose_name='Category')), + ('encrypted', models.BooleanField(default=False, verbose_name='Encrypted')), ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), ('comment', models.TextField(verbose_name='Comment')), diff --git a/apps/settings/models.py b/apps/settings/models.py index 8b91765cf..8506a0fa1 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -32,8 +32,8 @@ class SettingManager(models.Manager): class Setting(models.Model): name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) value = models.TextField(verbose_name=_("Value"), null=True, blank=True) - category = models.CharField(max_length=128, default="default") - encrypted = models.BooleanField(default=False) + category = models.CharField(max_length=128, default="default", verbose_name=_('Category')) + encrypted = models.BooleanField(default=False, verbose_name=_('Encrypted')) enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) comment = models.TextField(verbose_name=_("Comment")) diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py index d4ea37f36..24abb372e 100644 --- a/apps/settings/serializers/auth/base.py +++ b/apps/settings/serializers/auth/base.py @@ -7,6 +7,8 @@ __all__ = [ class AuthSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('Basic')) + AUTH_CAS = serializers.BooleanField(required=False, label=_('CAS Auth')) AUTH_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth')) AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth')) diff --git a/apps/settings/serializers/auth/cas.py b/apps/settings/serializers/auth/cas.py index bb1ab9c76..4814a92bb 100644 --- a/apps/settings/serializers/auth/cas.py +++ b/apps/settings/serializers/auth/cas.py @@ -7,6 +7,8 @@ __all__ = [ class CASSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('CAS')) + AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth')) CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url')) CAS_ROOT_PROXIED_AS = serializers.CharField( diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py index 37875bba3..ac22cc056 100644 --- a/apps/settings/serializers/auth/dingtalk.py +++ b/apps/settings/serializers/auth/dingtalk.py @@ -7,6 +7,8 @@ __all__ = ['DingTalkSettingSerializer'] class DingTalkSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('DingTalk')) + DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId') DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey') DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='AppSecret') diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index 67478bae5..0f5ba93d6 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -7,6 +7,8 @@ __all__ = ['FeiShuSettingSerializer'] class FeiShuSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('FeiShu')) + FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index ddf963443..2e371c388 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -36,6 +36,7 @@ class LDAPUserSerializer(serializers.Serializer): class LDAPSettingSerializer(serializers.Serializer): # encrypt_fields 现在使用 write_only 来判断了 + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('LDAP')) AUTH_LDAP_SERVER_URI = serializers.CharField( required=True, max_length=1024, label=_('LDAP server'), diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py index a2230750a..42c310333 100644 --- a/apps/settings/serializers/auth/oauth2.py +++ b/apps/settings/serializers/auth/oauth2.py @@ -16,6 +16,8 @@ class SettingImageField(serializers.ImageField): class OAuth2SettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('OAuth2')) + AUTH_OAUTH2 = serializers.BooleanField( default=False, label=_('Enable OAuth2 Auth') ) diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index 3ecf2bb6c..ea25ca9d7 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -9,6 +9,7 @@ __all__ = [ class CommonSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('OIDC')) # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) BASE_SITE_URL = serializers.CharField( required=False, allow_null=True, allow_blank=True, diff --git a/apps/settings/serializers/auth/radius.py b/apps/settings/serializers/auth/radius.py index 4d56510bf..6ef574eef 100644 --- a/apps/settings/serializers/auth/radius.py +++ b/apps/settings/serializers/auth/radius.py @@ -10,6 +10,8 @@ __all__ = ['RadiusSettingSerializer'] class RadiusSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('Radius')) + AUTH_RADIUS = serializers.BooleanField(required=False, label=_('Enable Radius Auth')) RADIUS_SERVER = serializers.CharField(required=False, allow_blank=True, max_length=1024, label=_('Host')) RADIUS_PORT = serializers.IntegerField(required=False, label=_('Port')) diff --git a/apps/settings/serializers/auth/saml2.py b/apps/settings/serializers/auth/saml2.py index d061c4936..e98a8bbe5 100644 --- a/apps/settings/serializers/auth/saml2.py +++ b/apps/settings/serializers/auth/saml2.py @@ -8,6 +8,8 @@ __all__ = [ class SAML2SettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('SAML2')) + AUTH_SAML2 = serializers.BooleanField( default=False, required=False, label=_('Enable SAML2 Auth') ) diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index dda38b586..1696be723 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -24,6 +24,8 @@ class SignTmplPairSerializer(serializers.Serializer): class BaseSMSSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('SMS') + SMS_TEST_PHONE = serializers.CharField( max_length=256, required=False, validators=[PhoneValidator(), ], allow_blank=True, label=_('Test phone') @@ -38,7 +40,7 @@ class BaseSMSSettingSerializer(serializers.Serializer): class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer): ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId') ALIBABA_ACCESS_KEY_SECRET = EncryptedField( - max_length=256, required=False, label='AccessKeySecret', + max_length=256, required=False, label='access_key_secret', ) ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature')) ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code')) diff --git a/apps/settings/serializers/auth/sso.py b/apps/settings/serializers/auth/sso.py index 113391b45..0dad5e25b 100644 --- a/apps/settings/serializers/auth/sso.py +++ b/apps/settings/serializers/auth/sso.py @@ -7,6 +7,8 @@ __all__ = [ class SSOSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('SSO')) + AUTH_SSO = serializers.BooleanField( required=False, label=_('Enable SSO auth'), help_text=_("Other service can using SSO token login to JumpServer without password") diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py index bd1498105..38516e790 100644 --- a/apps/settings/serializers/auth/wecom.py +++ b/apps/settings/serializers/auth/wecom.py @@ -7,6 +7,8 @@ __all__ = ['WeComSettingSerializer'] class WeComSettingSerializer(serializers.Serializer): + PREFIX_TITLE = '%s-%s' % (_('Authentication'), _('WeCom')) + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid') WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid') WECOM_SECRET = EncryptedField(max_length=256, required=False, label='secret') diff --git a/apps/settings/serializers/basic.py b/apps/settings/serializers/basic.py index 3208ad1be..d7d7f7ed9 100644 --- a/apps/settings/serializers/basic.py +++ b/apps/settings/serializers/basic.py @@ -24,6 +24,8 @@ class AnnouncementSerializer(serializers.Serializer): class BasicSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Basic') + SITE_URL = serializers.URLField( required=True, label=_("Site url"), help_text=_('eg: http://dev.jumpserver.org:8080') diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py index 68e8092cb..a180a9ac5 100644 --- a/apps/settings/serializers/cleaning.py +++ b/apps/settings/serializers/cleaning.py @@ -5,6 +5,8 @@ __all__ = ['CleaningSerializer'] class CleaningSerializer(serializers.Serializer): + PREFIX_TITLE = _('Period clean') + LOGIN_LOG_KEEP_DAYS = serializers.IntegerField( min_value=1, max_value=9999, label=_("Login log keep days"), help_text=_("Unit: day") diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py index c67cf2216..42618b611 100644 --- a/apps/settings/serializers/email.py +++ b/apps/settings/serializers/email.py @@ -16,6 +16,7 @@ class MailTestSerializer(serializers.Serializer): class EmailSettingSerializer(serializers.Serializer): # encrypt_fields 现在使用 write_only 来判断了 + PREFIX_TITLE = _('Email') EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host")) EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port")) @@ -46,6 +47,8 @@ class EmailSettingSerializer(serializers.Serializer): class EmailContentSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Email') + EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField( max_length=1024, allow_blank=True, required=False, label=_('Create user email subject'), diff --git a/apps/settings/serializers/other.py b/apps/settings/serializers/other.py index 775e26dd7..1eb57a673 100644 --- a/apps/settings/serializers/other.py +++ b/apps/settings/serializers/other.py @@ -3,6 +3,8 @@ from rest_framework import serializers class OtherSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('More...') + EMAIL_SUFFIX = serializers.CharField( required=False, max_length=1024, label=_("Email suffix"), help_text=_('This is used by default if no email is returned during SSO authentication') diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index 04e2b85af..7768375e4 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -40,6 +40,8 @@ class PrivateSettingSerializer(PublicSettingSerializer): TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField() TERMINAL_OMNIDB_ENABLED = serializers.BooleanField() + TERMINAL_GRAPHICAL_RESOLUTION = serializers.CharField() + ANNOUNCEMENT_ENABLED = serializers.BooleanField() ANNOUNCEMENT = serializers.DictField() diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index fb96b3502..25f1d887f 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -143,6 +143,8 @@ class SecurityAuthSerializer(serializers.Serializer): class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer): + PREFIX_TITLE = _('Security') + SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField( required=True, label=_('Enable terminal register'), help_text=_( diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 3152a5ef5..cb6472de4 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -1,4 +1,6 @@ # coding: utf-8 +from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ from .basic import BasicSettingSerializer from .other import OtherSettingSerializer @@ -7,7 +9,8 @@ from .auth import ( LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer, CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer, - TencentSMSSettingSerializer, CMPP2SMSSettingSerializer + TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer, + SAML2SettingSerializer, OAuth2SettingSerializer, SSOSettingSerializer ) from .terminal import TerminalSettingSerializer from .security import SecuritySettingSerializer @@ -22,6 +25,7 @@ __all__ = [ class SettingsSerializer( BasicSettingSerializer, LDAPSettingSerializer, + AuthSettingSerializer, TerminalSettingSerializer, SecuritySettingSerializer, WeComSettingSerializer, @@ -31,13 +35,33 @@ class SettingsSerializer( EmailContentSettingSerializer, OtherSettingSerializer, OIDCSettingSerializer, + SAML2SettingSerializer, + OAuth2SettingSerializer, KeycloakSettingSerializer, CASSettingSerializer, RadiusSettingSerializer, + SSOSettingSerializer, CleaningSerializer, AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, ): + CACHE_KEY = 'SETTING_FIELDS_MAPPING' + # encrypt_fields 现在使用 write_only 来判断了 - pass + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields_label_mapping = None + + # 单次计算量不大,搞个缓存,以防操作日志大量写入时,这里影响性能 + def get_field_label(self, field_name): + if self.fields_label_mapping is None: + self.fields_label_mapping = {} + for subclass in SettingsSerializer.__bases__: + prefix = getattr(subclass, 'PREFIX_TITLE', _('Setting')) + fields = subclass().get_fields() + for name, item in fields.items(): + label = '[%s] %s' % (prefix, getattr(item, 'label', '')) + self.fields_label_mapping[name] = label + cache.set(self.CACHE_KEY, self.fields_label_mapping, 3600 * 24) + return self.fields_label_mapping.get(field_name) diff --git a/apps/settings/serializers/terminal.py b/apps/settings/serializers/terminal.py index 1b70fadf3..757e21210 100644 --- a/apps/settings/serializers/terminal.py +++ b/apps/settings/serializers/terminal.py @@ -3,6 +3,8 @@ from rest_framework import serializers class TerminalSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Terminal') + SORT_BY_CHOICES = ( ('hostname', _('Hostname')), ('ip', _('IP')) @@ -36,3 +38,16 @@ class TerminalSettingSerializer(serializers.Serializer): TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy")) TERMINAL_RAZOR_ENABLED = serializers.BooleanField(label=_("Enable Razor")) TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField(label=_("Enable SSH Client")) + + RESOLUTION_CHOICES = ( + ('Auto', 'Auto'), + ('1024x768', '1024x768'), + ('1366x768', '1366x768'), + ('1600x900', '1600x900'), + ('1920x1080', '1920x1080') + ) + TERMINAL_GRAPHICAL_RESOLUTION = serializers.ChoiceField( + default='Auto', choices=RESOLUTION_CHOICES, required=False, + label=_('Default graphics resolution'), + help_text=_('Tip: Default resolution to use when connecting graphical assets in Luna pages') + ) diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html index 29c3ea17b..a5720aa71 100644 --- a/apps/templates/_mfa_login_field.html +++ b/apps/templates/_mfa_login_field.html @@ -16,17 +16,17 @@ {% for backend in mfa_backends %} {% endfor %} @@ -39,12 +39,12 @@ } .challenge-required .input-style { - width: calc(100% - 114px); + width: calc(100% - 160px); display: inline-block; } .btn-challenge { - width: 110px !important; + width: 156px !important; height: 100%; vertical-align: top; } @@ -74,7 +74,7 @@ localStorage.setItem(preferMFAKey, name) } - $('.input-style').each(function (i, ele){ + $('.input-style').each(function (i, ele) { $(ele).attr('name', 'code-test') }) @@ -112,7 +112,7 @@ clearInterval(interval) } }, 1000) - setTimeout(function (){ + setTimeout(function () { toastr.success("{% trans 'The verification code has been sent' %}"); }) } diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index 9938d60a4..df0577307 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -37,15 +37,21 @@ p { - {% if XPACK_ENABLED %} +
-

{% trans 'Windows Remote application publisher tools' %} v2.0

-

{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}

- +

{% trans 'Windows Remote application publisher tools' %}

+

{% trans 'OpenSSH is a program used to connect remote applications in the Windows Remote Application Publisher' %}

+ + {% if XPACK_ENABLED %} +

{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}

+ + {% endif %}
- {% endif %} +

JumpServer {% trans 'Offline video player' %} v0.1.5

diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index 4d830b1b2..3601e3894 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -1,109 +1,18 @@ # -*- coding: utf-8 -*- # import pytz -import inspect from datetime import datetime -from functools import reduce, partial -from itertools import groupby -from uuid import UUID -from django.utils.translation import gettext_lazy as _ -from django.db.models import QuerySet as DJQuerySet -from elasticsearch import Elasticsearch -from elasticsearch.helpers import bulk -from elasticsearch.exceptions import RequestError, NotFoundError - -from common.utils.common import lazyproperty from common.utils import get_logger -from common.utils.timezone import local_now_date_display, utc_now -from common.exceptions import JMSException -from terminal.models import Command +from common.plugins.es import ES + logger = get_logger(__file__) -class InvalidElasticsearch(JMSException): - default_code = 'invalid_elasticsearch' - default_detail = _('Invalid elasticsearch config') - - -class NotSupportElasticsearch8(JMSException): - default_code = 'not_support_elasticsearch8' - default_detail = _('Not Support Elasticsearch8') - - -class CommandStore(object): +class CommandStore(ES): def __init__(self, config): - self.doc_type = config.get("DOC_TYPE") or '_doc' - self.index_prefix = config.get('INDEX') or 'jumpserver' - self.is_index_by_date = bool(config.get('INDEX_BY_DATE')) - self.exact_fields = {} - self.match_fields = {} - hosts = config.get("HOSTS") - kwargs = config.get("OTHER", {}) - - ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False) - if ignore_verify_certs: - kwargs['verify_certs'] = None - self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs) - - self.exact_fields = set() - self.match_fields = {'input', 'risk_level', 'user', 'asset', 'system_user'} - may_exact_fields = {'session', 'org_id'} - - if self.is_new_index_type(): - self.exact_fields.update(may_exact_fields) - self.doc_type = '_doc' - else: - self.match_fields.update(may_exact_fields) - - self.init_index(config) - - def init_index(self, config): - if self.is_index_by_date: - date = local_now_date_display() - self.index = '%s-%s' % (self.index_prefix, date) - self.query_index = '%s-alias' % self.index_prefix - else: - self.index = config.get("INDEX") or 'jumpserver' - self.query_index = config.get("INDEX") or 'jumpserver' - - def is_new_index_type(self): - if not self.ping(timeout=3): - return False - - info = self.es.info() - version = info['version']['number'].split('.')[0] - - if version == '8': - raise NotSupportElasticsearch8 - - try: - # 获取索引信息,如果没有定义,直接返回 - data = self.es.indices.get_mapping(self.index) - except NotFoundError: - return False - - try: - if version == '6': - # 检测索引是不是新的类型 es6 - properties = data[self.index]['mappings']['data']['properties'] - else: - # 检测索引是不是新的类型 es7 default index type: _doc - properties = data[self.index]['mappings']['properties'] - if properties['session']['type'] == 'keyword' \ - and properties['org_id']['type'] == 'keyword': - return True - except KeyError: - return False - - def pre_use_check(self): - if not self.ping(timeout=3): - raise InvalidElasticsearch - self._ensure_index_exists() - - def _ensure_index_exists(self): properties = { "session": { "type": "keyword" @@ -118,25 +27,11 @@ class CommandStore(object): "type": "long" } } - info = self.es.info() - version = info['version']['number'].split('.')[0] - if version == '6': - mappings = {'mappings': {'data': {'properties': properties}}} - else: - mappings = {'mappings': {'properties': properties}} + exact_fields = {} + match_fields = {'input', 'risk_level', 'user', 'asset', 'system_user'} + keyword_fields = {'session', 'org_id'} - if self.is_index_by_date: - mappings['aliases'] = { - self.query_index: {} - } - try: - self.es.indices.create(self.index, body=mappings) - return - except RequestError as e: - if e.error == 'resource_already_exists_exception': - logger.warning(e) - else: - logger.exception(e) + super().__init__(config, properties, keyword_fields, exact_fields, match_fields) @staticmethod def make_data(command): @@ -150,274 +45,14 @@ class CommandStore(object): data["date"] = datetime.fromtimestamp(command['timestamp'], tz=pytz.UTC) return data - def bulk_save(self, command_set, raise_on_error=True): - actions = [] - for command in command_set: - data = dict( - _index=self.index, - _type=self.doc_type, - _source=self.make_data(command), - ) - actions.append(data) - return bulk(self.es, actions, index=self.index, raise_on_error=raise_on_error) - - def save(self, command): - """ - 保存命令到数据库 - """ - data = self.make_data(command) - return self.es.index(index=self.index, doc_type=self.doc_type, body=data) - - def filter(self, query: dict, from_=None, size=None, sort=None): - try: - data = self._filter(query, from_, size, sort) - except Exception as e: - logger.error('ES filter error: {}'.format(e)) - data = [] - return data - - def _filter(self, query: dict, from_=None, size=None, sort=None): - body = self.get_query_body(**query) - - data = self.es.search( - index=self.query_index, doc_type=self.doc_type, body=body, from_=from_, size=size, - sort=sort - ) - source_data = [] - for item in data['hits']['hits']: - if item: - item['_source'].update({'id': item['_id']}) - source_data.append(item['_source']) - - return Command.from_multi_dict(source_data) - - def count(self, **query): - try: - body = self.get_query_body(**query) - data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body) - count = data["count"] - except Exception as e: - logger.error('ES count error: {}'.format(e)) - count = 0 - return count - - def __getattr__(self, item): - return getattr(self.es, item) - - def all(self): - """返回所有数据""" - raise NotImplementedError("Not support") - - def ping(self, timeout=None): - try: - return self.es.ping(request_timeout=timeout) - except Exception: - return False - - def get_query_body(self, **kwargs): - new_kwargs = {} - for k, v in kwargs.items(): - new_kwargs[k] = str(v) if isinstance(v, UUID) else v - kwargs = new_kwargs - - index_in_field = 'id__in' - exact_fields = self.exact_fields - match_fields = self.match_fields - - match = {} - exact = {} - index = {} - - if index_in_field in kwargs: - index['values'] = kwargs[index_in_field] - - for k, v in kwargs.items(): - if k in exact_fields: - exact[k] = v - elif k in match_fields: - match[k] = v - - # 处理时间 - timestamp__gte = kwargs.get('timestamp__gte') - timestamp__lte = kwargs.get('timestamp__lte') + @staticmethod + def handler_time_field(data): + timestamp__gte = data.get('timestamp__gte') + timestamp__lte = data.get('timestamp__lte') timestamp_range = {} if timestamp__gte: timestamp_range['gte'] = timestamp__gte if timestamp__lte: timestamp_range['lte'] = timestamp__lte - - # 处理组织 - should = [] - org_id = match.get('org_id') - - real_default_org_id = '00000000-0000-0000-0000-000000000002' - root_org_id = '00000000-0000-0000-0000-000000000000' - - if org_id == root_org_id: - match.pop('org_id') - elif org_id in (real_default_org_id, ''): - match.pop('org_id') - should.append({ - 'bool': { - 'must_not': [ - { - 'wildcard': {'org_id': '*'} - } - ]} - }) - should.append({'match': {'org_id': real_default_org_id}}) - - # 构建 body - body = { - 'query': { - 'bool': { - 'must': [ - {'match': {k: v}} for k, v in match.items() - ], - 'should': should, - 'filter': [ - { - 'term': {k: v} - } for k, v in exact.items() - ] + [ - { - 'range': { - 'timestamp': timestamp_range - } - } - ] + [ - { - 'ids': {k: v} - } for k, v in index.items() - ] - } - }, - } - return body - - -class QuerySet(DJQuerySet): - _method_calls = None - _storage = None - _command_store_config = None - _slice = None # (from_, size) - default_days_ago = 5 - max_result_window = 10000 - - def __init__(self, command_store_config): - self._method_calls = [] - self._command_store_config = command_store_config - self._storage = CommandStore(command_store_config) - - # 命令列表模糊搜索时报错 - super().__init__() - - @lazyproperty - def _grouped_method_calls(self): - _method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])} - return _method_calls - - @lazyproperty - def _filter_kwargs(self): - _method_calls = self._grouped_method_calls - filter_calls = _method_calls.get('filter') - if not filter_calls: - return {} - names, multi_args, multi_kwargs = zip(*filter_calls) - kwargs = reduce(lambda x, y: {**x, **y}, multi_kwargs, {}) - - striped_kwargs = {} - for k, v in kwargs.items(): - k = k.replace('__exact', '') - k = k.replace('__startswith', '') - k = k.replace('__icontains', '') - striped_kwargs[k] = v - return striped_kwargs - - @lazyproperty - def _sort(self): - order_by = self._grouped_method_calls.get('order_by') - if order_by: - for call in reversed(order_by): - fields = call[1] - if fields: - field = fields[-1] - - if field.startswith('-'): - direction = 'desc' - else: - direction = 'asc' - field = field.lstrip('-+') - sort = f'{field}:{direction}' - return sort - - def __execute(self): - _filter_kwargs = self._filter_kwargs - _sort = self._sort - from_, size = self._slice or (None, None) - data = self._storage.filter(_filter_kwargs, from_=from_, size=size, sort=_sort) - return data - - def __stage_method_call(self, item, *args, **kwargs): - _clone = self.__clone() - _clone._method_calls.append((item, args, kwargs)) - return _clone - - def __clone(self): - uqs = QuerySet(self._command_store_config) - uqs._method_calls = self._method_calls.copy() - uqs._slice = self._slice - uqs.model = self.model - return uqs - - def count(self, limit_to_max_result_window=True): - filter_kwargs = self._filter_kwargs - count = self._storage.count(**filter_kwargs) - if limit_to_max_result_window: - count = min(count, self.max_result_window) - return count - - def __getattribute__(self, item): - if any(( - item.startswith('__'), - item in QuerySet.__dict__, - )): - return object.__getattribute__(self, item) - - origin_attr = object.__getattribute__(self, item) - if not inspect.ismethod(origin_attr): - return origin_attr - - attr = partial(self.__stage_method_call, item) - return attr - - def __getitem__(self, item): - max_window = self.max_result_window - if isinstance(item, slice): - if self._slice is None: - clone = self.__clone() - from_ = item.start or 0 - if item.stop is None: - size = self.max_result_window - from_ - else: - size = item.stop - from_ - - if from_ + size > max_window: - if from_ >= max_window: - from_ = max_window - size = 0 - else: - size = max_window - from_ - clone._slice = (from_, size) - return clone - return self.__execute()[item] - - def __repr__(self): - return self.__execute().__repr__() - - def __iter__(self): - return iter(self.__execute()) - - def __len__(self): - return self.count() + return 'timestamp', timestamp_range diff --git a/apps/terminal/migrations/0054_alter_session_protocol.py b/apps/terminal/migrations/0054_alter_session_protocol.py new file mode 100644 index 000000000..fe0e859f4 --- /dev/null +++ b/apps/terminal/migrations/0054_alter_session_protocol.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-04 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0053_auto_20221009_1755'), + ] + + operations = [ + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql'), ('oracle', 'oracle'), ('mariadb', 'mariadb'), ('sqlserver', 'sqlserver'), ('postgresql', 'postgresql'), ('redis', 'redis'), ('mongodb', 'MongoDB'), ('clickhouse', 'ClickHouse'), ('k8s', 'kubernetes')], db_index=True, default='ssh', max_length=16), + ), + ] diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 8eff9368b..3f6a8c338 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -38,6 +38,7 @@ class Session(OrgModelMixin): POSTGRESQL = 'postgresql', 'postgresql' REDIS = 'redis', 'redis' MONGODB = 'mongodb', 'MongoDB' + CLICKHOUSE = 'clickhouse', 'ClickHouse' K8S = 'k8s', 'kubernetes' id = models.UUIDField(default=uuid.uuid4, primary_key=True) @@ -150,7 +151,7 @@ class Session(OrgModelMixin): def db_protocols(self): _PROTOCOL = self.PROTOCOL return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.ORACLE, - _PROTOCOL.POSTGRESQL, _PROTOCOL.SQLSERVER, + _PROTOCOL.POSTGRESQL, _PROTOCOL.SQLSERVER, _PROTOCOL.CLICKHOUSE, _PROTOCOL.REDIS, _PROTOCOL.MONGODB] @property diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py index 555c44aea..c333c5498 100644 --- a/apps/terminal/models/storage.py +++ b/apps/terminal/models/storage.py @@ -10,6 +10,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.mixins import CommonModelMixin +from common.plugins.es import QuerySet as ESQuerySet from common.utils import get_logger from common.db.fields import EncryptJsonDictTextField from common.utils.timezone import local_now_date_display @@ -117,7 +118,8 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin): if self.type in TYPE_ENGINE_MAPPING: engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type]) - qs = engine_mod.QuerySet(self.config) + store = engine_mod.CommandStore(self.config) + qs = ESQuerySet(store) qs.model = Command return qs diff --git a/apps/terminal/models/terminal.py b/apps/terminal/models/terminal.py index 4b585cfff..6d7b67815 100644 --- a/apps/terminal/models/terminal.py +++ b/apps/terminal/models/terminal.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.utils import get_logger +from common.const.signals import SKIP_SIGNAL from users.models import User from orgs.utils import tmp_to_root_org from .status import Status @@ -107,8 +108,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') - user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) - is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') + user = models.OneToOneField(User, related_name='terminal', verbose_name=_('Application User'), null=True, on_delete=models.CASCADE) + is_accepted = models.BooleanField(default=False, verbose_name=_('Is Accepted')) is_deleted = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) comment = models.TextField(blank=True, verbose_name=_('Comment')) @@ -159,6 +160,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): def delete(self, using=None, keep_parents=False): if self.user: + setattr(self.user, SKIP_SIGNAL, True) self.user.delete() self.user = None self.is_deleted = True diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py index 91509a901..700bfde9b 100644 --- a/apps/terminal/notifications.py +++ b/apps/terminal/notifications.py @@ -70,6 +70,9 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage): def __init__(self, command): self.command = command + def __str__(self): + return str(self.message_type_label) + @classmethod def gen_test_msg(cls): command = Command.objects.first().to_dict() diff --git a/apps/tickets/migrations/0019_alter_applyapplicationticket_apply_type.py b/apps/tickets/migrations/0019_alter_applyapplicationticket_apply_type.py new file mode 100644 index 000000000..c4c844f71 --- /dev/null +++ b/apps/tickets/migrations/0019_alter_applyapplicationticket_apply_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-04 07:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0018_applyapplicationticket_apply_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='applyapplicationticket', + name='apply_type', + field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('clickhouse', 'ClickHouse'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py index 4917f8353..ca9db9e0c 100644 --- a/apps/users/forms/profile.py +++ b/apps/users/forms/profile.py @@ -99,8 +99,11 @@ class UserTokenResetPasswordForm(forms.Form): class UserForgotPasswordForm(forms.Form): - email = forms.EmailField(label=_("Email")) - captcha = CaptchaField(label=_("Captcha")) + username = forms.CharField(label=_("Username")) + email = forms.CharField(label=_("Email"), required=False) + phone = forms.CharField(label=_('Phone'), required=False, max_length=11) + code = forms.CharField(label=_('Verify code'), max_length=6, required=False) + form_type = forms.CharField(widget=forms.HiddenInput({'value': 'email'})) class UserPasswordForm(UserTokenResetPasswordForm): diff --git a/apps/users/migrations/0003_auto_20180101_0046.py b/apps/users/migrations/0003_auto_20180101_0046.py index 46a85be54..6795a1782 100644 --- a/apps/users/migrations/0003_auto_20180101_0046.py +++ b/apps/users/migrations/0003_auto_20180101_0046.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='is_first_login', - field=models.BooleanField(default=True), + field=models.BooleanField(default=True, verbose_name='Is first login'), ), ] diff --git a/apps/users/migrations/0021_auto_20190625_1104.py b/apps/users/migrations/0021_auto_20190625_1104.py index 4112a8693..f2575b933 100644 --- a/apps/users/migrations/0021_auto_20190625_1104.py +++ b/apps/users/migrations/0021_auto_20190625_1104.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='_otp_secret_key', - field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True), + field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True, verbose_name='OTP secret key'), ), migrations.AlterField( model_name='user', diff --git a/apps/users/migrations/0032_userpasswordhistory.py b/apps/users/migrations/0032_userpasswordhistory.py index 8fcf1d2d1..5088b7968 100644 --- a/apps/users/migrations/0032_userpasswordhistory.py +++ b/apps/users/migrations/0032_userpasswordhistory.py @@ -1,8 +1,8 @@ # Generated by Django 3.1 on 2021-04-27 12:43 +import common.db.models from django.conf import settings from django.db import migrations, models -import django.db.models.deletion import uuid @@ -19,7 +19,7 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('password', models.CharField(max_length=128)), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')), ], ), ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 6830c2ac5..f08a487c2 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -16,11 +16,12 @@ from django.contrib.auth.models import AbstractUser from django.contrib.auth.hashers import check_password from django.utils.translation import ugettext_lazy as _ from django.shortcuts import reverse +from django.utils.module_loading import import_string from orgs.utils import current_org from orgs.models import Organization from rbac.const import Scope -from common.db import fields +from common.db import fields, models as jms_models from common.utils import ( date_expired_default, get_logger, lazyproperty, random_string, bulk_create_with_signal ) @@ -602,8 +603,11 @@ class MFAMixin: @staticmethod def get_user_mfa_backends(user): - from authentication.mfa import MFA_BACKENDS - backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()] + backends = [] + for cls in settings.MFA_BACKENDS: + cls = import_string(cls) + if cls.global_enabled(): + backends.append(cls(user)) return backends def get_active_mfa_backend_by_type(self, mfa_type): @@ -691,7 +695,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): mfa_level = models.SmallIntegerField( default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA') ) - otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) + otp_secret_key = fields.EncryptCharField( + max_length=128, blank=True, null=True, verbose_name=_('OTP secret key') + ) # Todo: Auto generate key, let user download private_key = fields.EncryptTextField( blank=True, null=True, verbose_name=_('Private key') @@ -705,7 +711,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): comment = models.TextField( blank=True, null=True, verbose_name=_('Comment') ) - is_first_login = models.BooleanField(default=True) + is_first_login = models.BooleanField(default=True, verbose_name=_('Is first login')) date_expired = models.DateTimeField( default=date_expired_default, blank=True, null=True, db_index=True, verbose_name=_('Date expired') @@ -927,7 +933,7 @@ class UserPasswordHistory(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) password = models.CharField(max_length=128) user = models.ForeignKey("users.User", related_name='history_passwords', - on_delete=models.CASCADE, verbose_name=_('User')) + on_delete=jms_models.CASCADE_SIGNAL_SKIP, verbose_name=_('User')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) def __str__(self): diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index 16c784250..a5770fdcc 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -4,12 +4,29 @@ {% load bootstrap3 %} {% block custom_head_css_js %} {% endblock %} @@ -17,22 +34,111 @@ {% block title %} {% trans 'Forgot password' %}?{% endblock %} {% block content %} - {% if errors %} -

{{ errors }}

- {% endif %} -

+

{% trans 'Input your email, that will send a mail to your' %}

+

+ Enter your phone number and a verification code will be sent to your phone +

{% csrf_token %} - {% bootstrap_field form.email layout="horizontal" %} - {% bootstrap_field form.captcha layout="horizontal" %} - + {% bootstrap_field form.form_type layout="horizontal" %} +
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ +
+ {% if XPACK_ENABLED %} + {% trans 'Use the phone number to retrieve the password' %} + {% trans 'Use email to retrieve the password' %} + {% endif %}
+ {% endblock %} diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index cc14f6d53..bef98789a 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -4,15 +4,13 @@ from __future__ import unicode_literals from django.views.generic import RedirectView from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ -from django.views.generic.base import TemplateView from django.conf import settings from django.urls import reverse_lazy from django.views.generic import FormView -from users.notifications import ResetPasswordSuccessMsg, ResetPasswordMsg +from users.notifications import ResetPasswordSuccessMsg from common.utils import get_object_or_none, FlashMessageUtil -from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.utils.verify_code import SendAndVerifyCodeUtil from ...models import User from ...utils import ( get_password_check_rules, check_password_rules, @@ -34,35 +32,49 @@ class UserForgotPasswordView(FormView): template_name = 'users/forgot_password.html' form_class = forms.UserForgotPasswordForm + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = context['form'] + if getattr(form, 'errors', None): + e = getattr(form, 'errors') + context['errors'] = e + context['form_type'] = 'email' + context['XPACK_ENABLED'] = settings.XPACK_ENABLED + cleaned_data = getattr(form, 'cleaned_data', {}) + for k, v in cleaned_data.items(): + if v: + context[k] = v + return context + @staticmethod - def get_redirect_message_url(): - message_data = { - 'title': _('Send reset password message'), - 'message': _('Send reset password mail success, ' - 'login your mail box and follow it '), - 'redirect_url': reverse('authentication:login'), - } - url = FlashMessageUtil.gen_message_url(message_data) - return url + def get_redirect_url(user): + query_params = '?token=%s' % user.generate_reset_token() + reset_password_url = reverse('authentication:reset-password') + return reset_password_url + query_params def form_valid(self, form): - email = form.cleaned_data['email'] - user = get_object_or_none(User, email=email) + form_type = form.cleaned_data['form_type'] + code = form.cleaned_data['code'] + username = form.cleaned_data['username'] + if settings.XPACK_ENABLED and form_type == 'phone': + backend = 'sms' + target = form.cleaned_data['phone'] + else: + backend = 'email' + target = form.cleaned_data['email'] + try: + sender_util = SendAndVerifyCodeUtil(target, backend=backend) + sender_util.verify(code) + except Exception as e: + form.add_error('code', str(e)) + return super().form_invalid(form) + + user = get_object_or_none(User, **{'username': username, form_type: target}) if not user: - error = _('Email address invalid, please input again') - form.add_error('email', error) - return self.form_invalid(form) + form.add_error('username', _('User does not exist: {}').format(username)) + return super().form_invalid(form) - if not user.is_local: - error = _( - 'The user is from {}, please go to the corresponding system to change the password' - ).format(user.get_source_display()) - form.add_error('email', error) - return self.form_invalid(form) - - ResetPasswordMsg(user).publish_async() - url = self.get_redirect_message_url() - return redirect(url) + return redirect(self.get_redirect_url(user)) class UserResetPasswordView(FormView): diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5e94c69e5..26c277dcc 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -37,6 +37,7 @@ jms-storage==0.0.44 simplejson==3.17.6 six==1.16.0 sshpubkeys==3.3.1 +sshtunnel==0.4.0 uritemplate==4.1.1 urllib3==1.26.9 vine==5.0.0 @@ -62,7 +63,7 @@ jsonfield2==4.0.0.post0 geoip2==4.5.0 ipip-ipdb==1.6.1 # Django environment -Django==3.2.15 +Django==3.2.16 django-bootstrap3==14.2.0 django-filter==2.4.0 django-formtools==2.2 @@ -83,7 +84,7 @@ Pillow==9.1.1 pytz==2022.1 # Runtime django-proxy==1.2.1 -channels-redis==3.4.0 +channels-redis==4.0.0 channels==3.0.4 daphne==3.0.2 python-daemon==2.3.0 diff --git a/utils/start_celery_beat.py b/utils/start_celery_beat.py index f6259ddcc..247906f5c 100644 --- a/utils/start_celery_beat.py +++ b/utils/start_celery_beat.py @@ -6,7 +6,8 @@ import signal import subprocess import redis_lock -from redis import Redis + +from redis import Redis, Sentinel BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) APPS_DIR = os.path.join(BASE_DIR, 'apps') @@ -19,18 +20,35 @@ os.environ.setdefault('PYTHONOPTIMIZE', '1') if os.getuid() == 0: os.environ.setdefault('C_FORCE_ROOT', '1') -params = { - 'host': settings.REDIS_HOST, - 'port': settings.REDIS_PORT, +connection_params = { 'password': settings.REDIS_PASSWORD, - 'ssl': settings.REDIS_USE_SSL, - 'ssl_cert_reqs': settings.REDIS_SSL_REQUIRED, - 'ssl_keyfile': settings.REDIS_SSL_KEY, - 'ssl_certfile': settings.REDIS_SSL_CERT, - 'ssl_ca_certs': settings.REDIS_SSL_CA } -print("Pamras: ", params) -redis = Redis(**params) + +if settings.REDIS_USE_SSL: + connection_params['ssl'] = settings.REDIS_USE_SSL + connection_params['ssl_cert_reqs'] = settings.REDIS_SSL_REQUIRED + connection_params['ssl_keyfile'] = settings.REDIS_SSL_KEY + connection_params['ssl_certfile'] = settings.REDIS_SSL_CERT + connection_params['ssl_ca_certs'] = settings.REDIS_SSL_CA + +REDIS_SENTINEL_SERVICE_NAME = settings.REDIS_SENTINEL_SERVICE_NAME +REDIS_SENTINELS = settings.REDIS_SENTINELS +REDIS_SENTINEL_PASSWORD = settings.REDIS_SENTINEL_PASSWORD +REDIS_SENTINEL_SOCKET_TIMEOUT = settings.REDIS_SENTINEL_SOCKET_TIMEOUT +if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: + connection_params['sentinels'] = REDIS_SENTINELS + sentinel_client = Sentinel( + **connection_params, sentinel_kwargs={ + 'password': REDIS_SENTINEL_PASSWORD, + 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT + } + ) + redis_client = sentinel_client.master_for(REDIS_SENTINEL_SERVICE_NAME) +else: + connection_params['host'] = settings.REDIS_HOST + connection_params['port'] = settings.REDIS_PORT + redis_client = Redis(**connection_params) + scheduler = "django_celery_beat.schedulers:DatabaseScheduler" processes = [] cmd = [ @@ -52,7 +70,7 @@ def main(): # 父进程结束通知子进程结束 signal.signal(signal.SIGTERM, stop_beat_process) - with redis_lock.Lock(redis, name="beat-distribute-start-lock", expire=60, auto_renewal=True): + with redis_lock.Lock(redis_client, name="beat-distribute-start-lock", expire=60, auto_renewal=True): print("Get beat lock start to run it") process = subprocess.Popen(cmd, cwd=APPS_DIR) processes.append(process)