diff --git a/Dockerfile b/Dockerfile index 2174df9c0..581f40f18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,9 +36,11 @@ ARG TOOLS=" \ curl \ default-libmysqlclient-dev \ default-mysql-client \ + iputils-ping \ locales \ nmap \ openssh-client \ + patch \ sshpass \ telnet \ vim \ diff --git a/README.md b/README.md index e83f8ee8f..c445740a2 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@

- JumpServer v3.0 正式发布。 -
9 年时间,倾情投入,用心做好一款开源堡垒机。

diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index 740ccf041..8cfeeec8c 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -6,11 +6,12 @@ from rest_framework.status import HTTP_200_OK from accounts import serializers from accounts.filters import AccountFilterSet -from accounts.models import Account from accounts.mixins import AccountRecordViewLogMixin +from accounts.models import Account from assets.models import Asset, Node +from authentication.permissions import UserConfirmation, ConfirmType from common.api.mixin import ExtraFilterFieldsMixin -from common.permissions import UserConfirmation, ConfirmType, IsValidUser +from common.permissions import IsValidUser from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission @@ -57,19 +58,19 @@ class AccountViewSet(OrgBulkModelViewSet): permission_classes=[IsValidUser] ) def username_suggestions(self, request, *args, **kwargs): - asset_ids = request.data.get('assets') - node_ids = request.data.get('nodes') - username = request.data.get('username') + asset_ids = request.data.get('assets', []) + node_ids = request.data.get('nodes', []) + username = request.data.get('username', '') - assets = Asset.objects.all() - if asset_ids: - assets = assets.filter(id__in=asset_ids) + accounts = Account.objects.all() if node_ids: nodes = Node.objects.filter(id__in=node_ids) node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) - assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids))) + asset_ids.extend(node_asset_ids) + + if asset_ids: + accounts = accounts.filter(asset_id__in=list(set(asset_ids))) - accounts = Account.objects.filter(asset__in=assets) if username: accounts = accounts.filter(username__icontains=username) usernames = list(accounts.values_list('username', flat=True).distinct()[:10]) diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index f9c3637f8..22d9508a7 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,13 +1,15 @@ from django_filters import rest_framework as drf_filters +from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from accounts import serializers -from accounts.models import AccountTemplate from accounts.mixins import AccountRecordViewLogMixin +from accounts.models import AccountTemplate +from accounts.tasks import template_sync_related_accounts from assets.const import Protocol +from authentication.permissions import UserConfirmation, ConfirmType from common.drf.filters import BaseFilterSet -from common.permissions import UserConfirmation, ConfirmType from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission @@ -44,6 +46,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): } rbac_perms = { 'su_from_account_templates': 'accounts.view_accounttemplate', + 'sync_related_accounts': 'accounts.change_account', } @action(methods=['get'], detail=False, url_path='su-from-account-templates') @@ -54,6 +57,13 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): serializer = self.get_serializer(templates, many=True) return Response(data=serializer.data) + @action(methods=['patch'], detail=True, url_path='sync-related-accounts') + def sync_related_accounts(self, request, *args, **kwargs): + instance = self.get_object() + user_id = str(request.user.id) + task = template_sync_related_accounts.delay(str(instance.id), user_id) + return Response({'task': task.id}, status=status.HTTP_200_OK) + class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet): serializer_classes = { diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py index 2af9afd09..e334403f5 100644 --- a/apps/accounts/api/automations/change_secret.py +++ b/apps/accounts/api/automations/change_secret.py @@ -5,8 +5,7 @@ from rest_framework import mixins from accounts import serializers from accounts.const import AutomationTypes -from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution -from common.utils import get_object_or_none +from accounts.models import ChangeSecretAutomation, ChangeSecretRecord from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from .base import ( AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, @@ -30,8 +29,8 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): serializer_class = serializers.ChangeSecretRecordSerializer - filter_fields = ['asset', 'execution_id'] - search_fields = ['asset__hostname'] + filter_fields = ('asset', 'execution_id') + search_fields = ('asset__address',) def get_queryset(self): return ChangeSecretRecord.objects.filter( @@ -41,10 +40,7 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) eid = self.request.query_params.get('execution_id') - execution = get_object_or_none(AutomationExecution, pk=eid) - if execution: - queryset = queryset.filter(execution=execution) - return queryset + return queryset.filter(execution_id=eid) class ChangSecretExecutionViewSet(AutomationExecutionViewSet): diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml index b35d2175a..7e30bf62b 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/main.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml @@ -47,4 +47,8 @@ login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - become: false + become: "{{ account.become.ansible_become | default(False) }}" + become_method: su + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml index 56fde8a2c..e86319ce1 100644 --- a/apps/accounts/automations/change_secret/host/aix/main.yml +++ b/apps/accounts/automations/change_secret/host/aix/main.yml @@ -80,7 +80,11 @@ login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false + become: "{{ account.become.ansible_become | default(False) }}" + become_method: su + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" when: account.secret_type == "password" delegate_to: localhost @@ -91,6 +95,5 @@ login_user: "{{ account.username }}" login_private_key_path: "{{ account.private_key_path }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false when: account.secret_type == "ssh_key" delegate_to: localhost diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml index 1dca70a5a..6ac4b7aa9 100644 --- a/apps/accounts/automations/change_secret/host/posix/main.yml +++ b/apps/accounts/automations/change_secret/host/posix/main.yml @@ -80,7 +80,11 @@ login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false + become: "{{ account.become.ansible_become | default(False) }}" + become_method: su + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" when: account.secret_type == "password" delegate_to: localhost @@ -91,6 +95,5 @@ login_user: "{{ account.username }}" login_private_key_path: "{{ account.private_key_path }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false when: account.secret_type == "ssh_key" delegate_to: localhost diff --git a/apps/accounts/automations/push_account/host/aix/main.yml b/apps/accounts/automations/push_account/host/aix/main.yml index 0e6fba5c5..b0256348c 100644 --- a/apps/accounts/automations/push_account/host/aix/main.yml +++ b/apps/accounts/automations/push_account/host/aix/main.yml @@ -80,7 +80,11 @@ login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false + become: "{{ account.become.ansible_become | default(False) }}" + become_method: su + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" when: account.secret_type == "password" delegate_to: localhost @@ -91,7 +95,6 @@ login_user: "{{ account.username }}" login_private_key_path: "{{ account.private_key_path }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false when: account.secret_type == "ssh_key" delegate_to: localhost diff --git a/apps/accounts/automations/push_account/host/posix/main.yml b/apps/accounts/automations/push_account/host/posix/main.yml index ea5128b17..2d2cc8e3e 100644 --- a/apps/accounts/automations/push_account/host/posix/main.yml +++ b/apps/accounts/automations/push_account/host/posix/main.yml @@ -80,7 +80,11 @@ login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false + become: "{{ account.become.ansible_become | default(False) }}" + become_method: su + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" when: account.secret_type == "password" delegate_to: localhost @@ -91,7 +95,6 @@ login_user: "{{ account.username }}" login_private_key_path: "{{ account.private_key_path }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}" - become: false when: account.secret_type == "ssh_key" delegate_to: localhost diff --git a/apps/accounts/automations/verify_account/custom/rdp/main.yml b/apps/accounts/automations/verify_account/custom/rdp/main.yml index b0c7cbe4f..017f4bab3 100644 --- a/apps/accounts/automations/verify_account/custom/rdp/main.yml +++ b/apps/accounts/automations/verify_account/custom/rdp/main.yml @@ -8,7 +8,7 @@ - name: Verify account (pyfreerdp) rdp_ping: login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" + login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index 4e35b9587..c565f6f83 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -13,8 +13,8 @@ login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" login_private_key_path: "{{ account.private_key_path }}" - become: "{{ custom_become | default(False) }}" - become_method: "{{ custom_become_method | default('su') }}" - become_user: "{{ custom_become_user | default('') }}" - become_password: "{{ custom_become_password | default('') }}" - become_private_key_path: "{{ custom_become_private_key_path | default(None) }}" + become: "{{ account.become.ansible_become | default(False) }}" + become_method: "{{ account.become.ansible_become_method | default('su') }}" + become_user: "{{ account.become.ansible_user | default('') }}" + become_password: "{{ account.become.ansible_password | default('') }}" + become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" diff --git a/apps/accounts/automations/verify_account/host/posix/main.yml b/apps/accounts/automations/verify_account/host/posix/main.yml index b096f9d84..49e84e5ee 100644 --- a/apps/accounts/automations/verify_account/host/posix/main.yml +++ b/apps/accounts/automations/verify_account/host/posix/main.yml @@ -1,11 +1,23 @@ - hosts: demo gather_facts: no tasks: - - name: Verify account connectivity - become: no + - name: Verify account connectivity(Do not switch) ansible.builtin.ping: vars: ansible_become: no ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}" + when: not account.become.ansible_become + + - name: Verify account connectivity(Switch) + ansible.builtin.ping: + vars: + ansible_become: yes + ansible_user: "{{ account.become.ansible_user }}" + ansible_password: "{{ account.become.ansible_password }}" + ansible_ssh_private_key_file: "{{ account.become.ansible_ssh_private_key_file }}" + ansible_become_method: "{{ account.become.ansible_become_method }}" + ansible_become_user: "{{ account.become.ansible_become_user }}" + ansible_become_password: "{{ account.become.ansible_become_password }}" + when: account.become.ansible_become diff --git a/apps/accounts/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py index b0e4a10ab..18478fb21 100644 --- a/apps/accounts/automations/verify_account/manager.py +++ b/apps/accounts/automations/verify_account/manager.py @@ -42,7 +42,6 @@ class VerifyAccountManager(AccountBasePlaybookManager): if host.get('error'): return host - # host['ssh_args'] = '-o ControlMaster=no -o ControlPersist=no' accounts = asset.accounts.all() accounts = self.get_accounts(account, accounts) inventory_hosts = [] @@ -64,7 +63,8 @@ class VerifyAccountManager(AccountBasePlaybookManager): 'username': account.username, 'secret_type': account.secret_type, 'secret': secret, - 'private_key_path': private_key_path + 'private_key_path': private_key_path, + 'become': account.get_ansible_become_auth(), } if account.platform.type == 'oracle': h['account']['mode'] = 'sysdba' if account.privileged else None diff --git a/apps/accounts/migrations/0004_auto_20230106_1507.py b/apps/accounts/migrations/0004_auto_20230106_1507.py index 3be64ef5a..6392fce3b 100644 --- a/apps/accounts/migrations/0004_auto_20230106_1507.py +++ b/apps/accounts/migrations/0004_auto_20230106_1507.py @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='changesecretautomation', name='secret_strategy', - field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'), ), migrations.AlterField( model_name='pushaccountautomation', name='secret_strategy', - field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'), ), ] diff --git a/apps/accounts/migrations/0015_auto_20230825_1120.py b/apps/accounts/migrations/0015_auto_20230825_1120.py index 083c88634..533613855 100644 --- a/apps/accounts/migrations/0015_auto_20230825_1120.py +++ b/apps/accounts/migrations/0015_auto_20230825_1120.py @@ -29,6 +29,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='accounttemplate', name='secret_strategy', - field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'), ), ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 1bdf6e83d..644961ff7 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -95,6 +95,33 @@ class Account(AbsConnectivity, BaseAccount): """ 排除自己和以自己为 su-from 的账号 """ return self.asset.accounts.exclude(id=self.id).exclude(su_from=self) + @staticmethod + def make_account_ansible_vars(su_from): + var = { + 'ansible_user': su_from.username, + } + if not su_from.secret: + return var + var['ansible_password'] = su_from.secret + var['ansible_ssh_private_key_file'] = su_from.private_key_path + return var + + def get_ansible_become_auth(self): + su_from = self.su_from + platform = self.platform + auth = {'ansible_become': False} + if not (platform.su_enabled and su_from): + return auth + + auth.update(self.make_account_ansible_vars(su_from)) + become_method = 'sudo' if platform.su_method != 'su' else 'su' + password = su_from.secret if become_method == 'sudo' else self.secret + auth['ansible_become'] = True + auth['ansible_become_method'] = become_method + auth['ansible_become_user'] = self.username + auth['ansible_become_password'] = password + return auth + def replace_history_model_with_mixin(): """ diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py index 84aa1bb6e..99bbda01d 100644 --- a/apps/accounts/models/automations/push_account.py +++ b/apps/accounts/models/automations/push_account.py @@ -1,9 +1,9 @@ +from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ from accounts.const import AutomationTypes from accounts.models import Account -from jumpserver.utils import has_valid_xpack_license from .base import AccountBaseAutomation from .change_secret import ChangeSecretMixin @@ -41,7 +41,7 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.push_account - if not has_valid_xpack_license(): + if not settings.XPACK_LICENSE_IS_VALID: self.is_periodic = False super().save(*args, **kwargs) diff --git a/apps/accounts/models/mixins/vault.py b/apps/accounts/models/mixins/vault.py index b16927b62..d61bc18bb 100644 --- a/apps/accounts/models/mixins/vault.py +++ b/apps/accounts/models/mixins/vault.py @@ -37,8 +37,9 @@ class VaultManagerMixin(models.Manager): post_save.send(obj.__class__, instance=obj, created=True) return objs - def bulk_update(self, objs, batch_size=None, ignore_conflicts=False): - objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) + def bulk_update(self, objs, fields, batch_size=None): + fields = ["_secret" if field == "secret" else field for field in fields] + super().bulk_update(objs, fields, batch_size=batch_size) for obj in objs: post_save.send(obj.__class__, instance=obj, created=False) return objs diff --git a/apps/accounts/models/template.py b/apps/accounts/models/template.py index 0834dcb03..c56be1464 100644 --- a/apps/accounts/models/template.py +++ b/apps/accounts/models/template.py @@ -49,8 +49,7 @@ class AccountTemplate(BaseAccount, SecretWithRandomMixin): ).first() return account - @staticmethod - def bulk_update_accounts(accounts, data): + def bulk_update_accounts(self, accounts): history_model = Account.history.model account_ids = accounts.values_list('id', flat=True) history_accounts = history_model.objects.filter(id__in=account_ids) @@ -63,8 +62,7 @@ class AccountTemplate(BaseAccount, SecretWithRandomMixin): for account in accounts: account_id = str(account.id) account.version = account_id_count_map.get(account_id) + 1 - for k, v in data.items(): - setattr(account, k, v) + account.secret = self.get_secret() Account.objects.bulk_update(accounts, ['version', 'secret']) @staticmethod @@ -86,7 +84,5 @@ class AccountTemplate(BaseAccount, SecretWithRandomMixin): def bulk_sync_account_secret(self, accounts, user_id): """ 批量同步账号密码 """ - if not accounts: - return - self.bulk_update_accounts(accounts, {'secret': self.secret}) + self.bulk_update_accounts(accounts) self.bulk_create_history_accounts(accounts, user_id) diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index 0864499b6..8b8d9be36 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -78,7 +78,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): def get_template_attr_for_account(template): # Set initial data from template field_names = [ - 'username', 'secret', 'secret_type', 'privileged', 'is_active' + 'name', 'username', 'secret', + 'secret_type', 'privileged', 'is_active' ] attrs = {} diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index 149760908..635c221bf 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,7 +1,9 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from accounts.models import AccountTemplate, Account +from accounts.const import SecretStrategy, SecretType +from accounts.models import AccountTemplate +from accounts.utils import SecretGenerator from common.serializers import SecretReadableMixin from common.serializers.fields import ObjectRelatedField from .base import BaseAccountSerializer @@ -16,9 +18,6 @@ class PasswordRulesSerializer(serializers.Serializer): class AccountTemplateSerializer(BaseAccountSerializer): - is_sync_account = serializers.BooleanField(default=False, write_only=True) - _is_sync_account = False - password_rules = PasswordRulesSerializer(required=False, label=_('Password rules')) su_from = ObjectRelatedField( required=False, queryset=AccountTemplate.objects, allow_null=True, @@ -30,7 +29,7 @@ class AccountTemplateSerializer(BaseAccountSerializer): fields = BaseAccountSerializer.Meta.fields + [ 'secret_strategy', 'password_rules', 'auto_push', 'push_params', 'platforms', - 'is_sync_account', 'su_from' + 'su_from' ] extra_kwargs = { 'secret_strategy': {'help_text': _('Secret generation strategy for account creation')}, @@ -44,34 +43,21 @@ class AccountTemplateSerializer(BaseAccountSerializer): }, } - def sync_accounts_secret(self, instance, diff): - if not self._is_sync_account or 'secret' not in diff: + @staticmethod + def generate_secret(attrs): + secret_type = attrs.get('secret_type', SecretType.PASSWORD) + secret_strategy = attrs.get('secret_strategy', SecretStrategy.custom) + password_rules = attrs.get('password_rules') + if secret_strategy != SecretStrategy.random: return - query_data = { - 'source_id': instance.id, - 'username': instance.username, - 'secret_type': instance.secret_type - } - accounts = Account.objects.filter(**query_data) - instance.bulk_sync_account_secret(accounts, self.context['request'].user.id) + generator = SecretGenerator(secret_strategy, secret_type, password_rules) + attrs['secret'] = generator.get_secret() def validate(self, attrs): - self._is_sync_account = attrs.pop('is_sync_account', None) attrs = super().validate(attrs) + self.generate_secret(attrs) return attrs - def update(self, instance, validated_data): - diff = { - k: v for k, v in validated_data.items() - if getattr(instance, k, None) != v - } - instance = super().update(instance, validated_data) - if {'username', 'secret_type'} & set(diff.keys()): - Account.objects.filter(source_id=instance.id).update(source_id=None) - else: - self.sync_accounts_secret(instance, diff) - return instance - class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer): class Meta(AccountTemplateSerializer.Meta): diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py index 055508b24..8a79c1aa4 100644 --- a/apps/accounts/tasks/__init__.py +++ b/apps/accounts/tasks/__init__.py @@ -1,5 +1,6 @@ -from .backup_account import * from .automation import * -from .push_account import * -from .verify_account import * +from .backup_account import * from .gather_accounts import * +from .push_account import * +from .template import * +from .verify_account import * diff --git a/apps/accounts/tasks/template.py b/apps/accounts/tasks/template.py new file mode 100644 index 000000000..dbad2cf06 --- /dev/null +++ b/apps/accounts/tasks/template.py @@ -0,0 +1,60 @@ +from datetime import datetime + +from celery import shared_task +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from orgs.utils import tmp_to_root_org, tmp_to_org + + +@shared_task( + verbose_name=_('Template sync info to related accounts'), + activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None) +) +def template_sync_related_accounts(template_id, user_id=None): + from accounts.models import Account, AccountTemplate + with tmp_to_root_org(): + template = get_object_or_404(AccountTemplate, id=template_id) + org_id = template.org_id + + with tmp_to_org(org_id): + accounts = Account.objects.filter(source_id=template_id) + if not accounts: + print('\033[35m>>> 没有需要同步的账号, 结束任务') + print('\033[0m') + return + + failed, succeeded = 0, 0 + succeeded_account_ids = [] + name = template.name + username = template.username + secret_type = template.secret_type + print(f'\033[32m>>> 开始同步模版名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})') + with tmp_to_org(org_id): + for account in accounts: + account.name = name + account.username = username + account.secret_type = secret_type + try: + account.save(update_fields=['name', 'username', 'secret_type']) + succeeded += 1 + succeeded_account_ids.append(account.id) + except Exception as e: + account.source_id = None + account.save(update_fields=['source_id']) + print(f'\033[31m- 同步失败: [{account}] 原因: [{e}]') + failed += 1 + accounts = Account.objects.filter(id__in=succeeded_account_ids) + if accounts: + print(f'\033[33m>>> 批量更新账号密文 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})') + template.bulk_sync_account_secret(accounts, user_id) + + total = succeeded + failed + print( + f'\033[33m>>> 同步完成:, ' + f'共计: {total}, ' + f'成功: {succeeded}, ' + f'失败: {failed}, ' + f'({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) ' + ) + print('\033[0m') diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py index fb3be63a7..ac896f8c0 100644 --- a/apps/accounts/utils.py +++ b/apps/accounts/utils.py @@ -16,7 +16,7 @@ class SecretGenerator: @staticmethod def generate_ssh_key(): - private_key, public_key = ssh_key_gen() + private_key, __ = ssh_key_gen() return private_key def generate_password(self): diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index 182b881eb..2043f274d 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -10,7 +10,7 @@ __all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet'] class CommandGroupViewSet(OrgBulkModelViewSet): model = models.CommandGroup - filterset_fields = ('name',) + filterset_fields = ('name', 'command_filters') search_fields = filterset_fields serializer_class = serializers.CommandGroupSerializer diff --git a/apps/acls/const.py b/apps/acls/const.py index c2d2be586..cccc906f4 100644 --- a/apps/acls/const.py +++ b/apps/acls/const.py @@ -7,3 +7,4 @@ class ActionChoices(models.TextChoices): accept = 'accept', _('Accept') review = 'review', _('Review') warning = 'warning', _('Warning') + notice = 'notice', _('Notifications') diff --git a/apps/acls/migrations/0018_alter_commandfilteracl_command_groups.py b/apps/acls/migrations/0018_alter_commandfilteracl_command_groups.py new file mode 100644 index 000000000..0dea952a8 --- /dev/null +++ b/apps/acls/migrations/0018_alter_commandfilteracl_command_groups.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-10-18 10:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('acls', '0017_alter_connectmethodacl_options'), + ] + + operations = [ + migrations.AlterField( + model_name='commandfilteracl', + name='command_groups', + field=models.ManyToManyField(related_name='command_filters', to='acls.commandgroup', verbose_name='Command group'), + ), + ] diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index 4fbfd0829..2011cc60a 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -93,7 +93,10 @@ class CommandGroup(JMSOrgBaseModel): class CommandFilterACL(UserAssetAccountBaseACL): - command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Command group')) + command_groups = models.ManyToManyField( + CommandGroup, verbose_name=_('Command group'), + related_name='command_filters' + ) class Meta(UserAssetAccountBaseACL.Meta): abstract = False diff --git a/apps/acls/notifications.py b/apps/acls/notifications.py new file mode 100644 index 000000000..cf19b7a51 --- /dev/null +++ b/apps/acls/notifications.py @@ -0,0 +1,68 @@ +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ + +from assets.models import Asset +from audits.models import UserLoginLog +from notifications.notifications import UserMessage +from users.models import User + + +class UserLoginReminderMsg(UserMessage): + subject = _('User login reminder') + + def __init__(self, user, user_log: UserLoginLog): + self.user_log = user_log + super().__init__(user) + + def get_html_msg(self) -> dict: + user_log = self.user_log + + context = { + 'ip': user_log.ip, + 'city': user_log.city, + 'username': user_log.username, + 'recipient': self.user.username, + 'user_agent': user_log.user_agent, + } + message = render_to_string('acls/user_login_reminder.html', context) + + return { + 'subject': str(self.subject), + 'message': message + } + + @classmethod + def gen_test_msg(cls): + user = User.objects.first() + user_log = UserLoginLog.objects.first() + return cls(user, user_log) + + +class AssetLoginReminderMsg(UserMessage): + subject = _('Asset login reminder') + + def __init__(self, user, asset: Asset, login_user: User, account_username): + self.asset = asset + self.login_user = login_user + self.account_username = account_username + super().__init__(user) + + def get_html_msg(self) -> dict: + context = { + 'recipient': self.user.username, + 'username': self.login_user.username, + 'asset': str(self.asset), + 'account': self.account_username, + } + message = render_to_string('acls/asset_login_reminder.html', context) + + return { + 'subject': str(self.subject), + 'message': message + } + + @classmethod + def gen_test_msg(cls): + user = User.objects.first() + asset = Asset.objects.first() + return cls(user, asset, user) diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index 4c2d80bfc..09f75bf42 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -1,9 +1,9 @@ +from django.conf import settings from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from acls.models.base import BaseACL from common.serializers.fields import JSONManyToManyField, LabeledChoiceField -from jumpserver.utils import has_valid_xpack_license from orgs.models import Organization from ..const import ActionChoices @@ -68,7 +68,7 @@ class ActionAclSerializer(serializers.Serializer): field_action = self.fields.get("action") if not field_action: return - if not has_valid_xpack_license(): + if not settings.XPACK_LICENSE_IS_VALID: field_action._choices.pop(ActionChoices.review, None) for choice in self.Meta.action_choices_exclude: field_action._choices.pop(choice, None) diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py index dde953277..44fd8e0ed 100644 --- a/apps/acls/serializers/command_acl.py +++ b/apps/acls/serializers/command_acl.py @@ -8,6 +8,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.utils import tmp_to_root_org from terminal.models import Session from .base import BaseUserAssetAccountACLSerializer as BaseSerializer +from ..const import ActionChoices __all__ = ["CommandFilterACLSerializer", "CommandGroupSerializer", "CommandReviewSerializer"] @@ -31,8 +32,7 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) class Meta(BaseSerializer.Meta): model = CommandFilterACL fields = BaseSerializer.Meta.fields + ['command_groups'] - # 默认都支持所有的 actions - action_choices_exclude = [] + action_choices_exclude = [ActionChoices.notice] class CommandReviewSerializer(serializers.Serializer): diff --git a/apps/acls/serializers/connect_method.py b/apps/acls/serializers/connect_method.py index b1daf56ac..917933dcb 100644 --- a/apps/acls/serializers/connect_method.py +++ b/apps/acls/serializers/connect_method.py @@ -1,7 +1,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .base import BaseUserAssetAccountACLSerializer as BaseSerializer -from ..models import ConnectMethodACL from ..const import ActionChoices +from ..models import ConnectMethodACL __all__ = ["ConnectMethodACLSerializer"] @@ -14,5 +14,5 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) if i not in ['assets', 'accounts'] ] action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [ - ActionChoices.review, ActionChoices.accept + ActionChoices.review, ActionChoices.accept, ActionChoices.notice ] diff --git a/apps/acls/templates/acls/asset_login_reminder.html b/apps/acls/templates/acls/asset_login_reminder.html new file mode 100644 index 000000000..af836cab5 --- /dev/null +++ b/apps/acls/templates/acls/asset_login_reminder.html @@ -0,0 +1,13 @@ +{% load i18n %} + +

{% trans 'Respectful' %}{{ recipient }},

+
+

{% trans 'Username' %}: [{{ username }}]

+

{% trans 'Assets' %}: [{{ asset }}]

+

{% trans 'Account' %}: [{{ account }}]

+
+ +

{% trans 'The user has just logged in to the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}

+ +

{% trans 'Thank you' %}!

+ diff --git a/apps/acls/templates/acls/user_login_reminder.html b/apps/acls/templates/acls/user_login_reminder.html new file mode 100644 index 000000000..3af4fd52a --- /dev/null +++ b/apps/acls/templates/acls/user_login_reminder.html @@ -0,0 +1,14 @@ +{% load i18n %} + +

{% trans 'Respectful' %}{{ recipient }},

+
+

{% trans 'Username' %}: [{{ username }}]

+

IP: [{{ ip }}]

+

{% trans 'Login city' %}: [{{ city }}]

+

{% trans 'User agent' %}: [{{ user_agent }}]

+
+ +

{% trans 'The user has just successfully logged into the system. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}

+ +

{% trans 'Thank you' %}!

+ diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index f1573629e..4b36b4bbe 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +from collections import defaultdict + import django_filters from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ +from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.status import HTTP_200_OK @@ -12,7 +15,7 @@ from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connect from assets import serializers from assets.exceptions import NotSupportedTemporarilyError from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend -from assets.models import Asset, Gateway, Platform +from assets.models import Asset, Gateway, Platform, Protocol from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual from common.api import SuggestionMixin from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend @@ -115,6 +118,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ("gateways", "assets.view_gateway"), ("spec_info", "assets.view_asset"), ("gathered_info", "assets.view_asset"), + ("sync_platform_protocols", "assets.change_asset"), ) extra_filter_backends = [ LabelFilterBackend, IpInFilterBackend, @@ -152,6 +156,39 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): gateways = asset.domain.gateways return self.get_paginated_response_from_queryset(gateways) + @action(methods=['post'], detail=False, url_path='sync-platform-protocols') + def sync_platform_protocols(self, request, *args, **kwargs): + platform_id = request.data.get('platform_id') + platform = get_object_or_404(Platform, pk=platform_id) + assets = platform.assets.all() + + platform_protocols = { + p['name']: p['port'] + for p in platform.protocols.values('name', 'port') + } + asset_protocols_map = defaultdict(set) + protocols = assets.prefetch_related('protocols').values_list( + 'id', 'protocols__name' + ) + for asset_id, protocol in protocols: + asset_id = str(asset_id) + asset_protocols_map[asset_id].add(protocol) + objs = [] + for asset_id, protocols in asset_protocols_map.items(): + protocol_names = set(platform_protocols) - protocols + if not protocol_names: + continue + for name in protocol_names: + objs.append( + Protocol( + name=name, + port=platform_protocols[name], + asset_id=asset_id, + ) + ) + Protocol.objects.bulk_create(objs) + return Response(status=status.HTTP_200_OK) + def create(self, request, *args, **kwargs): if request.path.find('/api/v1/assets/assets/') > -1: error = _('Cannot create asset directly, you should create a host or other') diff --git a/apps/assets/automations/ping/custom/rdp/main.yml b/apps/assets/automations/ping/custom/rdp/main.yml index 75e40c027..a68670998 100644 --- a/apps/assets/automations/ping/custom/rdp/main.yml +++ b/apps/assets/automations/ping/custom/rdp/main.yml @@ -10,6 +10,6 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" + login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" login_secret_type: "{{ jms_account.secret_type }}" login_private_key_path: "{{ jms_account.private_key_path }}" diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py index 66c991caa..a77691c1b 100644 --- a/apps/assets/const/base.py +++ b/apps/assets/const/base.py @@ -1,9 +1,8 @@ +from django.conf import settings from django.db import models from django.db.models import TextChoices from django.utils.translation import gettext_lazy as _ -from jumpserver.utils import has_valid_xpack_license - class Type: def __init__(self, label, value): @@ -113,7 +112,7 @@ class BaseType(TextChoices): @classmethod def get_choices(cls): - if not has_valid_xpack_license(): + if not settings.XPACK_LICENSE_IS_VALID: return [ (tp.value, tp.label) for tp in cls.get_community_types() diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py index 86e86ed40..261373688 100644 --- a/apps/assets/const/database.py +++ b/apps/assets/const/database.py @@ -7,6 +7,7 @@ class DatabaseTypes(BaseType): POSTGRESQL = 'postgresql', 'PostgreSQL' ORACLE = 'oracle', 'Oracle' SQLSERVER = 'sqlserver', 'SQLServer' + DB2 = 'db2', 'DB2' CLICKHOUSE = 'clickhouse', 'ClickHouse' MONGODB = 'mongodb', 'MongoDB' REDIS = 'redis', 'Redis' @@ -45,6 +46,15 @@ class DatabaseTypes(BaseType): 'change_secret_enabled': False, 'push_account_enabled': False, }, + cls.DB2: { + 'ansible_enabled': False, + 'ping_enabled': False, + 'gather_facts_enabled': False, + 'gather_accounts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + }, cls.CLICKHOUSE: { 'ansible_enabled': False, 'ping_enabled': False, @@ -73,6 +83,7 @@ class DatabaseTypes(BaseType): cls.POSTGRESQL: [{'name': 'PostgreSQL'}], cls.ORACLE: [{'name': 'Oracle'}], cls.SQLSERVER: [{'name': 'SQLServer'}], + cls.DB2: [{'name': 'DB2'}], cls.CLICKHOUSE: [{'name': 'ClickHouse'}], cls.MONGODB: [{'name': 'MongoDB'}], cls.REDIS: [ diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index f1c593522..5aea2daec 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -22,6 +22,7 @@ class Protocol(ChoicesMixin, models.TextChoices): oracle = 'oracle', 'Oracle' postgresql = 'postgresql', 'PostgreSQL' sqlserver = 'sqlserver', 'SQLServer' + db2 = 'db2', 'DB2' clickhouse = 'clickhouse', 'ClickHouse' redis = 'redis', 'Redis' mongodb = 'mongodb', 'MongoDB' @@ -170,6 +171,12 @@ class Protocol(ChoicesMixin, models.TextChoices): } } }, + cls.db2: { + 'port': 5000, + 'required': True, + 'secret_types': ['password'], + 'xpack': True, + }, cls.clickhouse: { 'port': 9000, 'required': True, @@ -269,7 +276,7 @@ class Protocol(ChoicesMixin, models.TextChoices): } } } - if settings.XPACK_ENABLED: + if settings.XPACK_LICENSE_IS_VALID: choices = protocols[cls.chatgpt]['setting']['api_mode']['choices'] choices.extend([ ('gpt-4', 'GPT-4'), diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index 4e57be633..8bb3309e4 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -25,7 +25,7 @@ def migrate_asset_accounts(apps, schema_editor): count += len(auth_books) # auth book 和 account 相同的属性 same_attrs = [ - 'id', 'username', 'comment', 'date_created', 'date_updated', + 'username', 'comment', 'date_created', 'date_updated', 'created_by', 'asset_id', 'org_id', ] # 认证的属性,可能是 auth_book 的,可能是 system_user 的 diff --git a/apps/assets/migrations/0124_auto_20231007_1437.py b/apps/assets/migrations/0124_auto_20231007_1437.py new file mode 100644 index 000000000..930327439 --- /dev/null +++ b/apps/assets/migrations/0124_auto_20231007_1437.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.10 on 2023-10-07 06:37 + +from django.db import migrations + + +def add_db2_platform(apps, schema_editor): + platform_cls = apps.get_model('assets', 'Platform') + automation_cls = apps.get_model('assets', 'PlatformAutomation') + platform = platform_cls.objects.create( + name='DB2', internal=True, category='database', type='db2', + domain_enabled=True, su_enabled=False, comment='DB2', + created_by='System', updated_by='System', + ) + platform.protocols.create(name='db2', port=5000, primary=True, setting={}) + automation_cls.objects.create(ansible_enabled=False, platform=platform) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0123_device_automation_ansible_enabled'), + ] + + operations = [ + migrations.RunPython(add_db2_platform) + ] diff --git a/apps/assets/migrations/0125_auto_20231011_1053.py b/apps/assets/migrations/0125_auto_20231011_1053.py new file mode 100644 index 000000000..11161c909 --- /dev/null +++ b/apps/assets/migrations/0125_auto_20231011_1053.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.10 on 2023-10-11 02:53 + +from django.db import migrations + + +def change_windows_ping_method(apps, schema_editor): + platform_automation_cls = apps.get_model('assets', 'PlatformAutomation') + automations = platform_automation_cls.objects.filter(platform__name__in=['Windows', 'Windows2016']) + automations.update(ping_method='ping_by_rdp') + automations.update(verify_account_method='verify_account_by_rdp') + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0124_auto_20231007_1437'), + ] + + operations = [ + migrations.RunPython(change_windows_ping_method) + ] diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 139295f69..d4fc8165d 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -402,12 +402,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin): return Asset.objects.filter(q).distinct() def get_assets_amount(self): - q = Q(node__key__startswith=f'{self.key}:') | Q(node__key=self.key) - return self.assets.through.objects.filter(q).count() - - def get_assets_account_by_children(self): - children = self.get_all_children().values_list() - return self.assets.through.objects.filter(node_id__in=children).count() + return self.get_all_assets().count() @classmethod def get_node_all_assets_by_key_v2(cls, key): diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index b88d22ce7..75d8c4c19 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -175,6 +175,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali protocols = self.initial_data.get('protocols') if protocols is not None: return + if getattr(self, 'instance', None): + return protocols_required, protocols_default = self._get_protocols_required_default() protocol_map = {str(protocol.id): protocol for protocol in protocols_required + protocols_default} @@ -281,14 +283,52 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali return protocols_data_map.values() @staticmethod - def accounts_create(accounts_data, asset): + def update_account_su_from(accounts, include_su_from_accounts): + if not include_su_from_accounts: + return + name_map = {account.name: account for account in accounts} + username_secret_type_map = { + (account.username, account.secret_type): account for account in accounts + } + + for name, username_secret_type in include_su_from_accounts.items(): + account = name_map.get(name) + if not account: + continue + su_from_account = username_secret_type_map.get(username_secret_type) + if su_from_account: + account.su_from = su_from_account + account.save() + + def accounts_create(self, accounts_data, asset): + from accounts.models import AccountTemplate if not accounts_data: return + + if not isinstance(accounts_data[0], dict): + raise serializers.ValidationError({'accounts': _("Invalid data")}) + + su_from_name_username_secret_type_map = {} for data in accounts_data: data['asset'] = asset.id + name = data.get('name') + su_from = data.pop('su_from', None) + template_id = data.get('template', None) + if template_id: + template = AccountTemplate.objects.get(id=template_id) + if template and template.su_from: + su_from_name_username_secret_type_map[template.name] = ( + template.su_from.username, template.su_from.secret_type + ) + elif isinstance(su_from, dict): + su_from = Account.objects.get(id=su_from.get('id')) + su_from_name_username_secret_type_map[name] = ( + su_from.username, su_from.secret_type + ) s = AssetAccountSerializer(data=accounts_data, many=True) s.is_valid(raise_exception=True) - s.save() + accounts = s.save() + self.update_account_su_from(accounts, su_from_name_username_secret_type_map) @atomic def create(self, validated_data): @@ -298,10 +338,37 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali self.perform_nodes_display_create(instance, nodes_display) return instance + @staticmethod + def sync_platform_protocols(instance, old_platform): + platform = instance.platform + + if str(old_platform.id) == str(instance.platform_id): + return + + platform_protocols = { + p['name']: p['port'] + for p in platform.protocols.values('name', 'port') + } + + protocols = set(instance.protocols.values_list('name', flat=True)) + protocol_names = set(platform_protocols) - protocols + objs = [] + for name in protocol_names: + objs.append( + Protocol( + name=name, + port=platform_protocols[name], + asset_id=instance.id, + ) + ) + Protocol.objects.bulk_create(objs) + @atomic def update(self, instance, validated_data): + old_platform = instance.platform nodes_display = validated_data.pop('nodes_display', '') instance = super().update(instance, validated_data) + self.sync_platform_protocols(instance, old_platform) self.perform_nodes_display_create(instance, nodes_display) return instance diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py index 17a122fd6..ff3e7288a 100644 --- a/apps/assets/serializers/asset/database.py +++ b/apps/assets/serializers/asset/database.py @@ -1,8 +1,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rest_framework.serializers import ValidationError -from assets.models import Database +from assets.models import Database, Platform from assets.serializers.gateway import GatewayWithAccountSecretSerializer from .common import AssetSerializer @@ -20,13 +19,44 @@ class DatabaseSerializer(AssetSerializer): ] fields = AssetSerializer.Meta.fields + extra_fields - def validate(self, attrs): - platform = attrs.get('platform') - db_type_required = ('mongodb', 'postgresql') - if platform and getattr(platform, 'type') in db_type_required \ - and not attrs.get('db_name'): - raise ValidationError({'db_name': _('This field is required.')}) - return attrs + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_db_name_required() + + def get_platform(self): + platform = None + platform_id = None + + if getattr(self, 'initial_data', None): + platform_id = self.initial_data.get('platform') + if isinstance(platform_id, dict): + platform_id = platform_id.get('id') or platform_id.get('pk') + if not platform_id and self.instance: + platform = self.instance.platform + elif getattr(self, 'instance', None): + if isinstance(self.instance, list): + return + platform = self.instance.platform + elif self.context.get('request'): + platform_id = self.context['request'].query_params.get('platform') + + if not platform and platform_id: + platform = Platform.objects.filter(id=platform_id).first() + return platform + + def set_db_name_required(self): + db_field = self.fields.get('db_name') + if not db_field: + return + + platform = self.get_platform() + if not platform: + return + + if platform.type in ['mysql', 'mariadb']: + db_field.required = False + db_field.allow_blank = True + db_field.allow_null = True class DatabaseWithGatewaySerializer(DatabaseSerializer): diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index ac161318b..70da291c3 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -30,8 +30,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer): if '/' in data: error = _("Can't contains: " + "/") raise serializers.ValidationError(error) - if self.instance: - instance = self.instance + view = self.context['view'] + instance = self.instance or getattr(view, 'instance', None) + if instance: siblings = instance.get_siblings() else: instance = Node.org_root() diff --git a/apps/audits/api.py b/apps/audits/api.py index 72c1d8a99..35315aa65 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -6,7 +6,6 @@ from importlib import import_module from django.conf import settings from django.db.models import F, Value, CharField, Q from django.http import HttpResponse, FileResponse -from django.utils import timezone from django.utils.encoding import escape_uri_path from rest_framework import generics from rest_framework import status @@ -185,6 +184,8 @@ class ResourceActivityAPIView(generics.ListAPIView): 'r_user', 'r_action', 'r_type' ) org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id) + if resource_id: + org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID) with tmp_to_root_org(): qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id) qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id) @@ -216,11 +217,10 @@ class OperateLogViewSet(OrgReadonlyModelViewSet): return super().get_serializer_class() def get_queryset(self): - org_q = Q(org_id=current_org.id) + qs = OperateLog.objects.all() if self.is_action_detail: - org_q |= Q(org_id=Organization.SYSTEM_ID) - with tmp_to_root_org(): - qs = OperateLog.objects.filter(org_q) + with tmp_to_root_org(): + qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID) es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG if es_config: engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) @@ -257,9 +257,8 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet): serializer_class = UserSessionSerializer filterset_fields = ['id', 'ip', 'city', 'type'] search_fields = ['id', 'ip', 'city'] - rbac_perms = { - 'offline': ['users.offline_usersession'] + 'offline': ['audits.offline_usersession'] } @property @@ -269,9 +268,7 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet): def get_queryset(self): keys = UserSession.get_keys() - queryset = UserSession.objects.filter( - date_expired__gt=timezone.now(), key__in=keys - ) + queryset = UserSession.objects.filter(key__in=keys) if current_org.is_root(): return queryset user_ids = self.org_user_ids @@ -281,7 +278,9 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet): @action(['POST'], detail=False, url_path='offline') def offline(self, request, *args, **kwargs): ids = request.data.get('ids', []) - queryset = self.get_queryset().exclude(key=request.session.session_key).filter(id__in=ids) + queryset = self.get_queryset() + session_key = request.session.session_key + queryset = queryset.exclude(key=session_key).filter(id__in=ids) if not queryset.exists(): return Response(status=status.HTTP_200_OK) diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py index ac589931d..870c25c9c 100644 --- a/apps/audits/backends/db.py +++ b/apps/audits/backends/db.py @@ -58,7 +58,7 @@ class OperateLogStore(object): return diff_list def save(self, **kwargs): - log_id = kwargs.get('id', '') + log_id = kwargs.pop('id', None) before = kwargs.pop('before') or {} after = kwargs.pop('after') or {} diff --git a/apps/audits/const.py b/apps/audits/const.py index 44d3a556f..4418e9e7f 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -30,6 +30,13 @@ class ActionChoices(TextChoices): login = "login", _("Login") change_auth = "change_password", _("Change password") + accept = 'accept', _('Accept') + review = 'review', _('Review') + notice = 'notice', _('Notifications') + reject = 'reject', _('Reject') + approve = 'approve', _('Approve') + close = 'close', _('Close') + class LoginTypeChoices(TextChoices): web = "W", _("Web") diff --git a/apps/audits/migrations/0023_auto_20230906_1322.py b/apps/audits/migrations/0023_auto_20230906_1322.py index 98447b7b3..34998318c 100644 --- a/apps/audits/migrations/0023_auto_20230906_1322.py +++ b/apps/audits/migrations/0023_auto_20230906_1322.py @@ -1,7 +1,7 @@ # Generated by Django 4.1.10 on 2023-09-06 05:31 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): @@ -19,7 +19,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='operatelog', name='action', - field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password')], max_length=16, verbose_name='Action'), + field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close')], max_length=16, verbose_name='Action'), ), migrations.AlterField( model_name='userloginlog', diff --git a/apps/audits/migrations/0025_remove_usersession_date_expired.py b/apps/audits/migrations/0025_remove_usersession_date_expired.py new file mode 100644 index 000000000..0b495f72d --- /dev/null +++ b/apps/audits/migrations/0025_remove_usersession_date_expired.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.10 on 2023-10-18 08:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0024_usersession'), + ] + + operations = [ + migrations.RemoveField( + model_name='usersession', + name='date_expired', + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 34ec301f2..f7a669114 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -1,5 +1,6 @@ import os import uuid +from datetime import timedelta from importlib import import_module from django.conf import settings @@ -10,7 +11,7 @@ from django.utils import timezone from django.utils.translation import gettext, gettext_lazy as _ from common.db.encoder import ModelJSONFieldEncoder -from common.utils import lazyproperty +from common.utils import lazyproperty, i18n_trans from ops.models import JobExecution from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org @@ -155,6 +156,10 @@ class ActivityLog(OrgModelMixin): verbose_name = _("Activity log") ordering = ('-datetime',) + def __str__(self): + detail = i18n_trans(self.detail) + return "{} {}".format(detail, self.resource_id) + def save(self, *args, **kwargs): if current_org.is_root() and not self.org_id: self.org_id = Organization.ROOT_ID @@ -259,7 +264,6 @@ class UserSession(models.Model): type = models.CharField(choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type")) backend = models.CharField(max_length=32, default="", verbose_name=_("Authentication backend")) date_created = models.DateTimeField(null=True, blank=True, verbose_name=_('Date created')) - date_expired = models.DateTimeField(null=True, blank=True, verbose_name=_("Date expired"), db_index=True) user = models.ForeignKey( 'users.User', verbose_name=_('User'), related_name='sessions', on_delete=models.CASCADE ) @@ -271,6 +275,14 @@ class UserSession(models.Model): def backend_display(self): return gettext(self.backend) + @property + def date_expired(self): + session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore + session_store = session_store_cls(session_key=self.key) + cache_key = session_store.cache_key + ttl = caches[settings.SESSION_CACHE_ALIAS].ttl(cache_key) + return timezone.now() + timedelta(seconds=ttl) + @staticmethod def get_keys(): session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore @@ -280,8 +292,8 @@ class UserSession(models.Model): @classmethod def clear_expired_sessions(cls): - cls.objects.filter(date_expired__lt=timezone.now()).delete() - cls.objects.exclude(key__in=cls.get_keys()).delete() + keys = cls.get_keys() + cls.objects.exclude(key__in=keys).delete() class Meta: ordering = ['-date_created'] diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 829986297..472f6ae28 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -169,6 +169,7 @@ class FileSerializer(serializers.Serializer): class UserSessionSerializer(serializers.ModelSerializer): type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type")) user = ObjectRelatedField(required=False, queryset=User.objects, label=_('User')) + date_expired = serializers.DateTimeField(format="%Y/%m/%d %H:%M:%S", label=_('Date expired')) is_current_user_session = serializers.SerializerMethodField() class Meta: diff --git a/apps/audits/signal_handlers/activity_log.py b/apps/audits/signal_handlers/activity_log.py index 4b41d2d1d..4f564a3c4 100644 --- a/apps/audits/signal_handlers/activity_log.py +++ b/apps/audits/signal_handlers/activity_log.py @@ -70,6 +70,8 @@ class ActivityLogHandler: def create_activities(resource_ids, detail, detail_id, action, org_id): if not resource_ids: return + if not org_id: + org_id = Organization.ROOT_ID activities = [ ActivityLog( resource_id=getattr(resource_id, 'pk', resource_id), @@ -92,6 +94,8 @@ def after_task_publish_for_activity_log(headers=None, body=None, **kwargs): logger.error(f'Get celery task info error: {e}', exc_info=True) else: logger.debug(f'Create activity log for celery task: {task_id}') + if not resource_ids: + return create_activities(resource_ids, detail, task_id, action=ActivityChoices.task, org_id=org_id) @@ -110,6 +114,8 @@ def on_session_or_login_log_created(sender, instance=None, created=False, **kwar logger.error('Activity log handler not found: {}'.format(sender)) resource_ids, detail, act_type, org_id = func(instance) + if not resource_ids: + return return create_activities(resource_ids, detail, instance.id, act_type, org_id) diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index fae32a44b..2d8560412 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # -from datetime import timedelta -from importlib import import_module from django.conf import settings from django.contrib.auth import BACKEND_SESSION_KEY @@ -11,6 +9,8 @@ from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _ from rest_framework.request import Request +from acls.models import LoginACL +from acls.notifications import UserLoginReminderMsg from audits.models import UserLoginLog from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need @@ -82,10 +82,10 @@ def generate_data(username, request, login_type=None): def create_user_session(request, user_id, instance: UserLoginLog): + # TODO 目前只记录 web 登录的 session + if instance.type != LoginTypeChoices.web: + return session_key = request.session.session_key or '-' - session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore - session_store = session_store_cls(session_key=session_key) - ttl = session_store.get_expiry_age() online_session_data = { 'user_id': user_id, @@ -96,26 +96,45 @@ def create_user_session(request, user_id, instance: UserLoginLog): 'backend': instance.backend, 'user_agent': instance.user_agent, 'date_created': instance.datetime, - 'date_expired': instance.datetime + timedelta(seconds=ttl), } user_session = UserSession.objects.create(**online_session_data) - request.session['user_session_id'] = user_session.id + request.session['user_session_id'] = str(user_session.id) + + +def send_login_info_to_reviewers(instance: UserLoginLog | str, auth_acl_id): + if isinstance(instance, str): + instance = UserLoginLog.objects.filter(id=instance).first() + + if not instance: + return + + acl = LoginACL.objects.filter(id=auth_acl_id).first() + if not acl or not acl.reviewers.exists(): + return + + reviewers = acl.reviewers.all() + for reviewer in reviewers: + UserLoginReminderMsg(reviewer, instance).publish_async() @receiver(post_auth_success) def on_user_auth_success(sender, user, request, login_type=None, **kwargs): logger.debug('User login success: {}'.format(user.username)) check_different_city_login_if_need(user, request) - data = generate_data( - user.username, request, login_type=login_type - ) - request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") + data = generate_data(user.username, request, login_type=login_type) + request.session['login_time'] = data['datetime'].strftime('%Y-%m-%d %H:%M:%S') data.update({'mfa': int(user.mfa_enabled), 'status': True}) instance = write_login_log(**data) - # TODO 目前只记录 web 登录的 session - if instance.type != LoginTypeChoices.web: - return + create_user_session(request, user.id, instance) + request.session['user_log_id'] = str(instance.id) + request.session['can_send_notifications'] = True + auth_notice_required = request.session.get('auth_notice_required') + if not auth_notice_required: + return + + auth_acl_id = request.session.get('auth_acl_id') + send_login_info_to_reviewers(instance, auth_acl_id) @receiver(post_auth_failed) diff --git a/apps/authentication/api/access_key.py b/apps/authentication/api/access_key.py index 9253f449d..027fdf75f 100644 --- a/apps/authentication/api/access_key.py +++ b/apps/authentication/api/access_key.py @@ -1,20 +1,48 @@ # -*- coding: utf-8 -*- # -from rest_framework.viewsets import ModelViewSet +from django.utils.translation import gettext as _ +from rest_framework import serializers +from rest_framework.response import Response +from common.api import JMSModelViewSet from rbac.permissions import RBACPermission -from ..serializers import AccessKeySerializer +from ..const import ConfirmType +from ..permissions import UserConfirmation +from ..serializers import AccessKeySerializer, AccessKeyCreateSerializer -class AccessKeyViewSet(ModelViewSet): - serializer_class = AccessKeySerializer - search_fields = ['^id', '^secret'] +class AccessKeyViewSet(JMSModelViewSet): + serializer_classes = { + 'default': AccessKeySerializer, + 'create': AccessKeyCreateSerializer + } + search_fields = ['^id'] permission_classes = [RBACPermission] def get_queryset(self): return self.request.user.access_keys.all() + def get_permissions(self): + if self.is_swagger_request(): + return super().get_permissions() + + if self.action == 'create': + self.permission_classes = [ + RBACPermission, UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() + def perform_create(self, serializer): user = self.request.user - user.create_access_key() + if user.access_keys.count() >= 10: + raise serializers.ValidationError(_('Access keys can be created at most 10')) + key = user.create_access_key() + return key + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + key = self.perform_create(serializer) + serializer = self.get_serializer(instance=key) + return Response(serializer.data, status=201) diff --git a/apps/authentication/api/confirm.py b/apps/authentication/api/confirm.py index 3c0e670f0..2bb371e48 100644 --- a/apps/authentication/api/confirm.py +++ b/apps/authentication/api/confirm.py @@ -4,27 +4,37 @@ import time from django.utils.translation import gettext_lazy as _ from rest_framework import status -from rest_framework.generics import RetrieveAPIView, CreateAPIView +from rest_framework.decorators import action +from rest_framework.generics import RetrieveAPIView from rest_framework.response import Response -from common.permissions import IsValidUser, UserConfirmation +from authentication.permissions import UserConfirmation +from common.api import JMSGenericViewSet +from common.permissions import IsValidUser from ..const import ConfirmType from ..serializers import ConfirmSerializer class ConfirmBindORUNBindOAuth(RetrieveAPIView): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) def retrieve(self, request, *args, **kwargs): return Response('ok') -class ConfirmApi(RetrieveAPIView, CreateAPIView): +class UserConfirmationViewSet(JMSGenericViewSet): permission_classes = (IsValidUser,) serializer_class = ConfirmSerializer + @action(methods=['get'], detail=False) + def check(self, request): + confirm_type = request.query_params.get('confirm_type', 'password') + permission = UserConfirmation.require(confirm_type)() + permission.has_permission(request, self) + return Response('ok') + def get_confirm_backend(self, confirm_type): - backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type) + backend_classes = ConfirmType.get_prop_backends(confirm_type) if not backend_classes: return for backend_cls in backend_classes: @@ -33,12 +43,12 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): continue return backend - def retrieve(self, request, *args, **kwargs): - confirm_type = request.query_params.get('confirm_type') + def list(self, request, *args, **kwargs): + confirm_type = request.query_params.get('confirm_type', 'password') backend = self.get_confirm_backend(confirm_type) if backend is None: msg = _('This action require verify your MFA') - return Response(data={'error': msg}, status=status.HTTP_404_NOT_FOUND) + return Response(data={'error': msg}, status=status.HTTP_400_BAD_REQUEST) data = { 'confirm_type': backend.name, @@ -51,7 +61,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): serializer.is_valid(raise_exception=True) validated_data = serializer.validated_data - confirm_type = validated_data.get('confirm_type') + confirm_type = validated_data.get('confirm_type', 'password') mfa_type = validated_data.get('mfa_type') secret_key = validated_data.get('secret_key') diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 955ca4028..e41b47f76 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -15,12 +15,14 @@ from rest_framework.request import Request from rest_framework.response import Response from accounts.const import AliasAccount +from acls.notifications import AssetLoginReminderMsg from common.api import JMSModelViewSet from common.exceptions import JMSException -from common.utils import random_string, get_logger, get_request_ip +from common.utils import random_string, get_logger, get_request_ip_or_data from common.utils.django import get_request_os from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin +from orgs.utils import tmp_to_org from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil from terminal.models import EndpointRule, Endpoint @@ -298,6 +300,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView 'get_rdp_file': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken', } + input_username = '' def get_queryset(self): queryset = ConnectionToken.objects \ @@ -313,21 +316,42 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return super().perform_create(serializer) def _insert_connect_options(self, data, user): - name = 'file_name_conflict_resolution' connect_options = data.pop('connect_options', {}) - preference = Preference.objects.filter( - name=name, user=user, category='koko' - ).first() - value = preference.value if preference else FileNameConflictResolution.REPLACE - connect_options[name] = value + default_name_opts = { + 'file_name_conflict_resolution': FileNameConflictResolution.REPLACE, + 'terminal_theme_name': 'Default', + } + preferences_query = Preference.objects.filter( + user=user, category='koko', name__in=default_name_opts.keys() + ).values_list('name', 'value') + preferences = dict(preferences_query) + for name in default_name_opts.keys(): + value = preferences.get(name, default_name_opts[name]) + connect_options[name] = value data['connect_options'] = connect_options + @staticmethod + def get_input_username(data): + input_username = data.get('input_username', '') + if input_username: + return input_username + + account = data.get('account', '') + if account == '@USER': + input_username = str(data.get('user', '')) + elif account == '@INPUT': + input_username = '@INPUT' + else: + input_username = account + return input_username + def validate_serializer(self, serializer): data = serializer.validated_data user = self.get_user(serializer) self._insert_connect_options(data, user) asset = data.get('asset') account_name = data.get('account') + self.input_username = self.get_input_username(data) _data = self._validate(user, asset, account_name) data.update(_data) return serializer @@ -374,28 +398,62 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView raise JMSException(code='perm_expired', detail=msg) return account + def _record_operate_log(self, acl, asset): + from audits.handler import create_or_update_operate_log + with tmp_to_org(asset.org_id): + after = { + str(_('Assets')): str(asset), + str(_('Account')): self.input_username + } + object_name = acl._meta.object_name + resource_type = acl._meta.verbose_name + create_or_update_operate_log( + acl.action, resource_type, resource=acl, + after=after, object_name=object_name + ) + def _validate_acl(self, user, asset, account): from acls.models import LoginAssetACL acls = LoginAssetACL.filter_queryset(user=user, asset=asset, account=account) - ip = get_request_ip(self.request) + ip = get_request_ip_or_data(self.request) acl = LoginAssetACL.get_match_rule_acls(user, ip, acls) if not acl: return if acl.is_action(acl.ActionChoices.accept): + self._record_operate_log(acl, asset) return if acl.is_action(acl.ActionChoices.reject): + self._record_operate_log(acl, asset) msg = _('ACL action is reject: {}({})'.format(acl.name, acl.id)) raise JMSException(code='acl_reject', detail=msg) if acl.is_action(acl.ActionChoices.review): if not self.request.query_params.get('create_ticket'): msg = _('ACL action is review') raise JMSException(code='acl_review', detail=msg) - + self._record_operate_log(acl, asset) ticket = LoginAssetACL.create_login_asset_review_ticket( - user=user, asset=asset, account_username=account.username, + user=user, asset=asset, account_username=self.input_username, assignees=acl.reviewers.all(), org_id=asset.org_id ) return ticket + if acl.is_action(acl.ActionChoices.notice): + reviewers = acl.reviewers.all() + if not reviewers: + return + + self._record_operate_log(acl, asset) + for reviewer in reviewers: + AssetLoginReminderMsg( + reviewer, asset, user, self.input_username + ).publish_async() + + def create(self, request, *args, **kwargs): + try: + response = super().create(request, *args, **kwargs) + except JMSException as e: + data = {'code': e.detail.code, 'detail': e.detail} + return Response(data, status=e.status_code) + return response class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index c8d9f7ce0..ad3bd26b1 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -4,8 +4,9 @@ from rest_framework.views import APIView from authentication import errors from authentication.const import ConfirmType +from authentication.permissions import UserConfirmation from common.api import RoleUserMixin, RoleAdminMixin -from common.permissions import UserConfirmation, IsValidUser +from common.permissions import IsValidUser from common.utils import get_logger from users.models import User @@ -27,7 +28,7 @@ class DingTalkQRUnBindBase(APIView): class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index 148b99e51..cf95bb6ea 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -4,12 +4,13 @@ from rest_framework.views import APIView from authentication import errors from authentication.const import ConfirmType +from authentication.permissions import UserConfirmation from common.api import RoleUserMixin, RoleAdminMixin -from common.permissions import UserConfirmation, IsValidUser +from common.permissions import IsValidUser from common.utils import get_logger from users.models import User -logger = get_logger(__file__) +logger = get_logger(__name__) class FeiShuQRUnBindBase(APIView): @@ -27,7 +28,7 @@ class FeiShuQRUnBindBase(APIView): class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase): diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py index 86801bc6c..cc1e6aff7 100644 --- a/apps/authentication/api/password.py +++ b/apps/authentication/api/password.py @@ -1,3 +1,5 @@ +import time + from django.core.cache import cache from django.http import HttpResponseRedirect from django.shortcuts import reverse @@ -7,7 +9,7 @@ from rest_framework.generics import CreateAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response -from authentication.errors import PasswordInvalid +from authentication.errors import PasswordInvalid, IntervalTooShort from authentication.mixins import AuthMixin from authentication.mixins import authenticate from authentication.serializers import ( @@ -38,18 +40,18 @@ class UserResetPasswordSendCodeApi(CreateAPIView): return None, err_msg return user, None - def create(self, request, *args, **kwargs): - token = request.GET.get('token') - userinfo = cache.get(token) - if not userinfo: - return HttpResponseRedirect(reverse('authentication:forgot-previewing')) + @staticmethod + def safe_send_code(token, code, target, form_type, content): + token_sent_key = '{}_send_at'.format(token) + token_send_at = cache.get(token_sent_key, 0) + if token_send_at: + raise IntervalTooShort(60) + SendAndVerifyCodeUtil(target, code, backend=form_type, **content).gen_and_send_async() + cache.set(token_sent_key, int(time.time()), 60) - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - username = userinfo.get('username') + def prepare_code_data(self, user_info, serializer): + username = user_info.get('username') form_type = serializer.validated_data['form_type'] - code = random_string(6, lower=False, upper=False) - other_args = {} target = serializer.validated_data[form_type] if form_type == 'sms': @@ -59,15 +61,30 @@ class UserResetPasswordSendCodeApi(CreateAPIView): query_key = form_type user, err = self.is_valid_user(username=username, **{query_key: target}) if not user: - return Response({'error': err}, status=400) + raise ValueError(err) + code = random_string(6, lower=False, upper=False) 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'], other_args['message'] = subject, message - SendAndVerifyCodeUtil(target, code, backend=form_type, **other_args).gen_and_send_async() + content = {'subject': subject, 'message': message} + return code, target, form_type, content + + def create(self, request, *args, **kwargs): + token = request.GET.get('token') + user_info = cache.get(token) + if not user_info: + return HttpResponseRedirect(reverse('authentication:forgot-previewing')) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + try: + code, target, form_type, content = self.prepare_code_data(user_info, serializer) + except ValueError as e: + return Response({'error': str(e)}, status=400) + self.safe_send_code(token, code, target, form_type, content) return Response({'data': 'ok'}, status=200) diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index e704d3d4b..6dcfe539c 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -4,8 +4,9 @@ from rest_framework.views import APIView from authentication import errors from authentication.const import ConfirmType +from authentication.permissions import UserConfirmation from common.api import RoleUserMixin, RoleAdminMixin -from common.permissions import UserConfirmation, IsValidUser +from common.permissions import IsValidUser from common.utils import get_logger from users.models import User @@ -27,7 +28,7 @@ class WeComQRUnBindBase(APIView): class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): diff --git a/apps/authentication/backends/drf.py b/apps/authentication/backends/drf.py index 8d7d60e99..4ba879cc2 100644 --- a/apps/authentication/backends/drf.py +++ b/apps/authentication/backends/drf.py @@ -1,119 +1,33 @@ # -*- coding: utf-8 -*- # -import time -import uuid - from django.contrib.auth import get_user_model from django.core.cache import cache +from django.utils import timezone from django.utils.translation import gettext as _ -from rest_framework import HTTP_HEADER_ENCODING from rest_framework import authentication, exceptions -from six import text_type from common.auth import signature -from common.utils import get_object_or_none, make_signature, http_to_unixtime -from .base import JMSBaseAuthBackend +from common.utils import get_object_or_none from ..models import AccessKey, PrivateToken -UserModel = get_user_model() + +def date_more_than(d, seconds): + return d is None or (timezone.now() - d).seconds > seconds -def get_request_date_header(request): - date = request.META.get('HTTP_DATE', b'') - if isinstance(date, text_type): - # Work around django test client oddness - date = date.encode(HTTP_HEADER_ENCODING) - return date +def after_authenticate_update_date(user, token=None): + if date_more_than(user.date_api_key_last_used, 60): + user.date_api_key_last_used = timezone.now() + user.save(update_fields=['date_api_key_last_used']) - -class AccessKeyAuthentication(authentication.BaseAuthentication): - """App使用Access key进行签名认证, 目前签名算法比较简单, - app注册或者手动建立后,会生成 access_key_id 和 access_key_secret, - 然后使用 如下算法生成签名: - Signature = md5(access_key_secret + '\n' + Date) - example: Signature = md5('d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc' + '\n' + - 'Thu, 12 Jan 2017 08:19:41 GMT') - 请求时设置请求header - header['Authorization'] = 'Sign access_key_id:Signature' 如: - header['Authorization'] = - 'Sign d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc:OKOlmdxgYPZ9+SddnUUDbQ==' - - 验证时根据相同算法进行验证, 取到access_key_id对应的access_key_id, 从request - headers取到Date, 然后进行md5, 判断得到的结果是否相同, 如果是认证通过, 否则 认证 - 失败 - """ - keyword = 'Sign' - - def authenticate(self, request): - auth = authentication.get_authorization_header(request).split() - if not auth or auth[0].lower() != self.keyword.lower().encode(): - return None - - if len(auth) == 1: - msg = _('Invalid signature header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid signature header. Signature ' - 'string should not contain spaces.') - raise exceptions.AuthenticationFailed(msg) - - try: - sign = auth[1].decode().split(':') - if len(sign) != 2: - msg = _('Invalid signature header. ' - 'Format like AccessKeyId:Signature') - raise exceptions.AuthenticationFailed(msg) - except UnicodeError: - msg = _('Invalid signature header. ' - 'Signature string should not contain invalid characters.') - raise exceptions.AuthenticationFailed(msg) - - access_key_id = sign[0] - try: - uuid.UUID(access_key_id) - except ValueError: - raise exceptions.AuthenticationFailed('Access key id invalid') - request_signature = sign[1] - - return self.authenticate_credentials( - request, access_key_id, request_signature - ) - - @staticmethod - def authenticate_credentials(request, access_key_id, request_signature): - access_key = get_object_or_none(AccessKey, id=access_key_id) - request_date = get_request_date_header(request) - if access_key is None or not access_key.user: - raise exceptions.AuthenticationFailed(_('Invalid signature.')) - access_key_secret = access_key.secret - - try: - request_unix_time = http_to_unixtime(request_date) - except ValueError: - raise exceptions.AuthenticationFailed( - _('HTTP header: Date not provide ' - 'or not %a, %d %b %Y %H:%M:%S GMT')) - - if int(time.time()) - request_unix_time > 15 * 60: - raise exceptions.AuthenticationFailed( - _('Expired, more than 15 minutes')) - - signature = make_signature(access_key_secret, request_date) - if not signature == request_signature: - raise exceptions.AuthenticationFailed(_('Invalid signature.')) - - if not access_key.user.is_active: - raise exceptions.AuthenticationFailed(_('User disabled.')) - return access_key.user, None - - def authenticate_header(self, request): - return 'Sign access_key_id:Signature' + if token and hasattr(token, 'date_last_used') and date_more_than(token.date_last_used, 60): + token.date_last_used = timezone.now() + token.save(update_fields=['date_last_used']) class AccessTokenAuthentication(authentication.BaseAuthentication): keyword = 'Bearer' - # expiration = settings.TOKEN_EXPIRATION or 3600 model = get_user_model() def authenticate(self, request): @@ -125,19 +39,20 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): msg = _('Invalid token header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid token header. Sign string ' - 'should not contain spaces.') + msg = _('Invalid token header. Sign string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: token = auth[1].decode() except UnicodeError: - msg = _('Invalid token header. Sign string ' - 'should not contain invalid characters.') + msg = _('Invalid token header. Sign string should not contain invalid characters.') raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(token) + user, header = self.authenticate_credentials(token) + after_authenticate_update_date(user) + return user, header - def authenticate_credentials(self, token): + @staticmethod + def authenticate_credentials(token): model = get_user_model() user_id = cache.get(token) user = get_object_or_none(model, id=user_id) @@ -151,15 +66,23 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): return self.keyword -class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication): +class PrivateTokenAuthentication(authentication.TokenAuthentication): model = PrivateToken + def authenticate(self, request): + user_token = super().authenticate(request) + if not user_token: + return + user, token = user_token + after_authenticate_update_date(user, token) + return user, token + class SessionAuthentication(authentication.SessionAuthentication): def authenticate(self, request): """ Returns a `User` if the request session currently has a logged in user. - Otherwise returns `None`. + Otherwise, returns `None`. """ # Get the session-based user from the underlying HttpRequest object @@ -195,6 +118,7 @@ class SignatureAuthentication(signature.SignatureAuthentication): if not key.is_active: return None, None user, secret = key.user, str(key.secret) + after_authenticate_update_date(user, key) return user, secret except (AccessKey.DoesNotExist, exceptions.ValidationError): return None, None diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index c638aeef6..98bd2ef2a 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -166,7 +166,7 @@ class OIDCAuthCallbackView(View): code_verifier = request.session.get('oidc_auth_code_verifier', None) logger.debug(log_prompt.format('Process authenticate')) user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier) - if user and user.is_valid: + if user: logger.debug(log_prompt.format('Login: {}'.format(user))) auth.login(self.request, user) # Stores an expiration timestamp in the user's session. This value will be used if diff --git a/apps/authentication/backends/passkey/api.py b/apps/authentication/backends/passkey/api.py index 8f5414122..ace882a92 100644 --- a/apps/authentication/backends/passkey/api.py +++ b/apps/authentication/backends/passkey/api.py @@ -4,24 +4,37 @@ from django.shortcuts import render from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.viewsets import ModelViewSet from authentication.mixins import AuthMixin +from common.api import JMSModelViewSet from .fido import register_begin, register_complete, auth_begin, auth_complete from .models import Passkey from .serializer import PasskeySerializer +from ...const import ConfirmType +from ...permissions import UserConfirmation from ...views import FlashMessageMixin -class PasskeyViewSet(AuthMixin, FlashMessageMixin, ModelViewSet): +class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet): serializer_class = PasskeySerializer permission_classes = (IsAuthenticated,) + def get_permissions(self): + if self.is_swagger_request(): + return super().get_permissions() + if self.action == 'register': + self.permission_classes = [ + IsAuthenticated, UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() + def get_queryset(self): return Passkey.objects.filter(user=self.request.user) @action(methods=['get', 'post'], detail=False, url_path='register') def register(self, request): + if request.user.source != 'local': + return JsonResponse({'error': _('Only register passkey for local user')}, status=400) if request.method == 'GET': register_data, state = register_begin(request) return JsonResponse(dict(register_data)) diff --git a/apps/authentication/backends/passkey/backends.py b/apps/authentication/backends/passkey/backends.py index dc7e1349b..4be1687d2 100644 --- a/apps/authentication/backends/passkey/backends.py +++ b/apps/authentication/backends/passkey/backends.py @@ -7,3 +7,6 @@ class PasskeyAuthBackend(JMSModelBackend): @staticmethod def is_enabled(): return settings.AUTH_PASSKEY + + def user_can_authenticate(self, user): + return user.source == 'local' diff --git a/apps/authentication/backends/pubkey.py b/apps/authentication/backends/pubkey.py index 1494d6b2e..bb9f91072 100644 --- a/apps/authentication/backends/pubkey.py +++ b/apps/authentication/backends/pubkey.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -from django.contrib.auth import get_user_model from django.conf import settings +from django.contrib.auth import get_user_model +from common.permissions import ServiceAccountSignaturePermission from .base import JMSBaseAuthBackend UserModel = get_user_model() @@ -18,6 +19,10 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend): def authenticate(self, request, username=None, public_key=None, **kwargs): if not public_key: return None + + permission = ServiceAccountSignaturePermission() + if not permission.has_permission(request, None): + return None if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) try: @@ -26,7 +31,7 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend): return None else: if user.check_public_key(public_key) and \ - self.user_can_authenticate(user): + self.user_can_authenticate(user): return user def get_user(self, user_id): diff --git a/apps/authentication/confirm/base.py b/apps/authentication/confirm/base.py index 63258abce..5926da3e1 100644 --- a/apps/authentication/confirm/base.py +++ b/apps/authentication/confirm/base.py @@ -2,7 +2,6 @@ import abc class BaseConfirm(abc.ABC): - def __init__(self, user, request): self.user = user self.request = request @@ -23,7 +22,7 @@ class BaseConfirm(abc.ABC): @property def content(self): - return '' + return [] @abc.abstractmethod def authenticate(self, secret_key, mfa_type) -> tuple: diff --git a/apps/authentication/confirm/password.py b/apps/authentication/confirm/password.py index dc49576a3..e497bc261 100644 --- a/apps/authentication/confirm/password.py +++ b/apps/authentication/confirm/password.py @@ -15,3 +15,14 @@ class ConfirmPassword(BaseConfirm): ok = authenticate(self.request, username=self.user.username, password=secret_key) msg = '' if ok else _('Authentication failed password incorrect') return ok, msg + + @property + def content(self): + return [ + { + 'name': 'password', + 'display_name': _('Password'), + 'disabled': False, + 'placeholder': _('Password'), + } + ] diff --git a/apps/authentication/const.py b/apps/authentication/const.py index d7e0690db..1e06a4d35 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -11,7 +11,7 @@ CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS} class ConfirmType(TextChoices): - ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name + RELOGIN = ConfirmReLogin.name, ConfirmReLogin.display_name PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name MFA = ConfirmMFA.name, ConfirmMFA.display_name @@ -23,10 +23,11 @@ class ConfirmType(TextChoices): return types @classmethod - def get_can_confirm_backend_classes(cls, confirm_type): + def get_prop_backends(cls, confirm_type): types = cls.get_can_confirm_types(confirm_type) backend_classes = [ - CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP + CONFIRM_BACKEND_MAP[tp] + for tp in types if tp in CONFIRM_BACKEND_MAP ] return backend_classes diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py index 40b97df92..8a0844145 100644 --- a/apps/authentication/errors/mfa.py +++ b/apps/authentication/errors/mfa.py @@ -36,3 +36,11 @@ class FeiShuNotBound(JMSException): class PasswordInvalid(JMSException): default_code = 'passwd_invalid' default_detail = _('Your password is invalid') + + +class IntervalTooShort(JMSException): + default_code = 'interval_too_short' + default_detail = _('Please wait for %s seconds before retry') + + def __init__(self, interval, *args, **kwargs): + super().__init__(detail=self.default_detail % interval, *args, **kwargs) diff --git a/apps/authentication/mfa/custom.py b/apps/authentication/mfa/custom.py index 0819dcfaa..70ceaf34c 100644 --- a/apps/authentication/mfa/custom.py +++ b/apps/authentication/mfa/custom.py @@ -10,7 +10,7 @@ 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) diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index b4ee86ed4..10659a53a 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -8,6 +8,7 @@ from django.utils.deprecation import MiddlewareMixin from django.utils.translation import gettext as _ from apps.authentication import mixins +from audits.signal_handlers import send_login_info_to_reviewers from authentication.signals import post_auth_failed from common.utils import gen_key_pair from common.utils import get_request_ip @@ -92,12 +93,12 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin): 'title': _('Authentication failed'), 'message': _('Authentication failed (before login check failed): {}').format(e), 'interval': 10, - 'redirect_url': reverse('authentication:login'), + 'redirect_url': reverse('authentication:login') + '?admin=1', 'auto_redirect': True, } response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context) else: - if not self.request.session['auth_confirm_required']: + if not self.request.session.get('auth_confirm_required'): return response guard_url = reverse('authentication:login-guard') args = request.META.get('QUERY_STRING', '') @@ -105,6 +106,12 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin): guard_url = "%s?%s" % (guard_url, args) response = redirect(guard_url) finally: + if request.session.get('can_send_notifications') and \ + self.request.session.get('auth_notice_required'): + request.session['can_send_notifications'] = False + user_log_id = self.request.session.get('user_log_id') + auth_acl_id = self.request.session.get('auth_acl_id') + send_login_info_to_reviewers(user_log_id, auth_acl_id) return response diff --git a/apps/authentication/migrations/0023_auto_20231010_1101.py b/apps/authentication/migrations/0023_auto_20231010_1101.py new file mode 100644 index 000000000..81920dfd7 --- /dev/null +++ b/apps/authentication/migrations/0023_auto_20231010_1101.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.10 on 2023-10-10 02:47 + +import uuid +import authentication.models.access_key +from django.db import migrations, models + + +def migrate_access_key_secret(apps, schema_editor): + access_key_model = apps.get_model('authentication', 'AccessKey') + db_alias = schema_editor.connection.alias + + batch_size = 100 + count = 0 + + while True: + access_keys = access_key_model.objects.using(db_alias).all()[count:count + batch_size] + if not access_keys: + break + + count += len(access_keys) + access_keys_updated = [] + for access_key in access_keys: + s = access_key.secret + if len(s) != 32 or not s.islower(): + continue + try: + access_key.secret = '%s-%s-%s-%s-%s' % (s[:8], s[8:12], s[12:16], s[16:20], s[20:]) + access_keys_updated.append(access_key) + except (ValueError, IndexError): + pass + access_key_model.objects.bulk_update(access_keys_updated, fields=['secret']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0022_passkey'), + ] + + operations = [ + migrations.AddField( + model_name='accesskey', + name='date_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'), + ), + migrations.AddField( + model_name='privatetoken', + name='date_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'), + ), + migrations.AlterField( + model_name='accesskey', + name='secret', + field=models.CharField(default=authentication.models.access_key.default_secret, max_length=36, verbose_name='AccessKeySecret'), + ), + migrations.RunPython(migrate_access_key_secret), + ] diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index b301af6c3..31cb1dc19 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -19,7 +19,7 @@ from django.utils.translation import gettext as _ from rest_framework.request import Request from acls.models import LoginACL -from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil +from common.utils import get_request_ip_or_data, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil from . import errors @@ -76,6 +76,12 @@ def authenticate(request=None, **credentials): if user is None: continue + if not user.is_valid: + temp_user = user + temp_user.backend = backend_path + request.error_message = _('User is invalid') + return temp_user + # 检查用户是否允许认证 if not backend.user_allow_authenticate(user): temp_user = user @@ -101,13 +107,12 @@ auth.authenticate = authenticate class CommonMixin: request: Request + _ip = '' def get_request_ip(self): - ip = '' - if hasattr(self.request, 'data'): - ip = self.request.data.get('remote_addr', '') - ip = ip or get_request_ip(self.request) - return ip + if not self._ip: + self._ip = get_request_ip_or_data(self.request) + return self._ip def raise_credential_error(self, error): raise self.partial_credential_error(error=error) @@ -355,6 +360,11 @@ class AuthACLMixin: self.request.session['auth_acl_id'] = str(acl.id) return + if acl.is_action(acl.ActionChoices.notice): + self.request.session['auth_notice_required'] = '1' + self.request.session['auth_acl_id'] = str(acl.id) + return + def _check_third_party_login_acl(self): request = self.request error_message = getattr(request, 'error_message', None) @@ -513,7 +523,8 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost def clear_auth_mark(self): keys = [ 'auth_password', 'user_id', 'auth_confirm_required', - 'auth_ticket_id', 'auth_acl_id' + 'auth_notice_required', 'auth_ticket_id', 'auth_acl_id', + 'user_session_id', 'user_log_id', 'can_send_notifications' ] for k in keys: self.request.session.pop(k, '') diff --git a/apps/authentication/models/access_key.py b/apps/authentication/models/access_key.py index 77fa67c74..5d9571569 100644 --- a/apps/authentication/models/access_key.py +++ b/apps/authentication/models/access_key.py @@ -5,16 +5,20 @@ from django.db import models from django.utils.translation import gettext_lazy as _ import common.db.models +from common.utils.random import random_string + + +def default_secret(): + return random_string(36) class AccessKey(models.Model): - id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, - default=uuid.uuid4, editable=False) - secret = models.UUIDField(verbose_name='AccessKeySecret', - default=uuid.uuid4, editable=False) + id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False) + secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys') is_active = models.BooleanField(default=True, verbose_name=_('Active')) + date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used')) date_created = models.DateTimeField(auto_now_add=True) def get_id(self): diff --git a/apps/authentication/models/private_token.py b/apps/authentication/models/private_token.py index bb5f1da87..56669f018 100644 --- a/apps/authentication/models/private_token.py +++ b/apps/authentication/models/private_token.py @@ -1,9 +1,11 @@ +from django.db import models from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token class PrivateToken(Token): """Inherit from auth token, otherwise migration is boring""" + date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used')) class Meta: verbose_name = _('Private Token') diff --git a/apps/authentication/permissions.py b/apps/authentication/permissions.py new file mode 100644 index 000000000..e49f72796 --- /dev/null +++ b/apps/authentication/permissions.py @@ -0,0 +1,58 @@ +import time + +from django.conf import settings +from rest_framework import permissions + +from authentication.const import ConfirmType +from authentication.models import ConnectionToken +from common.exceptions import UserConfirmRequired +from common.permissions import IsValidUser +from common.utils import get_object_or_none +from orgs.utils import tmp_to_root_org + + +class UserConfirmation(permissions.BasePermission): + ttl = 60 * 5 + min_level = 1 + confirm_type = 'relogin' + + def has_permission(self, request, view): + if not settings.SECURITY_VIEW_AUTH_NEED_MFA: + return True + + confirm_level = request.session.get('CONFIRM_LEVEL') + confirm_time = request.session.get('CONFIRM_TIME') + ttl = self.get_ttl() + if not confirm_level or not confirm_time or \ + confirm_level < self.min_level or \ + confirm_time < time.time() - ttl: + raise UserConfirmRequired(code=self.confirm_type) + return True + + def get_ttl(self): + if self.confirm_type == ConfirmType.MFA: + ttl = settings.SECURITY_MFA_VERIFY_TTL + else: + ttl = self.ttl + return ttl + + @classmethod + def require(cls, confirm_type=ConfirmType.RELOGIN, ttl=60 * 5): + min_level = ConfirmType.values.index(confirm_type) + 1 + name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl) + return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type}) + + +class IsValidUserOrConnectionToken(IsValidUser): + def has_permission(self, request, view): + return super().has_permission(request, view) \ + or self.is_valid_connection_token(request) + + @staticmethod + def is_valid_connection_token(request): + token_id = request.query_params.get('token') + if not token_id: + return False + with tmp_to_root_org(): + token = get_object_or_none(ConnectionToken, id=token_id) + return token and token.is_valid diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index 5d747c656..dc7c997a6 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -7,4 +7,4 @@ from ..const import ConfirmType, MFAType class ConfirmSerializer(serializers.Serializer): confirm_type = serializers.ChoiceField(required=True, allow_blank=True, choices=ConfirmType.choices) mfa_type = serializers.ChoiceField(required=False, allow_blank=True, choices=MFAType.choices) - secret_key = EncryptedField(allow_blank=True) + secret_key = EncryptedField(allow_blank=True, required=False) diff --git a/apps/authentication/serializers/token.py b/apps/authentication/serializers/token.py index d1e87c0c0..e4dab13a5 100644 --- a/apps/authentication/serializers/token.py +++ b/apps/authentication/serializers/token.py @@ -10,16 +10,22 @@ from users.serializers import UserProfileSerializer from ..models import AccessKey, TempToken __all__ = [ - 'AccessKeySerializer', 'BearerTokenSerializer', + 'AccessKeySerializer', 'BearerTokenSerializer', 'SSOTokenSerializer', 'TempTokenSerializer', + 'AccessKeyCreateSerializer' ] class AccessKeySerializer(serializers.ModelSerializer): class Meta: model = AccessKey - fields = ['id', 'secret', 'is_active', 'date_created'] - read_only_fields = ['id', 'secret', 'date_created'] + fields = ['id', 'is_active', 'date_created', 'date_last_used'] + read_only_fields = ['id', 'date_created', 'date_last_used'] + + +class AccessKeyCreateSerializer(AccessKeySerializer): + class Meta(AccessKeySerializer.Meta): + fields = AccessKeySerializer.Meta.fields + ['secret'] class BearerTokenSerializer(serializers.Serializer): @@ -37,7 +43,8 @@ class BearerTokenSerializer(serializers.Serializer): def get_keyword(obj): return 'Bearer' - def update_last_login(self, user): + @staticmethod + def update_last_login(user): user.last_login = timezone.now() user.save(update_fields=['last_login']) @@ -96,7 +103,7 @@ class TempTokenSerializer(serializers.ModelSerializer): username = request.user.username kwargs = { 'username': username, 'secret': secret, - 'date_expired': timezone.now() + timezone.timedelta(seconds=5*60), + 'date_expired': timezone.now() + timezone.timedelta(seconds=5 * 60), } token = TempToken(**kwargs) token.save() diff --git a/apps/authentication/tests/__init__.py b/apps/authentication/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/authentication/tests/access_key.py b/apps/authentication/tests/access_key.py new file mode 100644 index 000000000..d0ec2842c --- /dev/null +++ b/apps/authentication/tests/access_key.py @@ -0,0 +1,34 @@ +# Python 示例 +# pip install requests drf-httpsig +import datetime +import json + +import requests +from httpsig.requests_auth import HTTPSignatureAuth + + +def get_auth(KeyID, SecretID): + signature_headers = ['(request-target)', 'accept', 'date'] + auth = HTTPSignatureAuth(key_id=KeyID, secret=SecretID, algorithm='hmac-sha256', headers=signature_headers) + return auth + + +def get_user_info(jms_url, auth): + url = jms_url + '/api/v1/users/users/?limit=1' + gmt_form = '%a, %d %b %Y %H:%M:%S GMT' + headers = { + 'Accept': 'application/json', + 'X-JMS-ORG': '00000000-0000-0000-0000-000000000002', + 'Date': datetime.datetime.utcnow().strftime(gmt_form) + } + + response = requests.get(url, auth=auth, headers=headers) + print(json.loads(response.text)) + + +if __name__ == '__main__': + jms_url = 'http://localhost:8080' + KeyID = '0753098d-810c-45fb-b42c-b27077147933' + SecretID = 'a58d2530-d7ee-4390-a204-3492e44dde84' + auth = get_auth(KeyID, SecretID) + get_user_info(jms_url, auth) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 6d40f68e8..e7e561ffd 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -13,6 +13,7 @@ router.register('sso', api.SSOViewSet, 'sso') router.register('temp-tokens', api.TempTokenViewSet, 'temp-token') router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token') router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token') +router.register('confirm', api.UserConfirmationViewSet, 'confirm') urlpatterns = [ path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), @@ -29,7 +30,6 @@ urlpatterns = [ name='feishu-event-subscription-callback'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), - path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'), diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index b53afa423..9cbc95bf2 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -20,19 +20,22 @@ def check_different_city_login_if_need(user, request): return ip = get_request_ip(request) or '0.0.0.0' - if not (ip and validate_ip(ip)): - city = DEFAULT_CITY - else: - city = get_ip_city(ip) or DEFAULT_CITY - city_white = [_('LAN'), 'LAN'] is_private = ipaddress.ip_address(ip).is_private - if not is_private: - last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \ - .filter(username=user.username, status=True).first() + if is_private: + return + last_user_login = UserLoginLog.objects.exclude( + city__in=city_white + ).filter(username=user.username, status=True).first() + if not last_user_login: + return - if last_user_login and last_user_login.city != city: - DifferentCityLoginMessage(user, ip, city).publish_async() + city = get_ip_city(ip) + last_city = get_ip_city(last_user_login.ip) + if city == last_city: + return + + DifferentCityLoginMessage(user, ip, city).publish_async() def build_absolute_uri(request, path=None): diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 819f1d055..536ef4155 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -1,6 +1,7 @@ from urllib.parse import urlencode from django.conf import settings +from django.contrib.auth import logout as auth_logout from django.db.utils import IntegrityError from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect @@ -13,7 +14,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.permissions import UserConfirmation +from authentication.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import get_logger from common.utils.common import get_request_ip @@ -99,7 +100,7 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View): class DingTalkQRBindView(DingTalkQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): user = request.user @@ -158,6 +159,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View): ip = get_request_ip(request) OAuthBindMessage(user, ip, _('DingTalk'), user_id).publish_async() msg = _('Binding DingTalk successfully') + auth_logout(request) response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 0f62ef75d..4ceacc21b 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -1,6 +1,7 @@ from urllib.parse import urlencode from django.conf import settings +from django.contrib.auth import logout as auth_logout from django.db.utils import IntegrityError from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect @@ -11,7 +12,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from authentication.const import ConfirmType from authentication.notifications import OAuthBindMessage -from common.permissions import UserConfirmation +from authentication.permissions import UserConfirmation from common.sdk.im.feishu import URL, FeiShu from common.utils import get_logger from common.utils.common import get_request_ip @@ -69,7 +70,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe class FeiShuQRBindView(FeiShuQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') @@ -121,6 +122,7 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View): ip = get_request_ip(request) OAuthBindMessage(user, ip, _('FeiShu'), user_id).publish_async() msg = _('Binding FeiShu successfully') + auth_logout(request) response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index a222aa4d8..6514e14eb 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -310,12 +310,6 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView): age = self.request.session.get_expiry_age() self.request.session.set_expiry(age) - def get(self, request, *args, **kwargs): - response = super().get(request, *args, **kwargs) - if request.user.is_authenticated: - response.set_cookie('jms_username', request.user.username) - return response - def get_redirect_url(self, *args, **kwargs): try: user = self.get_user_from_session() diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 238d0b3e6..6817c84a0 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -1,6 +1,7 @@ from urllib.parse import urlencode from django.conf import settings +from django.contrib.auth import logout as auth_logout from django.db.utils import IntegrityError from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect @@ -13,7 +14,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.permissions import UserConfirmation +from authentication.permissions import UserConfirmation from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom from common.utils import get_logger @@ -100,7 +101,7 @@ class WeComOAuthMixin(WeComBaseMixin, View): class WeComQRBindView(WeComQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): user = request.user @@ -158,6 +159,7 @@ class WeComQRBindCallbackView(WeComQRMixin, View): ip = get_request_ip(request) OAuthBindMessage(user, ip, _('WeCom'), wecom_userid).publish_async() msg = _('Binding WeCom successfully') + auth_logout(request) response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/common/api/mixin.py b/apps/common/api/mixin.py index 4795673d7..d93459104 100644 --- a/apps/common/api/mixin.py +++ b/apps/common/api/mixin.py @@ -13,7 +13,7 @@ from common.drf.filters import ( IDSpmFilterBackend, CustomFilterBackend, IDInFilterBackend, IDNotFilterBackend, NotOrRelFilterBackend ) -from common.utils import get_logger +from common.utils import get_logger, lazyproperty from .action import RenderToJsonMixin from .serializer import SerializerMixin @@ -150,9 +150,9 @@ class OrderingFielderFieldsMixin: ordering_fields = None extra_ordering_fields = [] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ordering_fields = self._get_ordering_fields() + @lazyproperty + def ordering_fields(self): + return self._get_ordering_fields() def _get_ordering_fields(self): if isinstance(self.__class__.ordering_fields, (list, tuple)): @@ -179,7 +179,10 @@ class OrderingFielderFieldsMixin: model = self.queryset.model else: queryset = self.get_queryset() - model = queryset.model + if isinstance(queryset, list): + model = None + else: + model = queryset.model if not model: return [] @@ -201,4 +204,6 @@ class CommonApiMixin( SerializerMixin, ExtraFilterFieldsMixin, OrderingFielderFieldsMixin, QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin ): - pass + def is_swagger_request(self): + return getattr(self, 'swagger_fake_view', False) or \ + getattr(self, 'raw_action', '') == 'metadata' diff --git a/apps/common/const/crontab.py b/apps/common/const/crontab.py index bd9809176..e4de195eb 100644 --- a/apps/common/const/crontab.py +++ b/apps/common/const/crontab.py @@ -1,5 +1,5 @@ -CRONTAB_AT_AM_TWO = '0 14 * * *' +CRONTAB_AT_AM_TWO = '0 2 * * *' CRONTAB_AT_AM_TEN = '0 10 * * *' -CRONTAB_AT_PM_TWO = '0 2 * * *' +CRONTAB_AT_PM_TWO = '0 14 * * *' diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 5fa8861b0..0c22d6723 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # from django.utils.translation import gettext_lazy as _ -from rest_framework.exceptions import APIException from rest_framework import status +from rest_framework.exceptions import APIException class JMSException(APIException): @@ -42,8 +42,11 @@ class ReferencedByOthers(JMSException): class UserConfirmRequired(JMSException): + status_code = status.HTTP_412_PRECONDITION_FAILED + def __init__(self, code=None): detail = { + 'type': 'user_confirm_required', 'code': code, 'detail': _('This action require confirm current user') } diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 5c58de68e..ee2e24a8b 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -5,12 +5,6 @@ import time from django.conf import settings from rest_framework import permissions -from authentication.const import ConfirmType -from authentication.models import ConnectionToken -from common.exceptions import UserConfirmRequired -from common.utils import get_object_or_none -from orgs.utils import tmp_to_root_org - class IsValidUser(permissions.IsAuthenticated): """Allows access to valid user, is active and not expired""" @@ -20,21 +14,6 @@ class IsValidUser(permissions.IsAuthenticated): and request.user.is_valid -class IsValidUserOrConnectionToken(IsValidUser): - def has_permission(self, request, view): - return super().has_permission(request, view) \ - or self.is_valid_connection_token(request) - - @staticmethod - def is_valid_connection_token(request): - token_id = request.query_params.get('token') - if not token_id: - return False - with tmp_to_root_org(): - token = get_object_or_none(ConnectionToken, id=token_id) - return token and token.is_valid - - class OnlySuperUser(IsValidUser): def has_permission(self, request, view): return super().has_permission(request, view) \ @@ -56,33 +35,36 @@ class WithBootstrapToken(permissions.BasePermission): return settings.BOOTSTRAP_TOKEN == request_bootstrap_token -class UserConfirmation(permissions.BasePermission): - ttl = 60 * 5 - min_level = 1 - confirm_type = ConfirmType.ReLogin - +class ServiceAccountSignaturePermission(permissions.BasePermission): def has_permission(self, request, view): - if not settings.SECURITY_VIEW_AUTH_NEED_MFA: + from authentication.models import AccessKey + from common.utils.crypto import get_aes_crypto + signature = request.META.get('HTTP_X_JMS_SVC', '') + if not signature or not signature.startswith('Sign'): + return False + data = signature[4:].strip() + if not data or ':' not in data: + return False + ak_id, time_sign = data.split(':', 1) + if not ak_id or not time_sign: + return False + ak = AccessKey.objects.filter(id=ak_id).first() + if not ak or not ak.is_active: + return False + if not ak.user or not ak.user.is_active or not ak.user.is_service_account: + return False + aes = get_aes_crypto(str(ak.secret).replace('-', ''), mode='ECB') + try: + timestamp = aes.decrypt(time_sign) + if not timestamp or not timestamp.isdigit(): + return False + timestamp = int(timestamp) + interval = abs(int(time.time()) - timestamp) + if interval > 30: + return False return True + except Exception: + return False - confirm_level = request.session.get('CONFIRM_LEVEL') - confirm_time = request.session.get('CONFIRM_TIME') - ttl = self.get_ttl() - if not confirm_level or not confirm_time or \ - confirm_level < self.min_level or \ - confirm_time < time.time() - ttl: - raise UserConfirmRequired(code=self.confirm_type) - return True - - def get_ttl(self): - if self.confirm_type == ConfirmType.MFA: - ttl = settings.SECURITY_MFA_VERIFY_TTL - else: - ttl = self.ttl - return ttl - - @classmethod - def require(cls, confirm_type=ConfirmType.ReLogin, ttl=60 * 5): - min_level = ConfirmType.values.index(confirm_type) + 1 - name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl) - return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type}) + def has_object_permission(self, request, view, obj): + return False diff --git a/apps/common/sdk/sms/custom.py b/apps/common/sdk/sms/custom.py index 64f9a0246..2cfaca6ca 100644 --- a/apps/common/sdk/sms/custom.py +++ b/apps/common/sdk/sms/custom.py @@ -30,7 +30,7 @@ class CustomSMS(BaseSMSClient): code=template_param.get('code'), phone_numbers=phone_numbers_str ) - logger.info(f'Custom sms send: phone_numbers={phone_numbers}param={params}') + logger.info(f'Custom sms send: phone_numbers={phone_numbers}, param={params}') if settings.CUSTOM_SMS_REQUEST_METHOD == 'post': action = requests.post kwargs = {'json': params} diff --git a/apps/common/sdk/sms/custom_file.py b/apps/common/sdk/sms/custom_file.py new file mode 100644 index 000000000..ee24a0d31 --- /dev/null +++ b/apps/common/sdk/sms/custom_file.py @@ -0,0 +1,50 @@ +import os + +from collections import OrderedDict + +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from django.utils.module_loading import import_string + +from common.utils import get_logger +from common.exceptions import JMSException +from jumpserver.settings import get_file_md5 + +from .base import BaseSMSClient + + +logger = get_logger(__file__) + + +custom_sms_method = None +SMS_CUSTOM_FILE_MD5 = settings.SMS_CUSTOM_FILE_MD5 +SMS_CUSTOM_FILE_PATH = os.path.join(settings.PROJECT_DIR, 'data', 'sms', 'main.py') +if SMS_CUSTOM_FILE_MD5 == get_file_md5(SMS_CUSTOM_FILE_PATH): + try: + custom_sms_method_path = 'data.sms.main.send_sms' + custom_sms_method = import_string(custom_sms_method_path) + except Exception as e: + logger.warning('Import custom sms method failed: {}, Maybe not enabled'.format(e)) + + +class CustomFileSMS(BaseSMSClient): + @classmethod + def new_from_settings(cls): + return cls() + + @staticmethod + def need_pre_check(): + return False + + def send_sms(self, phone_numbers: list, template_param: OrderedDict, **kwargs): + if not callable(custom_sms_method): + raise JMSException(_('The custom sms file is invalid')) + + try: + logger.info(f'Custom file sms send: phone_numbers={phone_numbers}, param={template_param}') + custom_sms_method(phone_numbers, template_param, **kwargs) + except Exception as err: + raise JMSException(_('SMS sending failed[%s]: %s') % (f"{_('Custom type')}({_('File')})", err)) + + +client = CustomFileSMS diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py index ce016888a..6f5433fa5 100644 --- a/apps/common/sdk/sms/endpoint.py +++ b/apps/common/sdk/sms/endpoint.py @@ -17,7 +17,8 @@ class BACKENDS(TextChoices): TENCENT = 'tencent', _('Tencent cloud') HUAWEI = 'huawei', _('Huawei Cloud') CMPP2 = 'cmpp2', _('CMPP v2.0') - Custom = 'custom', _('Custom type') + CUSTOM = 'custom', _('Custom type') + CUSTOM_FILE = 'custom_file', f"{_('Custom type')}({_('File')})" class SMS: diff --git a/apps/common/serializers/fields.py b/apps/common/serializers/fields.py index 844c424e5..5fe019c65 100644 --- a/apps/common/serializers/fields.py +++ b/apps/common/serializers/fields.py @@ -218,12 +218,13 @@ class PhoneField(serializers.CharField): code = data.get('code') phone = data.get('phone', '') if code and phone: - data = '{}{}'.format(code, phone) + code = code.replace('+', '') + data = '+{}{}'.format(code, phone) else: data = phone try: phone = phonenumbers.parse(data, 'CN') - data = '{}{}'.format(phone.country_code, phone.national_number) + data = '+{}{}'.format(phone.country_code, phone.national_number) except phonenumbers.NumberParseException: data = '+86{}'.format(data) diff --git a/apps/common/tasks.py b/apps/common/tasks.py index a7d9aacdd..468fa2a51 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -36,8 +36,8 @@ def send_mail_async(*args, **kwargs): args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0] from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER args.insert(2, from_email) - args = tuple(args) + args = tuple(args) try: return send_mail(*args, **kwargs) except Exception as e: diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 56f1dbd09..063bd1ebc 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -17,6 +17,8 @@ import psutil from django.conf import settings from django.templatetags.static import static +from common.permissions import ServiceAccountSignaturePermission + UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') ipip_db = None @@ -153,19 +155,26 @@ def is_uuid(seq): def get_request_ip(request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') + x_real_ip = request.META.get('HTTP_X_REAL_IP', '') + if x_real_ip: + return x_real_ip + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') if x_forwarded_for and x_forwarded_for[0]: login_ip = x_forwarded_for[0] - else: - login_ip = request.META.get('REMOTE_ADDR', '') + return login_ip + + login_ip = request.META.get('REMOTE_ADDR', '') return login_ip def get_request_ip_or_data(request): ip = '' - if hasattr(request, 'data'): - ip = request.data.get('remote_addr', '') + + if hasattr(request, 'data') and request.data.get('remote_addr', ''): + permission = ServiceAccountSignaturePermission() + if permission.has_permission(request, None): + ip = request.data.get('remote_addr', '') ip = ip or get_request_ip(request) return ip diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index 267de8d22..505fbd041 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # import random +import secrets import socket import string import struct @@ -17,32 +18,37 @@ def random_ip(): return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff))) +def random_replace_char(s, chars, length): + using_index = set() + seq = list(s) + + while length > 0: + index = secrets.randbelow(len(seq) - 1) + if index in using_index or index == 0: + continue + seq[index] = secrets.choice(chars) + using_index.add(index) + length -= 1 + return ''.join(seq) + + def random_string(length: int, lower=True, upper=True, digit=True, special_char=False, symbols=string_punctuation): - random.seed() - args_names = ['lower', 'upper', 'digit'] - args_values = [lower, upper, digit] - args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits] - args_string_map = dict(zip(args_names, args_string)) - kwargs = dict(zip(args_names, args_values)) - kwargs_keys = list(kwargs.keys()) - kwargs_values = list(kwargs.values()) - args_true_count = len([i for i in kwargs_values if i]) + if not any([lower, upper, digit]): + raise ValueError('At least one of `lower`, `upper`, `digit` must be `True`') + if length < 4: + raise ValueError('The length of the string must be greater than 3') - assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`' - assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}' - - chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v]) - password = list(random.choice(chars) for i in range(length)) + chars_map = ( + (lower, string.ascii_lowercase), + (upper, string.ascii_uppercase), + (digit, string.digits), + ) + chars = ''.join([i[1] for i in chars_map if i[0]]) + texts = list(secrets.choice(chars) for __ in range(length)) + texts = ''.join(texts) + # 控制一下特殊字符的数量, 别随机出来太多 if special_char: - special_num = length // 16 + 1 - special_index = [] - for i in range(special_num): - index = random.randint(1, length - 1) - if index not in special_index: - special_index.append(index) - for i in special_index: - password[i] = random.choice(symbols) - - password = ''.join(password) - return password + symbol_num = length // 16 + 1 + texts = random_replace_char(texts, symbols, symbol_num) + return texts diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py index b21e43b26..f9a721c89 100644 --- a/apps/common/utils/verify_code.py +++ b/apps/common/utils/verify_code.py @@ -67,7 +67,7 @@ class SendAndVerifyCodeUtil(object): return cache.get(self.key) def __generate(self): - code = random_string(4, lower=False, upper=False) + code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False) self.code = code return code diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b24ef5746..6ba54732d 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -247,10 +247,11 @@ class Config(dict): 'AUTH_CUSTOM': False, 'AUTH_CUSTOM_FILE_MD5': '', - # Custom Config 'MFA_CUSTOM': False, 'MFA_CUSTOM_FILE_MD5': '', + 'SMS_CUSTOM_FILE_MD5': '', + # 临时密码 'AUTH_TEMP_TOKEN': False, @@ -409,6 +410,7 @@ class Config(dict): 'SMS_ENABLED': False, 'SMS_BACKEND': '', + 'SMS_CODE_LENGTH': 4, 'SMS_TEST_PHONE': '', 'ALIBABA_ACCESS_KEY_ID': '', @@ -439,7 +441,7 @@ class Config(dict): 'CMPP2_VERIFY_TEMPLATE_CODE': '{code}', 'CUSTOM_SMS_URL': '', - 'CUSTOM_SMS_API_PARAMS': {'phone_numbers': '{phone_numbers}', 'code': '{code}'}, + 'CUSTOM_SMS_API_PARAMS': {'phone_numbers': '{phone_numbers}', 'content': _('The verification code is: {code}')}, 'CUSTOM_SMS_REQUEST_METHOD': 'get', # Email @@ -495,7 +497,7 @@ class Config(dict): 'SECURITY_LUNA_REMEMBER_AUTH': True, 'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_MFA_VERIFY_TTL': 3600, - 'SECURITY_UNCOMMON_USERS_TTL': 30, + 'SECURITY_UNCOMMON_USERS_TTL': 90, 'VERIFY_CODE_TTL': 60, 'SECURITY_SESSION_SHARE': True, 'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True, @@ -543,12 +545,12 @@ class Config(dict): 'MAGNUS_ORACLE_PORTS': '30000-30030', # 记录清理清理 - 'LOGIN_LOG_KEEP_DAYS': 200, - 'TASK_LOG_KEEP_DAYS': 90, - 'OPERATE_LOG_KEEP_DAYS': 200, - 'ACTIVITY_LOG_KEEP_DAYS': 200, - 'FTP_LOG_KEEP_DAYS': 200, - 'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30, + 'LOGIN_LOG_KEEP_DAYS': 180, + 'TASK_LOG_KEEP_DAYS': 180, + 'OPERATE_LOG_KEEP_DAYS': 180, + 'ACTIVITY_LOG_KEEP_DAYS': 180, + 'FTP_LOG_KEEP_DAYS': 180, + 'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 180, 'TICKETS_ENABLED': True, diff --git a/apps/jumpserver/settings/_xpack.py b/apps/jumpserver/settings/_xpack.py index 9f4319a35..6e43c7501 100644 --- a/apps/jumpserver/settings/_xpack.py +++ b/apps/jumpserver/settings/_xpack.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- # +import datetime import os -from .. import const + from .base import INSTALLED_APPS, TEMPLATES +from .. import const + +current_year = datetime.datetime.now().year +corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}' XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack') XPACK_ENABLED = os.path.isdir(XPACK_DIR) XPACK_TEMPLATES_DIR = [] XPACK_CONTEXT_PROCESSOR = [] +XPACK_LICENSE_IS_VALID = False +XPACK_LICENSE_INFO = { + 'corporation': corporation, +} if XPACK_ENABLED: from xpack.utils import get_xpack_templates_dir, get_xpack_context_processor + INSTALLED_APPS.insert(0, 'xpack.apps.XpackConfig') XPACK_TEMPLATES_DIR = get_xpack_templates_dir(const.BASE_DIR) XPACK_CONTEXT_PROCESSOR = get_xpack_context_processor() diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 63a96fe1d..5b6a1cd35 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -259,6 +259,9 @@ if MFA_CUSTOM and MFA_CUSTOM_FILE_MD5 == get_file_md5(MFA_CUSTOM_FILE_PATH): # 自定义多因子认证模块 MFA_BACKENDS.append(MFA_BACKEND_CUSTOM) +SMS_CUSTOM_FILE_MD5 = CONFIG.SMS_CUSTOM_FILE_MD5 +SMS_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'sms', 'main.py') + AUTHENTICATION_BACKENDS_THIRD_PARTY = [ AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2 diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 4cf1c3c77..4344660f7 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -79,6 +79,7 @@ if CONFIG.SITE_URL: ALLOWED_DOMAINS = DOMAINS.split(',') if DOMAINS else ['localhost:8080'] ALLOWED_DOMAINS = [host.strip() for host in ALLOWED_DOMAINS] ALLOWED_DOMAINS = [host.replace('http://', '').replace('https://', '') for host in ALLOWED_DOMAINS if host] +ALLOWED_DOMAINS = [host.replace(':80', '').replace(':443', '') for host in ALLOWED_DOMAINS] ALLOWED_DOMAINS = [host.split('/')[0] for host in ALLOWED_DOMAINS if host] DEBUG_HOSTS = ('127.0.0.1', 'localhost', 'core') diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 18d0bccca..0ec5c54ff 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -124,6 +124,8 @@ TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS ACTIVITY_LOG_KEEP_DAYS = CONFIG.ACTIVITY_LOG_KEEP_DAYS FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS +CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS + ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD @@ -159,8 +161,6 @@ SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED -CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS - TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED TERMINAL_OMNIDB_ENABLED = CONFIG.TERMINAL_OMNIDB_ENABLED TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED @@ -169,6 +169,7 @@ TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED # SMS enabled SMS_ENABLED = CONFIG.SMS_ENABLED SMS_BACKEND = CONFIG.SMS_BACKEND +SMS_CODE_LENGTH = CONFIG.SMS_CODE_LENGTH SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE # Alibaba @@ -186,6 +187,11 @@ TENCENT_VERIFY_SIGN_NAME = CONFIG.TENCENT_VERIFY_SIGN_NAME TENCENT_VERIFY_TEMPLATE_CODE = CONFIG.TENCENT_VERIFY_TEMPLATE_CODE TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES +# CUSTOM_SMS +CUSTOM_SMS_URL = CONFIG.CUSTOM_SMS_URL +CUSTOM_SMS_API_PARAMS = CONFIG.CUSTOM_SMS_API_PARAMS +CUSTOM_SMS_REQUEST_METHOD = CONFIG.CUSTOM_SMS_REQUEST_METHOD + # 公告 ANNOUNCEMENT_ENABLED = CONFIG.ANNOUNCEMENT_ENABLED ANNOUNCEMENT = CONFIG.ANNOUNCEMENT diff --git a/apps/jumpserver/utils.py b/apps/jumpserver/utils.py index 24f2b10d7..350c9b346 100644 --- a/apps/jumpserver/utils.py +++ b/apps/jumpserver/utils.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- # -from datetime import datetime from functools import partial -from django.conf import settings from werkzeug.local import LocalProxy from common.local import thread_local @@ -21,25 +19,4 @@ def get_current_request(): return _find('current_request') -def has_valid_xpack_license(): - if not settings.XPACK_ENABLED: - return False - from xpack.plugins.license.models import License - return License.has_valid_license() - - -def get_xpack_license_info() -> dict: - if has_valid_xpack_license(): - from xpack.plugins.license.models import License - info = License.get_license_detail() - corporation = info.get('corporation', '') - else: - current_year = datetime.now().year - corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}' - info = { - 'corporation': corporation - } - return info - - current_request = LocalProxy(partial(_find, 'current_request')) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index e12251412..65199d7a6 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:2ffdd50c364a510b4f5cfe7922a5f1a604e8bc7b03aa43ece1dff0250ccde6d6 -size 160575 +oid sha256:67b3061a082605d0862007d0deb03dba0477b3eac85d99faf5755ee44e7453eb +size 163187 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index aa30a95b7..7eff444d0 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: 2023-09-19 15:41+0800\n" +"POT-Creation-Date: 2023-10-19 16:21+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,17 +18,18 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: accounts/api/automations/base.py:79 tickets/api/ticket.py:112 +#: accounts/api/automations/base.py:79 tickets/api/ticket.py:132 msgid "The parameter 'action' must be [{}]" msgstr "パラメータ 'action' は [{}] でなければなりません。" #: accounts/const/account.py:6 #: accounts/serializers/automations/change_secret.py:32 -#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:35 -#: authentication/confirm/password.py:9 authentication/forms.py:32 -#: authentication/templates/authentication/login.html:286 +#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:34 +#: authentication/confirm/password.py:9 authentication/confirm/password.py:24 +#: authentication/confirm/password.py:26 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:324 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -40,7 +41,7 @@ msgstr "パスワード" msgid "SSH key" msgstr "SSH キー" -#: accounts/const/account.py:8 authentication/models/access_key.py:33 +#: accounts/const/account.py:8 authentication/models/access_key.py:37 msgid "Access key" msgstr "アクセスキー" @@ -71,7 +72,7 @@ msgstr "動的コード" msgid "Anonymous account" msgstr "匿名ユーザー" -#: accounts/const/account.py:25 users/models/user.py:744 +#: accounts/const/account.py:25 users/models/user.py:738 msgid "Local" msgstr "ローカル" @@ -79,8 +80,8 @@ msgstr "ローカル" msgid "Collected" msgstr "集めました" -#: accounts/const/account.py:27 accounts/serializers/account/account.py:27 -#: settings/serializers/auth/sms.py:75 +#: accounts/const/account.py:27 accounts/serializers/account/account.py:28 +#: settings/serializers/auth/sms.py:79 msgid "Template" msgstr "テンプレート" @@ -88,15 +89,15 @@ msgstr "テンプレート" msgid "Skip" msgstr "スキップ" -#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:234 +#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:236 #: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" #: accounts/const/account.py:33 -#: accounts/serializers/automations/change_secret.py:155 audits/const.py:55 +#: accounts/serializers/automations/change_secret.py:155 audits/const.py:62 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:62 terminal/const.py:77 xpack/plugins/cloud/const.py:43 +#: ops/const.py:74 terminal/const.py:77 xpack/plugins/cloud/const.py:43 msgid "Failed" msgstr "失敗しました" @@ -217,15 +218,15 @@ msgstr "ユーザー %s がパスワードを閲覧/導き出しました" #: accounts/models/account.py:48 #: accounts/models/automations/gather_account.py:16 -#: accounts/serializers/account/account.py:205 -#: accounts/serializers/account/account.py:250 +#: accounts/serializers/account/account.py:210 +#: accounts/serializers/account/account.py:255 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:131 #: acls/serializers/base.py:123 assets/models/asset/common.py:93 #: assets/models/asset/common.py:334 assets/models/cmd_filter.py:36 #: assets/serializers/domain.py:19 assets/serializers/label.py:27 -#: audits/models.py:54 authentication/models/connection_token.py:36 +#: audits/models.py:57 authentication/models/connection_token.py:36 #: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 #: terminal/backends/command/models.py:17 terminal/models/session/session.py:31 #: terminal/notifications.py:155 terminal/serializers/command.py:17 @@ -237,21 +238,21 @@ msgid "Asset" msgstr "資産" #: accounts/models/account.py:52 accounts/models/template.py:15 -#: accounts/serializers/account/account.py:212 -#: accounts/serializers/account/account.py:260 -#: accounts/serializers/account/template.py:25 +#: accounts/serializers/account/account.py:217 +#: accounts/serializers/account/account.py:265 +#: accounts/serializers/account/template.py:24 #: authentication/serializers/connect_token_secret.py:49 msgid "Su from" msgstr "から切り替え" -#: accounts/models/account.py:54 assets/const/protocol.py:168 +#: accounts/models/account.py:54 assets/const/protocol.py:169 #: settings/serializers/auth/cas.py:20 settings/serializers/auth/feishu.py:20 #: terminal/models/applet/applet.py:35 msgid "Version" msgstr "バージョン" -#: accounts/models/account.py:56 accounts/serializers/account/account.py:207 -#: users/models/user.py:846 +#: accounts/models/account.py:56 accounts/serializers/account/account.py:212 +#: users/models/user.py:837 msgid "Source" msgstr "ソース" @@ -262,10 +263,12 @@ msgstr "ソース ID" #: accounts/models/account.py:60 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/serializers/base.py:124 assets/serializers/asset/common.py:125 -#: assets/serializers/gateway.py:28 audits/models.py:55 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 -#: terminal/backends/command/models.py:18 terminal/models/session/session.py:33 +#: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7 +#: assets/serializers/asset/common.py:125 assets/serializers/gateway.py:28 +#: audits/models.py:58 authentication/api/connection_token.py:406 +#: ops/models/base.py:18 perms/models/asset_permission.py:70 +#: perms/serializers/permission.py:39 terminal/backends/command/models.py:18 +#: terminal/models/session/session.py:33 #: terminal/templates/terminal/_msg_command_warning.html:8 #: terminal/templates/terminal/_msg_session_sharing.html:8 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:89 @@ -306,7 +309,7 @@ msgid "Account backup plan" msgstr "アカウントバックアップ計画" #: accounts/models/automations/backup_account.py:91 -#: assets/models/automations/base.py:115 audits/models.py:61 +#: assets/models/automations/base.py:115 audits/models.py:64 #: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:228 #: ops/templates/ops/celery_task_log.html:75 #: perms/models/asset_permission.py:72 terminal/models/applet/host.py:140 @@ -334,7 +337,7 @@ msgstr "アカウントのバックアップスナップショット" msgid "Trigger mode" msgstr "トリガーモード" -#: accounts/models/automations/backup_account.py:105 audits/models.py:195 +#: accounts/models/automations/backup_account.py:105 audits/models.py:202 #: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:205 msgid "Reason" msgstr "理由" @@ -342,7 +345,7 @@ msgstr "理由" #: accounts/models/automations/backup_account.py:107 #: accounts/serializers/automations/change_secret.py:110 #: accounts/serializers/automations/change_secret.py:133 -#: ops/serializers/job.py:56 terminal/serializers/session.py:46 +#: ops/serializers/job.py:56 terminal/serializers/session.py:49 msgid "Is success" msgstr "成功は" @@ -420,10 +423,10 @@ msgid "Date finished" msgstr "終了日" #: accounts/models/automations/change_secret.py:44 -#: accounts/serializers/account/account.py:252 assets/const/automation.py:8 +#: accounts/serializers/account/account.py:257 assets/const/automation.py:8 #: authentication/templates/authentication/passkey.html:173 -#: authentication/views/base.py:26 authentication/views/base.py:27 -#: authentication/views/base.py:28 common/const/choices.py:20 +#: authentication/views/base.py:27 authentication/views/base.py:28 +#: authentication/views/base.py:29 common/const/choices.py:20 msgid "Error" msgstr "間違い" @@ -442,13 +445,14 @@ msgstr "最終ログイン日" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:65 #: accounts/serializers/account/virtual.py:21 acls/serializers/base.py:19 -#: acls/serializers/base.py:50 assets/models/_user.py:23 audits/models.py:180 -#: authentication/forms.py:25 authentication/forms.py:27 +#: acls/serializers/base.py:50 acls/templates/acls/asset_login_reminder.html:5 +#: acls/templates/acls/user_login_reminder.html:5 assets/models/_user.py:23 +#: audits/models.py:187 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 #: users/forms/profile.py:32 users/forms/profile.py:115 -#: users/models/user.py:796 users/templates/users/_msg_user_created.html:12 +#: users/models/user.py:790 users/templates/users/_msg_user_created.html:12 #: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "ユーザー名" @@ -476,7 +480,7 @@ msgstr "トリガー方式" #: accounts/models/automations/push_account.py:16 acls/models/base.py:41 #: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 -#: audits/models.py:88 audits/serializers.py:84 +#: audits/models.py:91 audits/serializers.py:84 #: authentication/serializers/connect_token_secret.py:116 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" @@ -491,7 +495,7 @@ msgid "Verify asset account" msgstr "アカウントの確認" #: accounts/models/base.py:37 accounts/models/base.py:67 -#: accounts/serializers/account/account.py:432 +#: accounts/serializers/account/account.py:437 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:45 #: authentication/serializers/connect_token_secret.py:41 @@ -499,7 +503,7 @@ msgstr "アカウントの確認" msgid "Secret type" msgstr "鍵の種類" -#: accounts/models/base.py:39 accounts/models/mixins/vault.py:48 +#: accounts/models/base.py:39 accounts/models/mixins/vault.py:49 #: accounts/serializers/account/base.py:19 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 @@ -512,7 +516,7 @@ msgstr "ひみつ" msgid "Secret strategy" msgstr "鍵ポリシー" -#: accounts/models/base.py:44 accounts/serializers/account/template.py:22 +#: accounts/models/base.py:44 accounts/serializers/account/template.py:21 #: accounts/serializers/automations/change_secret.py:44 msgid "Password rules" msgstr "パスワードルール" @@ -535,10 +539,11 @@ msgstr "パスワードルール" #: terminal/models/applet/applet.py:33 terminal/models/component/endpoint.py:12 #: terminal/models/component/endpoint.py:94 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:13 -#: terminal/models/component/terminal.py:84 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/preference.py:11 -#: users/models/user.py:798 xpack/plugins/cloud/models.py:32 -#: xpack/plugins/cloud/models.py:273 xpack/plugins/cloud/serializers/task.py:68 +#: terminal/models/component/terminal.py:84 tickets/api/ticket.py:87 +#: users/forms/profile.py:33 users/models/group.py:13 +#: users/models/preference.py:11 users/models/user.py:792 +#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:273 +#: xpack/plugins/cloud/serializers/task.py:68 msgid "Name" msgstr "名前" @@ -551,7 +556,7 @@ msgstr "特権アカウント" #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 #: terminal/models/applet/applet.py:40 -#: terminal/models/component/endpoint.py:105 users/serializers/user.py:170 +#: terminal/models/component/endpoint.py:105 users/serializers/user.py:167 msgid "Is active" msgstr "アクティブです。" @@ -649,15 +654,15 @@ msgstr "" "{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" "情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" -#: accounts/serializers/account/account.py:30 +#: accounts/serializers/account/account.py:31 msgid "Push now" msgstr "今すぐプッシュ" -#: accounts/serializers/account/account.py:37 +#: accounts/serializers/account/account.py:38 msgid "Exist policy" msgstr "アカウントの存在ポリシー" -#: accounts/serializers/account/account.py:185 applications/models.py:11 +#: accounts/serializers/account/account.py:190 applications/models.py:11 #: assets/models/label.py:21 assets/models/platform.py:89 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 #: assets/serializers/platform.py:133 assets/serializers/platform.py:229 @@ -666,9 +671,9 @@ msgstr "アカウントの存在ポリシー" msgid "Category" msgstr "カテゴリ" -#: accounts/serializers/account/account.py:186 +#: accounts/serializers/account/account.py:191 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 -#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: acls/serializers/command_acl.py:19 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 #: assets/models/cmd_filter.py:74 assets/models/platform.py:90 #: assets/serializers/asset/common.py:122 assets/serializers/platform.py:113 @@ -686,57 +691,59 @@ msgstr "カテゴリ" msgid "Type" msgstr "タイプ" -#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:206 msgid "Asset not found" msgstr "資産が存在しません" -#: accounts/serializers/account/account.py:241 +#: accounts/serializers/account/account.py:246 msgid "Has secret" msgstr "エスクローされたパスワード" -#: accounts/serializers/account/account.py:251 ops/models/celery.py:60 +#: accounts/serializers/account/account.py:256 ops/models/celery.py:60 #: tickets/models/comment.py:13 tickets/models/ticket/general.py:45 #: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状態" -#: accounts/serializers/account/account.py:253 +#: accounts/serializers/account/account.py:258 msgid "Changed" msgstr "編集済み" -#: accounts/serializers/account/account.py:263 +#: accounts/serializers/account/account.py:268 #: accounts/serializers/automations/base.py:22 acls/models/base.py:97 +#: acls/templates/acls/asset_login_reminder.html:6 #: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: assets/serializers/automations/base.py:20 +#: authentication/api/connection_token.py:405 ops/models/base.py:17 #: ops/models/job.py:139 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" msgstr "資産" -#: accounts/serializers/account/account.py:318 +#: accounts/serializers/account/account.py:323 msgid "Account already exists" msgstr "アカウントはすでに存在しています" -#: accounts/serializers/account/account.py:368 +#: accounts/serializers/account/account.py:373 #, python-format msgid "Asset does not support this secret type: %s" msgstr "アセットはアカウント タイプをサポートしていません: %s" -#: accounts/serializers/account/account.py:400 +#: accounts/serializers/account/account.py:405 msgid "Account has exist" msgstr "アカウントはすでに存在しています" -#: accounts/serializers/account/account.py:433 +#: accounts/serializers/account/account.py:438 #: authentication/serializers/connect_token_secret.py:156 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:31 msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:440 acls/serializers/base.py:116 -#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:50 -#: audits/models.py:86 audits/models.py:164 audits/models.py:262 +#: accounts/serializers/account/account.py:448 acls/serializers/base.py:116 +#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:53 +#: audits/models.py:89 audits/models.py:171 audits/models.py:268 #: audits/serializers.py:171 authentication/models/connection_token.py:32 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 @@ -748,12 +755,12 @@ msgstr "ID" #: terminal/notifications.py:205 terminal/serializers/command.py:16 #: terminal/templates/terminal/_msg_command_warning.html:6 #: terminal/templates/terminal/_msg_session_sharing.html:6 -#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:996 -#: users/models/user.py:1032 users/serializers/group.py:18 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:987 +#: users/models/user.py:1023 users/serializers/group.py:18 msgid "User" msgstr "ユーザー" -#: accounts/serializers/account/account.py:441 +#: accounts/serializers/account/account.py:449 #: authentication/templates/authentication/_access_key_modal.html:33 #: terminal/notifications.py:158 terminal/notifications.py:207 msgid "Date" @@ -761,7 +768,7 @@ msgstr "日付" #: accounts/serializers/account/backup.py:31 #: accounts/serializers/automations/base.py:36 -#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103 +#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:104 #: settings/serializers/auth/ldap.py:66 msgid "Periodic perform" msgstr "定期的なパフォーマンス" @@ -785,7 +792,7 @@ msgid "Key password" msgstr "キーパスワード" #: accounts/serializers/account/base.py:78 -#: assets/serializers/asset/common.py:311 +#: assets/serializers/asset/common.py:378 msgid "Spec info" msgstr "特別情報" @@ -797,39 +804,35 @@ msgstr "" "ヒント: 認証にユーザー名が必要ない場合は、`null`を入力します。ADアカウントの" "場合は、`username@domain`のようになります。" -#: accounts/serializers/account/template.py:11 -#, fuzzy -#| msgid "Password strength" -msgid "Password length" -msgstr "パスワードの強さ" - -#: accounts/serializers/account/template.py:12 -#, fuzzy -#| msgid "Powershell" -msgid "Lowercase" -msgstr "PowerShell" - #: accounts/serializers/account/template.py:13 -msgid "Uppercase" -msgstr "" +msgid "Password length" +msgstr "パスワードの長さ" #: accounts/serializers/account/template.py:14 -msgid "Digit" -msgstr "" +msgid "Lowercase" +msgstr "小文字" #: accounts/serializers/account/template.py:15 -msgid "Special symbol" -msgstr "" +msgid "Uppercase" +msgstr "大文字" -#: accounts/serializers/account/template.py:36 +#: accounts/serializers/account/template.py:16 +msgid "Digit" +msgstr "数値#スウスウ#" + +#: accounts/serializers/account/template.py:17 +msgid "Special symbol" +msgstr "特殊記号" + +#: accounts/serializers/account/template.py:35 msgid "Secret generation strategy for account creation" msgstr "账号创建时,密文生成策略" -#: accounts/serializers/account/template.py:37 +#: accounts/serializers/account/template.py:36 msgid "Whether to automatically push the account to the asset" msgstr "是否自动推送账号到资产" -#: accounts/serializers/account/template.py:40 +#: accounts/serializers/account/template.py:39 msgid "" "Associated platform, you can configure push parameters. If not associated, " "default parameters will be used" @@ -840,11 +843,11 @@ msgstr "关联平台,可以配置推送参数,如果不关联,则使用默 #: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26 #: ops/models/job.py:145 ops/models/playbook.py:31 rbac/models/role.py:37 #: settings/models.py:37 terminal/models/applet/applet.py:45 -#: terminal/models/applet/applet.py:302 terminal/models/applet/host.py:142 +#: terminal/models/applet/applet.py:304 terminal/models/applet/host.py:142 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:104 #: terminal/models/session/session.py:46 tickets/models/comment.py:32 -#: tickets/models/ticket/general.py:297 users/models/user.py:834 +#: tickets/models/ticket/general.py:297 users/models/user.py:828 #: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:109 msgid "Comment" msgstr "コメント" @@ -894,11 +897,11 @@ msgstr "* パスワードの長さの範囲6-30ビット" msgid "Automation task execution" msgstr "自動タスク実行履歴" -#: accounts/serializers/automations/change_secret.py:154 audits/const.py:54 -#: audits/models.py:60 audits/signal_handlers/activity_log.py:33 -#: common/const/choices.py:18 ops/const.py:60 ops/serializers/celery.py:40 +#: accounts/serializers/automations/change_secret.py:154 audits/const.py:61 +#: audits/models.py:63 audits/signal_handlers/activity_log.py:33 +#: common/const/choices.py:18 ops/const.py:72 ops/serializers/celery.py:40 #: terminal/const.py:76 terminal/models/session/sharing.py:121 -#: tickets/views/approve.py:119 +#: tickets/views/approve.py:117 msgid "Success" msgstr "成功" @@ -933,6 +936,10 @@ msgstr "資産の口座番号を収集する" msgid "Push accounts to assets" msgstr "アカウントをアセットにプッシュ:" +#: accounts/tasks/template.py:11 +msgid "Template sync info to related accounts" +msgstr "" + #: accounts/tasks/vault.py:31 msgid "Sync secret to vault" msgstr "秘密をVaultに同期する" @@ -969,16 +976,16 @@ msgstr "秘密鍵が無効またはpassphraseエラー" msgid "Acls" msgstr "Acls" -#: acls/const.py:6 terminal/const.py:11 tickets/const.py:45 -#: tickets/templates/tickets/approve_check_password.html:49 +#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:45 +#: tickets/templates/tickets/approve_check_password.html:48 msgid "Reject" msgstr "拒否" -#: acls/const.py:7 terminal/const.py:9 +#: acls/const.py:7 audits/const.py:33 terminal/const.py:9 msgid "Accept" msgstr "受け入れられる" -#: acls/const.py:8 +#: acls/const.py:8 audits/const.py:34 msgid "Review" msgstr "レビュー担当者" @@ -986,6 +993,10 @@ msgstr "レビュー担当者" msgid "Warning" msgstr "警告" +#: acls/const.py:10 audits/const.py:35 notifications/apps.py:7 +msgid "Notifications" +msgstr "通知" + #: acls/models/base.py:37 assets/models/_user.py:51 #: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:97 #: xpack/plugins/cloud/models.py:275 @@ -1003,7 +1014,7 @@ msgstr "1-100、低い値は最初に一致します" msgid "Reviewers" msgstr "レビュー担当者" -#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: acls/models/base.py:43 authentication/models/access_key.py:20 #: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:29 @@ -1016,7 +1027,7 @@ msgid "Users" msgstr "ユーザー" #: acls/models/base.py:98 assets/models/automations/base.py:17 -#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:310 +#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:377 #: rbac/tree.py:35 msgid "Accounts" msgstr "アカウント" @@ -1048,8 +1059,8 @@ msgstr "1行1コマンド" msgid "Ignore case" msgstr "家を無視する" -#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 -#: acls/serializers/command_acl.py:28 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:97 +#: acls/serializers/command_acl.py:29 #: authentication/serializers/connect_token_secret.py:85 #: terminal/templates/terminal/_msg_command_warning.html:14 msgid "Command group" @@ -1059,12 +1070,12 @@ msgstr "コマンドグループ" msgid "The generated regular expression is incorrect: {}" msgstr "生成された正規表現が正しくありません: {}" -#: acls/models/command_acl.py:100 +#: acls/models/command_acl.py:103 #: terminal/templates/terminal/_msg_command_warning.html:12 msgid "Command acl" msgstr "コマンドフィルタリング" -#: acls/models/command_acl.py:109 tickets/const.py:11 +#: acls/models/command_acl.py:112 tickets/const.py:11 msgid "Command confirm" msgstr "コマンドの確認" @@ -1097,6 +1108,14 @@ msgstr "ログインasset acl" msgid "Login asset confirm" msgstr "ログイン資産の確認" +#: acls/notifications.py:11 +msgid "User login reminder" +msgstr "ユーザーログインのリマインダ" + +#: acls/notifications.py:42 +msgid "Asset login reminder" +msgstr "資産ログインのリマインダ" + #: acls/serializers/base.py:11 acls/serializers/login_acl.py:11 msgid "With * indicating a match all. " msgstr "* はすべて一致することを示します。" @@ -1152,6 +1171,47 @@ msgstr "IP" msgid "Time Period" msgstr "期間" +#: acls/templates/acls/asset_login_reminder.html:3 +#: acls/templates/acls/user_login_reminder.html:3 +msgid "Respectful" +msgstr "尊敬する" + +#: acls/templates/acls/asset_login_reminder.html:10 +msgid "" +"The user has just logged in to the asset. Please ensure that this is an " +"authorized operation. If you suspect that this is an unauthorized access, " +"please take appropriate measures immediately." +msgstr "" +"ユーザーは資産にログインしています。許可されたアクションであることを確認して" +"ください。不正アクセスが疑われる場合は、すぐに適切な措置を取ってください。" + +#: acls/templates/acls/asset_login_reminder.html:12 +#: acls/templates/acls/user_login_reminder.html:13 +msgid "Thank you" +msgstr "ありがとうございます。" + +#: acls/templates/acls/user_login_reminder.html:7 audits/models.py:193 +#: audits/models.py:262 +#: authentication/templates/authentication/_msg_different_city.html:11 +#: tickets/models/ticket/login_confirm.py:11 +msgid "Login city" +msgstr "ログイン都市" + +#: acls/templates/acls/user_login_reminder.html:8 audits/models.py:196 +#: audits/models.py:263 audits/serializers.py:65 +msgid "User agent" +msgstr "ユーザーエージェント" + +#: acls/templates/acls/user_login_reminder.html:11 +msgid "" +"The user has just successfully logged into the system. Please ensure that " +"this is an authorized operation. If you suspect that this is an unauthorized " +"access, please take appropriate measures immediately." +msgstr "" +"ユーザーはシステムに正常にログインしたばかりです。許可されたアクションである" +"ことを確認してください。不正アクセスが疑われる場合は、すぐに適切な措置を取っ" +"てください。" + #: applications/apps.py:9 msgid "Applications" msgstr "アプリケーション" @@ -1169,7 +1229,7 @@ msgstr "アプリケーション" msgid "Can match application" msgstr "アプリケーションを一致させることができます" -#: assets/api/asset/asset.py:157 +#: assets/api/asset/asset.py:194 msgid "Cannot create asset directly, you should create a host or other" msgstr "" "資産を直接作成することはできません。ホストまたはその他を作成する必要がありま" @@ -1191,7 +1251,7 @@ msgstr "ルートノード ({}) を削除できません。" msgid "Deletion failed and the node contains assets" msgstr "削除に失敗し、ノードにアセットが含まれています。" -#: assets/api/tree.py:49 assets/serializers/node.py:41 +#: assets/api/tree.py:49 assets/serializers/node.py:42 msgid "The same level node name cannot be the same" msgstr "同じレベルのノード名を同じにすることはできません。" @@ -1222,16 +1282,16 @@ msgid "Unable to connect to port {port} on {address}" msgstr "{port} のポート {address} に接続できません" #: assets/automations/ping_gateway/manager.py:58 -#: authentication/middleware.py:92 xpack/plugins/cloud/providers/fc.py:47 +#: authentication/middleware.py:93 xpack/plugins/cloud/providers/fc.py:47 msgid "Authentication failed" msgstr "認証に失敗しました" #: assets/automations/ping_gateway/manager.py:60 -#: assets/automations/ping_gateway/manager.py:86 +#: assets/automations/ping_gateway/manager.py:86 terminal/const.py:100 msgid "Connect failed" msgstr "接続に失敗しました" -#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:37 +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:44 #: audits/signal_handlers/activity_log.py:62 common/utils/ip/geoip/utils.py:31 #: common/utils/ip/geoip/utils.py:37 common/utils/ip/utils.py:104 msgid "Unknown" @@ -1253,25 +1313,25 @@ msgstr "テストゲートウェイ" msgid "Gather facts" msgstr "資産情報の収集" -#: assets/const/base.py:33 audits/const.py:48 +#: assets/const/base.py:32 audits/const.py:55 #: terminal/serializers/applet_host.py:32 msgid "Disabled" msgstr "無効" -#: assets/const/base.py:34 settings/serializers/basic.py:6 -#: users/serializers/preference/koko.py:15 -#: users/serializers/preference/lina.py:32 +#: assets/const/base.py:33 settings/serializers/basic.py:6 +#: users/serializers/preference/koko.py:19 +#: users/serializers/preference/lina.py:39 #: users/serializers/preference/luna.py:60 msgid "Basic" msgstr "基本" -#: assets/const/base.py:35 assets/const/protocol.py:245 +#: assets/const/base.py:34 assets/const/protocol.py:252 #: assets/models/asset/web.py:13 msgid "Script" msgstr "脚本" #: assets/const/category.py:10 assets/models/asset/host.py:8 -#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 +#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:71 #: settings/serializers/feature.py:47 terminal/models/component/endpoint.py:13 #: terminal/serializers/applet.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:72 @@ -1287,7 +1347,7 @@ msgid "Cloud service" msgstr "クラウド サービス" #: assets/const/category.py:14 assets/models/asset/gpt.py:11 -#: assets/models/asset/web.py:16 audits/const.py:35 +#: assets/models/asset/web.py:16 audits/const.py:42 #: terminal/models/applet/applet.py:27 users/const.py:47 msgid "Web" msgstr "Web" @@ -1333,11 +1393,11 @@ msgstr "ChatGPT" msgid "Other" msgstr "その他" -#: assets/const/protocol.py:48 +#: assets/const/protocol.py:49 msgid "SFTP root" msgstr "SFTPルート" -#: assets/const/protocol.py:50 +#: assets/const/protocol.py:51 #, python-brace-format msgid "" "SFTP root directory, Support variable:
- ${ACCOUNT} The connected " @@ -1348,81 +1408,81 @@ msgstr "" "ユーザー名
-${HOME}接続されたアカウントのホームディレクトリ
-${USER}" "ユーザーのユーザー名" -#: assets/const/protocol.py:65 +#: assets/const/protocol.py:66 msgid "Console" msgstr "Console" -#: assets/const/protocol.py:66 +#: assets/const/protocol.py:67 msgid "Connect to console session" msgstr "コンソールセッションに接続" -#: assets/const/protocol.py:70 +#: assets/const/protocol.py:71 msgid "Any" msgstr "任意" -#: assets/const/protocol.py:72 settings/serializers/security.py:228 +#: assets/const/protocol.py:73 settings/serializers/security.py:228 msgid "Security" msgstr "セキュリティ" -#: assets/const/protocol.py:73 +#: assets/const/protocol.py:74 msgid "Security layer to use for the connection" msgstr "接続に使用するセキュリティ レイヤー" -#: assets/const/protocol.py:79 +#: assets/const/protocol.py:80 msgid "AD domain" msgstr "AD ドメイン" -#: assets/const/protocol.py:94 +#: assets/const/protocol.py:95 msgid "Username prompt" msgstr "ユーザー名プロンプト" -#: assets/const/protocol.py:95 +#: assets/const/protocol.py:96 msgid "We will send username when we see this prompt" msgstr "このプロンプトが表示されたらユーザー名を送信します" -#: assets/const/protocol.py:100 +#: assets/const/protocol.py:101 msgid "Password prompt" msgstr "パスワードプロンプト" -#: assets/const/protocol.py:101 +#: assets/const/protocol.py:102 msgid "We will send password when we see this prompt" msgstr "このプロンプトが表示されたらパスワードを送信します" -#: assets/const/protocol.py:106 +#: assets/const/protocol.py:107 msgid "Success prompt" msgstr "成功プロンプト" -#: assets/const/protocol.py:107 +#: assets/const/protocol.py:108 msgid "We will consider login success when we see this prompt" msgstr "このプロンプトが表示されたらログイン成功とみなします" -#: assets/const/protocol.py:118 assets/models/asset/database.py:10 +#: assets/const/protocol.py:119 assets/models/asset/database.py:10 #: settings/serializers/msg.py:40 msgid "Use SSL" msgstr "SSLの使用" -#: assets/const/protocol.py:153 +#: assets/const/protocol.py:154 msgid "SYSDBA" msgstr "SYSDBA" -#: assets/const/protocol.py:154 +#: assets/const/protocol.py:155 msgid "Connect as SYSDBA" msgstr "SYSDBA として接続" -#: assets/const/protocol.py:169 +#: assets/const/protocol.py:170 msgid "" "SQL Server version, Different versions have different connection drivers" msgstr "SQL Server のバージョン。バージョンによって接続ドライバが異なります" -#: assets/const/protocol.py:192 +#: assets/const/protocol.py:199 msgid "Auth username" msgstr "ユーザー名で認証する" -#: assets/const/protocol.py:215 +#: assets/const/protocol.py:222 msgid "Safe mode" msgstr "安全モード" -#: assets/const/protocol.py:217 +#: assets/const/protocol.py:224 msgid "" "When safe mode is enabled, some operations will be disabled, such as: New " "tab, right click, visit other website, etc." @@ -1430,24 +1490,24 @@ msgstr "" "安全モードが有効になっている場合、新しいタブ、右クリック、他のウェブサイトへ" "のアクセスなど、一部の操作が無効になります" -#: assets/const/protocol.py:222 assets/models/asset/web.py:9 +#: assets/const/protocol.py:229 assets/models/asset/web.py:9 #: assets/serializers/asset/info/spec.py:16 msgid "Autofill" msgstr "自動充填" -#: assets/const/protocol.py:230 assets/models/asset/web.py:10 +#: assets/const/protocol.py:237 assets/models/asset/web.py:10 msgid "Username selector" msgstr "ユーザー名ピッカー" -#: assets/const/protocol.py:235 assets/models/asset/web.py:11 +#: assets/const/protocol.py:242 assets/models/asset/web.py:11 msgid "Password selector" msgstr "パスワードセレクター" -#: assets/const/protocol.py:240 assets/models/asset/web.py:12 +#: assets/const/protocol.py:247 assets/models/asset/web.py:12 msgid "Submit selector" msgstr "ボタンセレクターを確認する" -#: assets/const/protocol.py:263 +#: assets/const/protocol.py:270 msgid "API mode" msgstr "APIモード" @@ -1473,19 +1533,19 @@ msgstr "SSHパブリックキー" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:19 -#: audits/models.py:259 common/db/models.py:34 ops/models/base.py:54 -#: ops/models/job.py:227 users/models/user.py:1033 +#: audits/models.py:266 common/db/models.py:34 ops/models/base.py:54 +#: ops/models/job.py:227 users/models/user.py:1024 msgid "Date created" msgstr "作成された日付" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:35 users/models/user.py:855 +#: common/db/models.py:35 users/models/user.py:846 msgid "Date updated" msgstr "更新日" #: assets/models/_user.py:30 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:18 -#: common/db/models.py:32 users/models/user.py:841 +#: common/db/models.py:32 users/models/user.py:835 #: users/serializers/group.py:29 msgid "Created by" msgstr "によって作成された" @@ -1513,7 +1573,7 @@ msgstr "ユーザーと同じユーザー名" #: assets/models/_user.py:52 authentication/models/connection_token.py:41 #: authentication/serializers/connect_token_secret.py:111 #: terminal/models/applet/applet.py:42 terminal/serializers/session.py:19 -#: terminal/serializers/session.py:42 terminal/serializers/storage.py:70 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:70 msgid "Protocol" msgstr "プロトコル" @@ -1521,7 +1581,7 @@ msgstr "プロトコル" msgid "Sudo" msgstr "すど" -#: assets/models/_user.py:55 ops/const.py:49 +#: assets/models/_user.py:55 ops/const.py:49 ops/const.py:59 msgid "Shell" msgstr "シェル" @@ -1562,7 +1622,7 @@ msgid "Cloud" msgstr "クラウド サービス" #: assets/models/asset/common.py:92 assets/models/platform.py:16 -#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 +#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:72 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" @@ -1588,7 +1648,7 @@ msgstr "ドメイン" msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:312 +#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:379 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "資産ハードウェア情報の収集" @@ -1638,7 +1698,7 @@ msgid "Proxy" msgstr "プロキシー" #: assets/models/automations/base.py:22 ops/models/job.py:223 -#: settings/serializers/auth/sms.py:99 +#: settings/serializers/auth/sms.py:103 msgid "Parameters" msgstr "パラメータ" @@ -1650,9 +1710,9 @@ msgstr "自動化されたタスク" msgid "Asset automation task" msgstr "アセットの自動化タスク" -#: assets/models/automations/base.py:113 audits/models.py:200 +#: assets/models/automations/base.py:113 audits/models.py:207 #: audits/serializers.py:51 ops/models/base.py:49 ops/models/job.py:220 -#: terminal/models/applet/applet.py:301 terminal/models/applet/host.py:139 +#: terminal/models/applet/applet.py:303 terminal/models/applet/host.py:139 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:115 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1679,7 +1739,7 @@ msgstr "確認済みの日付" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:32 users/models/group.py:25 -#: users/models/user.py:804 +#: users/models/user.py:798 msgid "User group" msgstr "ユーザーグループ" @@ -1729,11 +1789,11 @@ msgstr "デフォルト" msgid "Default asset group" msgstr "デフォルトアセットグループ" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1018 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1009 msgid "System" msgstr "システム" -#: assets/models/label.py:19 assets/models/node.py:544 +#: assets/models/label.py:19 assets/models/node.py:539 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:29 #: authentication/serializers/connect_token_secret.py:122 @@ -1755,28 +1815,28 @@ msgstr "ラベル" msgid "New node" msgstr "新しいノード" -#: assets/models/node.py:472 audits/backends/db.py:55 audits/backends/db.py:56 +#: assets/models/node.py:467 audits/backends/db.py:55 audits/backends/db.py:56 msgid "empty" msgstr "空" -#: assets/models/node.py:543 perms/models/perm_node.py:28 +#: assets/models/node.py:538 perms/models/perm_node.py:28 msgid "Key" msgstr "キー" -#: assets/models/node.py:545 assets/serializers/node.py:20 +#: assets/models/node.py:540 assets/serializers/node.py:20 msgid "Full value" msgstr "フルバリュー" -#: assets/models/node.py:549 perms/models/perm_node.py:30 +#: assets/models/node.py:544 perms/models/perm_node.py:30 msgid "Parent key" msgstr "親キー" -#: assets/models/node.py:558 perms/serializers/permission.py:35 +#: assets/models/node.py:553 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:322 msgid "Node" msgstr "ノード" -#: assets/models/node.py:561 +#: assets/models/node.py:556 msgid "Can match node" msgstr "ノードを一致させることができます" @@ -1798,7 +1858,7 @@ msgstr "開ける" msgid "Setting" msgstr "設定" -#: assets/models/platform.py:38 audits/const.py:49 +#: assets/models/platform.py:38 audits/const.py:56 #: authentication/backends/passkey/models.py:11 settings/models.py:36 #: terminal/serializers/applet_host.py:33 msgid "Enabled" @@ -1925,35 +1985,30 @@ msgid "Node path" msgstr "ノードパスです" #: assets/serializers/asset/common.py:145 -#: assets/serializers/asset/common.py:313 +#: assets/serializers/asset/common.py:380 msgid "Auto info" msgstr "自動情報" -#: assets/serializers/asset/common.py:236 +#: assets/serializers/asset/common.py:238 msgid "Platform not exist" msgstr "プラットフォームが存在しません" -#: assets/serializers/asset/common.py:272 +#: assets/serializers/asset/common.py:274 msgid "port out of range (0-65535)" msgstr "ポート番号が範囲外です (0-65535)" -#: assets/serializers/asset/common.py:279 +#: assets/serializers/asset/common.py:281 msgid "Protocol is required: {}" msgstr "プロトコルが必要です: {}" -#: assets/serializers/asset/database.py:13 +#: assets/serializers/asset/common.py:309 +msgid "Invalid data" +msgstr "無効なデータ" + +#: assets/serializers/asset/database.py:12 msgid "Default database" msgstr "デフォルト・データベース" -#: assets/serializers/asset/database.py:28 common/db/fields.py:570 -#: common/db/fields.py:575 common/serializers/fields.py:104 -#: tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -#: xpack/plugins/cloud/serializers/account_attrs.py:79 -#: xpack/plugins/cloud/serializers/account_attrs.py:143 -msgid "This field is required." -msgstr "このフィールドは必須です。" - #: assets/serializers/asset/gpt.py:20 msgid "" "If the server cannot directly connect to the API address, you need set up an " @@ -2195,7 +2250,7 @@ msgstr "Rmdir" #: audits/const.py:14 audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:65 -#: perms/const.py:17 rbac/tree.py:235 +#: perms/const.py:17 rbac/tree.py:237 msgid "Delete" msgstr "削除" @@ -2212,7 +2267,7 @@ msgid "Symlink" msgstr "Symlink" #: audits/const.py:18 audits/const.py:28 perms/const.py:14 -#: terminal/api/session/session.py:141 +#: terminal/api/session/session.py:146 msgid "Download" msgstr "ダウンロード" @@ -2220,7 +2275,7 @@ msgstr "ダウンロード" msgid "Rename dir" msgstr "マップディレクトリ" -#: audits/const.py:23 rbac/tree.py:233 terminal/api/session/session.py:252 +#: audits/const.py:23 rbac/tree.py:235 terminal/api/session/session.py:257 #: terminal/templates/terminal/_msg_command_warning.html:18 #: terminal/templates/terminal/_msg_session_sharing.html:10 msgid "View" @@ -2228,7 +2283,7 @@ msgstr "表示" #: audits/const.py:26 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:232 +#: rbac/tree.py:234 msgid "Create" msgstr "作成" @@ -2236,8 +2291,8 @@ msgstr "作成" msgid "Connect" msgstr "接続" -#: audits/const.py:30 authentication/templates/authentication/login.html:252 -#: authentication/templates/authentication/login.html:325 +#: audits/const.py:30 authentication/templates/authentication/login.html:290 +#: authentication/templates/authentication/login.html:363 #: templates/_header_bar.html:95 msgid "Login" msgstr "ログイン" @@ -2246,30 +2301,41 @@ msgstr "ログイン" msgid "Change password" msgstr "パスワードを変更する" -#: audits/const.py:36 settings/serializers/terminal.py:6 +#: audits/const.py:37 tickets/const.py:46 +msgid "Approve" +msgstr "承認" + +#: audits/const.py:38 +#: authentication/templates/authentication/_access_key_modal.html:155 +#: authentication/templates/authentication/_mfa_confirm_modal.html:53 +#: templates/_modal.html:22 tickets/const.py:44 +msgid "Close" +msgstr "閉じる" + +#: audits/const.py:43 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:164 -#: terminal/serializers/session.py:49 terminal/serializers/session.py:63 +#: terminal/serializers/session.py:52 terminal/serializers/session.py:66 msgid "Terminal" msgstr "ターミナル" -#: audits/const.py:41 audits/models.py:128 +#: audits/const.py:48 audits/models.py:131 msgid "Operate log" msgstr "ログの操作" -#: audits/const.py:42 +#: audits/const.py:49 msgid "Session log" msgstr "セッションログ" -#: audits/const.py:43 +#: audits/const.py:50 msgid "Login log" msgstr "ログインログ" -#: audits/const.py:44 terminal/models/applet/host.py:143 +#: audits/const.py:51 terminal/models/applet/host.py:143 #: terminal/models/component/task.py:22 msgid "Task" msgstr "タスク" -#: audits/const.py:50 +#: audits/const.py:57 msgid "-" msgstr "-" @@ -2281,28 +2347,28 @@ msgstr "是" msgid "No" msgstr "否" -#: audits/models.py:43 +#: audits/models.py:46 msgid "Job audit log" msgstr "ジョブ監査ログ" -#: audits/models.py:52 audits/models.py:96 audits/models.py:167 +#: audits/models.py:55 audits/models.py:99 audits/models.py:174 #: terminal/models/session/session.py:38 terminal/models/session/sharing.py:113 msgid "Remote addr" msgstr "リモートaddr" -#: audits/models.py:57 audits/serializers.py:35 +#: audits/models.py:60 audits/serializers.py:35 msgid "Operate" msgstr "操作" -#: audits/models.py:59 +#: audits/models.py:62 msgid "Filename" msgstr "ファイル名" -#: audits/models.py:62 common/serializers/common.py:98 +#: audits/models.py:65 common/serializers/common.py:98 msgid "File" msgstr "書類" -#: audits/models.py:63 terminal/backends/command/models.py:21 +#: audits/models.py:66 terminal/backends/command/models.py:21 #: terminal/models/session/replay.py:9 terminal/models/session/sharing.py:20 #: terminal/models/session/sharing.py:95 #: terminal/templates/terminal/_msg_command_alert.html:10 @@ -2311,103 +2377,86 @@ msgstr "書類" msgid "Session" msgstr "セッション" -#: audits/models.py:66 +#: audits/models.py:69 msgid "File transfer log" msgstr "ファイル転送ログ" -#: audits/models.py:90 audits/serializers.py:86 +#: audits/models.py:93 audits/serializers.py:86 msgid "Resource Type" msgstr "リソースタイプ" -#: audits/models.py:91 audits/models.py:94 audits/models.py:140 +#: audits/models.py:94 audits/models.py:97 audits/models.py:143 #: audits/serializers.py:85 msgid "Resource" msgstr "リソース" -#: audits/models.py:97 audits/models.py:143 audits/models.py:169 +#: audits/models.py:100 audits/models.py:146 audits/models.py:176 #: terminal/serializers/command.py:75 msgid "Datetime" msgstr "時間" -#: audits/models.py:136 +#: audits/models.py:139 msgid "Activity type" msgstr "活動の種類" -#: audits/models.py:146 +#: audits/models.py:149 msgid "Detail" msgstr "詳細" -#: audits/models.py:149 +#: audits/models.py:152 msgid "Detail ID" msgstr "詳細 ID" -#: audits/models.py:153 +#: audits/models.py:156 msgid "Activity log" msgstr "活動記録" -#: audits/models.py:165 +#: audits/models.py:172 msgid "Change by" msgstr "による変更" -#: audits/models.py:175 +#: audits/models.py:182 msgid "Password change log" msgstr "パスワード変更ログ" -#: audits/models.py:182 audits/models.py:257 +#: audits/models.py:189 audits/models.py:264 msgid "Login type" msgstr "ログインタイプ" -#: audits/models.py:184 audits/models.py:253 +#: audits/models.py:191 audits/models.py:260 #: tickets/models/ticket/login_confirm.py:10 msgid "Login IP" msgstr "ログインIP" -#: audits/models.py:186 audits/models.py:255 -#: authentication/templates/authentication/_msg_different_city.html:11 -#: tickets/models/ticket/login_confirm.py:11 -msgid "Login city" -msgstr "ログイン都市" - -#: audits/models.py:189 audits/models.py:256 audits/serializers.py:65 -msgid "User agent" -msgstr "ユーザーエージェント" - -#: audits/models.py:192 audits/serializers.py:49 +#: audits/models.py:199 audits/serializers.py:49 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:821 +#: users/forms/profile.py:65 users/models/user.py:815 #: users/serializers/profile.py:102 msgid "MFA" msgstr "MFA" -#: audits/models.py:202 +#: audits/models.py:209 msgid "Date login" msgstr "日付ログイン" -#: audits/models.py:204 audits/models.py:258 audits/serializers.py:67 -#: audits/serializers.py:183 +#: audits/models.py:211 audits/models.py:265 audits/serializers.py:67 +#: audits/serializers.py:184 msgid "Authentication backend" msgstr "認証バックエンド" -#: audits/models.py:248 +#: audits/models.py:255 msgid "User login log" msgstr "ユーザーログインログ" -#: audits/models.py:254 +#: audits/models.py:261 msgid "Session key" msgstr "セッションID" -#: audits/models.py:260 authentication/models/connection_token.py:47 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 -#: tickets/models/ticket/apply_application.py:31 -#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:839 -msgid "Date expired" -msgstr "期限切れの日付" - -#: audits/models.py:278 +#: audits/models.py:300 msgid "User session" msgstr "ユーザーセッション" -#: audits/models.py:280 +#: audits/models.py:302 msgid "Offline ussr session" msgstr "ユーザー・セッションの下限" @@ -2420,6 +2469,13 @@ msgstr "理由表示" msgid "User %s %s this resource" msgstr "ユーザー %s %s が現在のリソースをサブスクライブしました。" +#: audits/serializers.py:172 authentication/models/connection_token.py:47 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 +#: tickets/models/ticket/apply_application.py:31 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:833 +msgid "Date expired" +msgstr "期限切れの日付" + #: audits/signal_handlers/activity_log.py:26 #, python-format msgid "User %s use account %s login asset %s" @@ -2435,46 +2491,46 @@ msgstr "ユーザー %s はシステム %s にログインしています" msgid "User %s perform a task for this resource: %s" msgstr "ユーザー %s が現在のリソースでタスク (%s) を実行しました" -#: audits/signal_handlers/login_log.py:34 +#: audits/signal_handlers/login_log.py:33 msgid "SSH Key" msgstr "SSHキー" -#: audits/signal_handlers/login_log.py:36 settings/serializers/auth/sso.py:13 +#: audits/signal_handlers/login_log.py:35 settings/serializers/auth/sso.py:13 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers/login_log.py:37 +#: audits/signal_handlers/login_log.py:36 msgid "Auth Token" msgstr "認証トークン" -#: audits/signal_handlers/login_log.py:38 authentication/notifications.py:73 -#: authentication/views/login.py:77 authentication/views/wecom.py:159 +#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73 +#: authentication/views/login.py:77 authentication/views/wecom.py:160 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:751 users/models/user.py:856 +#: users/models/user.py:745 users/models/user.py:847 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers/login_log.py:39 authentication/views/feishu.py:122 +#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:123 #: authentication/views/login.py:89 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 -#: settings/serializers/auth/feishu.py:13 users/models/user.py:753 -#: users/models/user.py:858 +#: settings/serializers/auth/feishu.py:13 users/models/user.py:747 +#: users/models/user.py:849 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers/login_log.py:40 authentication/views/dingtalk.py:159 +#: audits/signal_handlers/login_log.py:39 authentication/views/dingtalk.py:160 #: authentication/views/login.py:83 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:752 -#: users/models/user.py:857 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:746 +#: users/models/user.py:848 msgid "DingTalk" msgstr "DingTalk" -#: audits/signal_handlers/login_log.py:41 +#: audits/signal_handlers/login_log.py:40 #: authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "仮パスワード" -#: audits/signal_handlers/login_log.py:42 authentication/views/login.py:95 +#: audits/signal_handlers/login_log.py:41 authentication/views/login.py:95 #: settings/serializers/auth/passkey.py:8 msgid "Passkey" msgstr "" @@ -2487,33 +2543,37 @@ msgstr "監査セッション タスク ログのクリーンアップ" msgid "Upload FTP file to external storage" msgstr "外部ストレージへのFTPファイルのアップロード" -#: authentication/api/confirm.py:40 +#: authentication/api/access_key.py:39 +msgid "Access keys can be created at most 10" +msgstr "" + +#: authentication/api/confirm.py:50 msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" -#: authentication/api/connection_token.py:260 +#: authentication/api/connection_token.py:262 msgid "Reusable connection token is not allowed, global setting not enabled" msgstr "" "再使用可能な接続トークンの使用は許可されていません。グローバル設定は有効に" "なっていません" -#: authentication/api/connection_token.py:351 +#: authentication/api/connection_token.py:375 msgid "Anonymous account is not supported for this asset" msgstr "匿名アカウントはこのプロパティではサポートされていません" -#: authentication/api/connection_token.py:370 +#: authentication/api/connection_token.py:394 msgid "Account not found" msgstr "アカウントが見つかりません" -#: authentication/api/connection_token.py:373 +#: authentication/api/connection_token.py:397 msgid "Permission expired" msgstr "承認の有効期限が切れています" -#: authentication/api/connection_token.py:387 +#: authentication/api/connection_token.py:427 msgid "ACL action is reject: {}({})" msgstr "ACL アクションは拒否です: {}({})" -#: authentication/api/connection_token.py:391 +#: authentication/api/connection_token.py:431 msgid "ACL action is review" msgstr "ACL アクションはレビューです" @@ -2521,16 +2581,16 @@ msgstr "ACL アクションはレビューです" msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" -#: authentication/api/password.py:32 terminal/api/session/session.py:300 -#: users/views/profile/reset.py:44 +#: authentication/api/password.py:34 terminal/api/session/session.py:305 +#: users/views/profile/reset.py:61 msgid "User does not exist: {}" msgstr "ユーザーが存在しない: {}" -#: authentication/api/password.py:32 users/views/profile/reset.py:127 +#: authentication/api/password.py:34 users/views/profile/reset.py:163 msgid "No user matched" msgstr "ユーザーにマッチしなかった" -#: authentication/api/password.py:36 +#: authentication/api/password.py:38 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" @@ -2538,8 +2598,8 @@ msgstr "" "ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" "さい。" -#: authentication/api/password.py:64 -#: authentication/templates/authentication/login.html:317 +#: authentication/api/password.py:67 +#: authentication/templates/authentication/login.html:355 #: users/templates/users/forgot_password.html:27 #: users/templates/users/forgot_password.html:28 #: users/templates/users/forgot_password_previewing.html:13 @@ -2556,60 +2616,29 @@ msgstr "認証" msgid "User invalid, disabled or expired" msgstr "ユーザーが無効、無効、または期限切れです" -#: authentication/backends/drf.py:54 -msgid "Invalid signature header. No credentials provided." -msgstr "署名ヘッダーが無効です。資格情報は提供されていません。" - -#: authentication/backends/drf.py:57 -msgid "Invalid signature header. Signature string should not contain spaces." -msgstr "署名ヘッダーが無効です。署名文字列にはスペースを含まないでください。" - -#: authentication/backends/drf.py:64 -msgid "Invalid signature header. Format like AccessKeyId:Signature" -msgstr "署名ヘッダーが無効です。AccessKeyIdのような形式: Signature" - -#: authentication/backends/drf.py:68 -msgid "" -"Invalid signature header. Signature string should not contain invalid " -"characters." -msgstr "" -"署名ヘッダーが無効です。署名文字列に無効な文字を含めることはできません。" - -#: authentication/backends/drf.py:88 authentication/backends/drf.py:104 -msgid "Invalid signature." -msgstr "署名が無効です。" - -#: authentication/backends/drf.py:95 -msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" -msgstr "HTTP header: Date not provide or not" - -#: authentication/backends/drf.py:100 -msgid "Expired, more than 15 minutes" -msgstr "期限切れ、15分以上" - -#: authentication/backends/drf.py:107 -msgid "User disabled." -msgstr "ユーザーが無効になりました。" - -#: authentication/backends/drf.py:125 +#: authentication/backends/drf.py:39 msgid "Invalid token header. No credentials provided." msgstr "無効なトークンヘッダー。資格情報は提供されていません。" -#: authentication/backends/drf.py:128 +#: authentication/backends/drf.py:42 msgid "Invalid token header. Sign string should not contain spaces." msgstr "無効なトークンヘッダー。記号文字列にはスペースを含めないでください。" -#: authentication/backends/drf.py:135 +#: authentication/backends/drf.py:48 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" "無効なトークンヘッダー。署名文字列に無効な文字を含めることはできません。" -#: authentication/backends/drf.py:146 +#: authentication/backends/drf.py:61 msgid "Invalid token or cache refreshed." msgstr "無効なトークンまたはキャッシュの更新。" -#: authentication/backends/passkey/api.py:52 +#: authentication/backends/passkey/api.py:37 +msgid "Only register passkey for local user" +msgstr "" + +#: authentication/backends/passkey/api.py:65 msgid "Auth failed" msgstr "認証に失敗しました" @@ -2622,6 +2651,8 @@ msgid "Added on" msgstr "に追加" #: authentication/backends/passkey/models.py:14 +#: authentication/models/access_key.py:21 +#: authentication/models/private_token.py:8 msgid "Date last used" msgstr "最後に使用した日付" @@ -2765,21 +2796,21 @@ msgstr "電話が設定されていない" msgid "SSO auth closed" msgstr "SSO authは閉鎖されました" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:61 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:62 msgid "WeCom is already bound" msgstr "企業の微信はすでにバインドされています" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:202 -#: authentication/views/wecom.py:244 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:204 +#: authentication/views/wecom.py:246 msgid "WeCom is not bound" msgstr "企業の微信をバインドしていません" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:209 -#: authentication/views/dingtalk.py:251 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:211 +#: authentication/views/dingtalk.py:253 msgid "DingTalk is not bound" msgstr "DingTalkはバインドされていません" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:166 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:168 msgid "FeiShu is not bound" msgstr "本を飛ばすは拘束されていません" @@ -2787,15 +2818,20 @@ msgstr "本を飛ばすは拘束されていません" msgid "Your password is invalid" msgstr "パスワードが無効です" -#: authentication/errors/redirect.py:85 authentication/mixins.py:318 +#: authentication/errors/mfa.py:43 +#, python-format +msgid "Please wait for %s seconds before retry" +msgstr "%s 秒後に再試行してください" + +#: authentication/errors/redirect.py:85 authentication/mixins.py:323 msgid "Your password is too simple, please change it for security" msgstr "パスワードがシンプルすぎるので、セキュリティのために変更してください" -#: authentication/errors/redirect.py:93 authentication/mixins.py:325 +#: authentication/errors/redirect.py:93 authentication/mixins.py:330 msgid "You should to change your password before login" msgstr "ログインする前にパスワードを変更する必要があります" -#: authentication/errors/redirect.py:101 authentication/mixins.py:332 +#: authentication/errors/redirect.py:101 authentication/mixins.py:337 msgid "Your password has expired, please reset before logging in" msgstr "" "パスワードの有効期限が切れました。ログインする前にリセットしてください。" @@ -2876,9 +2912,9 @@ msgstr "メッセージ検証コードが無効" #: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 -#: settings/serializers/auth/sms.py:28 users/forms/profile.py:104 +#: settings/serializers/auth/sms.py:32 users/forms/profile.py:104 #: users/forms/profile.py:109 users/templates/users/forgot_password.html:112 -#: users/views/profile/reset.py:79 +#: users/views/profile/reset.py:98 msgid "SMS" msgstr "メッセージ" @@ -2894,11 +2930,15 @@ msgstr "電話番号を設定して有効にする" msgid "Clear phone number to disable" msgstr "無効にする電話番号をクリアする" -#: authentication/middleware.py:93 settings/utils/ldap.py:661 +#: authentication/middleware.py:94 settings/utils/ldap.py:679 msgid "Authentication failed (before login check failed): {}" msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" -#: authentication/mixins.py:91 +#: authentication/mixins.py:82 +msgid "User is invalid" +msgstr "無効なユーザーです" + +#: authentication/mixins.py:97 msgid "" "The administrator has enabled 'Only allow login from user source'. \n" " The current user source is {}. Please contact the administrator." @@ -2906,11 +2946,11 @@ msgstr "" "管理者は「ユーザーソースからのみログインを許可」をオンにしており、現在のユー" "ザーソースは {} です。管理者に連絡してください。" -#: authentication/mixins.py:268 +#: authentication/mixins.py:273 msgid "The MFA type ({}) is not enabled" msgstr "MFAタイプ ({}) が有効になっていない" -#: authentication/mixins.py:308 +#: authentication/mixins.py:313 msgid "Please change your password" msgstr "パスワードを変更してください" @@ -2989,7 +3029,7 @@ msgstr "スーパー接続トークンのシークレットを表示できます msgid "Super connection token" msgstr "スーパー接続トークン" -#: authentication/models/private_token.py:9 +#: authentication/models/private_token.py:11 msgid "Private Token" msgstr "プライベートトークン" @@ -3047,7 +3087,7 @@ msgstr "アクション" #: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:174 +#: users/serializers/user.py:97 users/serializers/user.py:171 msgid "Is expired" msgstr "期限切れです" @@ -3055,9 +3095,9 @@ msgstr "期限切れです" #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/msg.py:22 #: settings/serializers/msg.py:57 users/forms/profile.py:102 -#: users/forms/profile.py:109 users/models/user.py:800 +#: users/forms/profile.py:109 users/models/user.py:794 #: users/templates/users/forgot_password.html:117 -#: users/views/profile/reset.py:73 +#: users/views/profile/reset.py:92 msgid "Email" msgstr "メール" @@ -3066,9 +3106,9 @@ msgstr "メール" msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: authentication/serializers/token.py:86 perms/serializers/permission.py:37 #: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: users/serializers/user.py:168 msgid "Is valid" msgstr "有効です" @@ -3093,13 +3133,13 @@ msgid "Show" msgstr "表示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:646 users/serializers/profile.py:92 +#: users/models/user.py:640 users/serializers/profile.py:92 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "無効化" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:647 users/serializers/profile.py:93 +#: users/models/user.py:641 users/serializers/profile.py:93 #: users/templates/users/mfa_setting.html:26 #: users/templates/users/mfa_setting.html:68 msgid "Enable" @@ -3109,12 +3149,6 @@ msgstr "有効化" msgid "Delete success" msgstr "削除成功" -#: authentication/templates/authentication/_access_key_modal.html:155 -#: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/const.py:44 -msgid "Close" -msgstr "閉じる" - #: authentication/templates/authentication/_captcha_field.html:8 msgid "Play CAPTCHA as audio file" msgstr "CAPTCHAをオーディオファイルとして再生する" @@ -3144,7 +3178,7 @@ msgstr "コードエラー" #: 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:447 +#: jumpserver/conf.py:449 #: 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 @@ -3256,7 +3290,7 @@ msgstr "" msgid "Cancel" msgstr "キャンセル" -#: authentication/templates/authentication/login.html:237 +#: authentication/templates/authentication/login.html:270 msgid "" "Configuration file has problems and cannot be logged in. Please contact the " "administrator or view latest docs" @@ -3264,11 +3298,11 @@ msgstr "" "設定ファイルに問題があり、ログインできません。管理者に連絡するか、最新のド" "キュメントを参照してください。" -#: authentication/templates/authentication/login.html:238 +#: authentication/templates/authentication/login.html:271 msgid "If you are administrator, you can update the config resolve it, set" msgstr "管理者の場合は、configを更新して解決することができます。" -#: authentication/templates/authentication/login.html:332 +#: authentication/templates/authentication/login.html:370 msgid "More login options" msgstr "その他のログインオプション" @@ -3316,79 +3350,79 @@ msgstr "" msgid "Do you want to retry ?" msgstr "再試行しますか?" -#: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 +#: authentication/utils.py:23 common/utils/ip/geoip/utils.py:24 #: xpack/plugins/cloud/const.py:29 msgid "LAN" msgstr "ローカルエリアネットワーク" -#: authentication/views/base.py:61 +#: authentication/views/base.py:67 #: perms/templates/perms/_msg_permed_items_expire.html:21 msgid "If you have any question, please contact the administrator" msgstr "質問があったら、管理者に連絡して下さい" -#: authentication/views/dingtalk.py:41 +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "DingTalkエラー、システム管理者に連絡してください" -#: authentication/views/dingtalk.py:44 authentication/views/dingtalk.py:208 +#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:210 msgid "DingTalk Error" msgstr "DingTalkエラー" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:50 -#: authentication/views/wecom.py:57 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:58 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "システム設定が正しくありません。管理者に連絡してください" -#: authentication/views/dingtalk.py:60 +#: authentication/views/dingtalk.py:61 msgid "DingTalk is already bound" msgstr "DingTalkはすでにバインドされています" -#: authentication/views/dingtalk.py:128 authentication/views/wecom.py:129 +#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:130 msgid "Invalid user_id" msgstr "無効なuser_id" -#: authentication/views/dingtalk.py:144 +#: authentication/views/dingtalk.py:145 msgid "DingTalk query user failed" msgstr "DingTalkクエリユーザーが失敗しました" -#: authentication/views/dingtalk.py:153 +#: authentication/views/dingtalk.py:154 msgid "The DingTalk is already bound to another user" msgstr "DingTalkはすでに別のユーザーにバインドされています" -#: authentication/views/dingtalk.py:160 +#: authentication/views/dingtalk.py:161 msgid "Binding DingTalk successfully" msgstr "DingTalkのバインドに成功" -#: authentication/views/dingtalk.py:210 authentication/views/dingtalk.py:245 +#: authentication/views/dingtalk.py:212 authentication/views/dingtalk.py:247 msgid "Failed to get user from DingTalk" msgstr "DingTalkからユーザーを取得できませんでした" -#: authentication/views/dingtalk.py:252 +#: authentication/views/dingtalk.py:254 msgid "Please login with a password and then bind the DingTalk" msgstr "パスワードでログインし、DingTalkをバインドしてください" -#: authentication/views/feishu.py:38 authentication/views/feishu.py:165 +#: authentication/views/feishu.py:39 authentication/views/feishu.py:167 msgid "FeiShu Error" msgstr "FeiShuエラー" -#: authentication/views/feishu.py:66 +#: authentication/views/feishu.py:67 msgid "FeiShu is already bound" msgstr "FeiShuはすでにバインドされています" -#: authentication/views/feishu.py:107 +#: authentication/views/feishu.py:108 msgid "FeiShu query user failed" msgstr "FeiShuクエリユーザーが失敗しました" -#: authentication/views/feishu.py:116 +#: authentication/views/feishu.py:117 msgid "The FeiShu is already bound to another user" msgstr "FeiShuはすでに別のユーザーにバインドされています" -#: authentication/views/feishu.py:123 +#: authentication/views/feishu.py:124 msgid "Binding FeiShu successfully" msgstr "本を飛ばすのバインドに成功" -#: authentication/views/feishu.py:167 +#: authentication/views/feishu.py:169 msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" @@ -3408,7 +3442,7 @@ msgstr "ログインタイムアウト、もう一度お試しください" msgid "User email already exists ({})" msgstr "ユーザー メールボックスは既に存在します ({})" -#: authentication/views/login.py:361 +#: authentication/views/login.py:355 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3416,43 +3450,43 @@ msgstr "" "{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n" " このページを閉じないでください" -#: authentication/views/login.py:366 +#: authentication/views/login.py:360 msgid "No ticket found" msgstr "チケットが見つかりません" -#: authentication/views/login.py:402 +#: authentication/views/login.py:396 msgid "Logout success" msgstr "ログアウト成功" -#: authentication/views/login.py:403 +#: authentication/views/login.py:397 msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" -#: authentication/views/wecom.py:42 +#: authentication/views/wecom.py:43 msgid "WeCom Error, Please contact your system administrator" msgstr "企業微信エラー、システム管理者に連絡してください" -#: authentication/views/wecom.py:45 authentication/views/wecom.py:201 +#: authentication/views/wecom.py:46 authentication/views/wecom.py:203 msgid "WeCom Error" msgstr "企業微信エラー" -#: authentication/views/wecom.py:144 +#: authentication/views/wecom.py:145 msgid "WeCom query user failed" msgstr "企業微信ユーザーの問合せに失敗しました" -#: authentication/views/wecom.py:153 +#: authentication/views/wecom.py:154 msgid "The WeCom is already bound to another user" msgstr "この企業の微信はすでに他のユーザーをバインドしている。" -#: authentication/views/wecom.py:160 +#: authentication/views/wecom.py:161 msgid "Binding WeCom successfully" msgstr "企業の微信のバインドに成功" -#: authentication/views/wecom.py:203 authentication/views/wecom.py:238 +#: authentication/views/wecom.py:205 authentication/views/wecom.py:240 msgid "Failed to get user from WeCom" msgstr "企業の微信からユーザーを取得できませんでした" -#: authentication/views/wecom.py:245 +#: authentication/views/wecom.py:247 msgid "Please login with a password and then bind the WeCom" msgstr "パスワードでログインしてからWeComをバインドしてください" @@ -3477,7 +3511,7 @@ msgstr "の準備を" msgid "Pending" msgstr "未定" -#: common/const/choices.py:17 ops/const.py:59 +#: common/const/choices.py:17 ops/const.py:71 msgid "Running" msgstr "ランニング" @@ -3545,6 +3579,14 @@ msgstr "無効なタイプです。all、ids、またはattrsでなければな msgid "Invalid ids for ids, should be a list" msgstr "無効なID、リストでなければなりません" +#: common/db/fields.py:570 common/db/fields.py:575 +#: common/serializers/fields.py:104 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +#: xpack/plugins/cloud/serializers/account_attrs.py:79 +#: xpack/plugins/cloud/serializers/account_attrs.py:143 +msgid "This field is required." +msgstr "このフィールドは必須です。" + #: common/db/fields.py:573 common/db/fields.py:578 msgid "Invalid attrs, should be a list of dict" msgstr "無効な属性、dictリストでなければなりません" @@ -3561,7 +3603,7 @@ msgstr "は破棄されます" msgid "discard time" msgstr "時間を捨てる" -#: common/db/models.py:33 users/models/user.py:842 +#: common/db/models.py:33 users/models/user.py:836 msgid "Updated by" msgstr "によって更新" @@ -3618,11 +3660,11 @@ msgstr "M2Mリバースは許可されません" msgid "Is referenced by other objects and cannot be deleted" msgstr "他のオブジェクトによって参照され、削除できません。" -#: common/exceptions.py:48 +#: common/exceptions.py:51 msgid "This action require confirm current user" msgstr "このアクションでは、MFAの確認が必要です。" -#: common/exceptions.py:56 +#: common/exceptions.py:59 msgid "Unexpect error occur" msgstr "予期しないエラーが発生します" @@ -3654,6 +3696,15 @@ msgstr "SP idは6ビット" msgid "Failed to connect to the CMPP gateway server, err: {}" msgstr "接続ゲートウェイサーバエラー, 非: {}" +#: common/sdk/sms/custom_file.py:41 +msgid "The custom sms file is invalid" +msgstr "カスタムショートメッセージファイルが無効です" + +#: common/sdk/sms/custom_file.py:47 +#, python-format +msgid "SMS sending failed[%s]: %s" +msgstr "ショートメッセージの送信に失敗しました[%s]: %s" + #: common/sdk/sms/endpoint.py:16 msgid "Alibaba cloud" msgstr "アリ雲" @@ -3670,11 +3721,11 @@ msgstr "華為雲" msgid "CMPP v2.0" msgstr "CMPP v2.0" -#: common/sdk/sms/endpoint.py:31 +#: common/sdk/sms/endpoint.py:32 msgid "SMS provider not support: {}" msgstr "SMSプロバイダーはサポートしていません: {}" -#: common/sdk/sms/endpoint.py:53 +#: common/sdk/sms/endpoint.py:54 msgid "SMS verification code signature or template invalid" msgstr "SMS検証コードの署名またはテンプレートが無効" @@ -3745,11 +3796,16 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" -#: jumpserver/conf.py:446 +#: jumpserver/conf.py:444 +#, python-brace-format +msgid "The verification code is: {code}" +msgstr "認証コードは: {code}" + +#: jumpserver/conf.py:448 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:448 +#: jumpserver/conf.py:450 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -3789,10 +3845,6 @@ msgstr "" "いる場合は、nginxリスニングポートにアクセスしていないことを証明してください。" "頑張ってください。" -#: notifications/apps.py:7 -msgid "Notifications" -msgstr "通知" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "サイトメッセージ" @@ -3817,15 +3869,15 @@ msgstr "システムメッセージ" msgid "Publish the station message" msgstr "投稿サイトニュース" -#: ops/ansible/inventory.py:92 ops/models/job.py:60 +#: ops/ansible/inventory.py:95 ops/models/job.py:60 msgid "No account available" msgstr "利用可能なアカウントがありません" -#: ops/ansible/inventory.py:263 +#: ops/ansible/inventory.py:259 msgid "Ansible disabled" msgstr "Ansible 無効" -#: ops/ansible/inventory.py:279 +#: ops/ansible/inventory.py:275 msgid "Skip hosts below:" msgstr "次のホストをスキップします: " @@ -3841,14 +3893,34 @@ msgstr "タスクは存在しません" msgid "Task {} args or kwargs error" msgstr "タスク実行パラメータエラー" -#: ops/api/playbook.py:37 +#: ops/api/playbook.py:39 msgid "Currently playbook is being used in a job" msgstr "現在プレイブックは1つのジョブで使用されています" -#: ops/api/playbook.py:91 +#: ops/api/playbook.py:93 msgid "Unsupported file content" msgstr "サポートされていないファイルの内容" +#: ops/api/playbook.py:95 ops/api/playbook.py:141 ops/api/playbook.py:189 +msgid "Invalid file path" +msgstr "無効なファイルパス" + +#: ops/api/playbook.py:167 +msgid "This file can not be rename" +msgstr "ファイル名を変更することはできません" + +#: ops/api/playbook.py:186 +msgid "File already exists" +msgstr "ファイルは既に存在します。" + +#: ops/api/playbook.py:204 +msgid "File key is required" +msgstr "ファイルキーこのフィールドは必須です" + +#: ops/api/playbook.py:207 +msgid "This file can not be delete" +msgstr "このファイルを削除できません" + #: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:55 msgid "App ops" msgstr "アプリ操作" @@ -3901,31 +3973,39 @@ msgstr "特権アカウントのみ" msgid "Privileged First" msgstr "特権アカウント優先" -#: ops/const.py:50 +#: ops/const.py:50 ops/const.py:60 msgid "Powershell" msgstr "PowerShell" -#: ops/const.py:51 +#: ops/const.py:51 ops/const.py:61 msgid "Python" msgstr "Python" -#: ops/const.py:52 +#: ops/const.py:52 ops/const.py:62 msgid "MySQL" msgstr "MySQL" -#: ops/const.py:53 +#: ops/const.py:53 ops/const.py:64 msgid "PostgreSQL" msgstr "PostgreSQL" -#: ops/const.py:54 +#: ops/const.py:54 ops/const.py:65 msgid "SQLServer" msgstr "SQLServer" -#: ops/const.py:55 +#: ops/const.py:55 ops/const.py:67 msgid "Raw" msgstr "" -#: ops/const.py:61 +#: ops/const.py:63 +msgid "MariaDB" +msgstr "MariaDB" + +#: ops/const.py:66 +msgid "Oracle" +msgstr "Oracle" + +#: ops/const.py:73 msgid "Timeout" msgstr "タイムアウト" @@ -3933,28 +4013,28 @@ msgstr "タイムアウト" msgid "no valid program entry found." msgstr "利用可能なプログラムポータルがありません" -#: ops/mixin.py:26 ops/mixin.py:89 settings/serializers/auth/ldap.py:73 +#: ops/mixin.py:26 ops/mixin.py:90 settings/serializers/auth/ldap.py:73 msgid "Cycle perform" msgstr "サイクル実行" -#: ops/mixin.py:30 ops/mixin.py:87 ops/mixin.py:106 +#: ops/mixin.py:30 ops/mixin.py:88 ops/mixin.py:107 #: settings/serializers/auth/ldap.py:70 msgid "Regularly perform" msgstr "定期的に実行する" -#: ops/mixin.py:109 +#: ops/mixin.py:110 msgid "Interval" msgstr "間隔" -#: ops/mixin.py:119 +#: ops/mixin.py:120 msgid "* Please enter a valid crontab expression" msgstr "* 有効なcrontab式を入力してください" -#: ops/mixin.py:126 +#: ops/mixin.py:127 msgid "Range {} to {}" msgstr "{} から {} までの範囲" -#: ops/mixin.py:137 +#: ops/mixin.py:138 msgid "Require periodic or regularly perform setting" msgstr "定期的または定期的に設定を行う必要があります" @@ -4023,7 +4103,7 @@ msgstr "終了" msgid "Date published" msgstr "発売日" -#: ops/models/celery.py:86 +#: ops/models/celery.py:87 msgid "Celery Task Execution" msgstr "Celery タスク実行" @@ -4111,7 +4191,7 @@ msgstr "保存後に実行" msgid "Job type" msgstr "タスクの種類" -#: ops/serializers/job.py:57 terminal/serializers/session.py:50 +#: ops/serializers/job.py:57 terminal/serializers/session.py:53 msgid "Is finished" msgstr "終了しました" @@ -4123,23 +4203,23 @@ msgstr "時を過ごす" msgid "Run ansible task" msgstr "Ansible タスクを実行する" -#: ops/tasks.py:63 +#: ops/tasks.py:68 msgid "Run ansible task execution" msgstr "Ansible タスクの実行を開始する" -#: ops/tasks.py:85 +#: ops/tasks.py:90 msgid "Clear celery periodic tasks" msgstr "タスクログを定期的にクリアする" -#: ops/tasks.py:106 +#: ops/tasks.py:111 msgid "Create or update periodic tasks" msgstr "定期的なタスクの作成または更新" -#: ops/tasks.py:114 +#: ops/tasks.py:119 msgid "Periodic check service performance" msgstr "サービスのパフォーマンスを定期的に確認する" -#: ops/tasks.py:120 +#: ops/tasks.py:125 msgid "Clean up unexpected jobs" msgstr "例外ジョブのクリーンアップ" @@ -4202,7 +4282,7 @@ msgstr "" msgid "The organization have resource ({}) cannot be deleted" msgstr "組織のリソース ({}) は削除できません" -#: orgs/apps.py:7 rbac/tree.py:123 +#: orgs/apps.py:7 rbac/tree.py:125 msgid "App organizations" msgstr "アプリ組織" @@ -4425,7 +4505,7 @@ msgid "Scope" msgstr "スコープ" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:808 +#: users/models/user.py:802 msgid "Role" msgstr "ロール" @@ -4529,30 +4609,30 @@ msgid "My assets" msgstr "私の資産" #: rbac/tree.py:56 terminal/models/applet/applet.py:52 -#: terminal/models/applet/applet.py:298 terminal/models/applet/host.py:29 +#: terminal/models/applet/applet.py:300 terminal/models/applet/host.py:29 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "リモートアプリケーション" -#: rbac/tree.py:124 +#: rbac/tree.py:126 msgid "Ticket comment" msgstr "チケットコメント" -#: rbac/tree.py:125 settings/serializers/feature.py:58 +#: rbac/tree.py:127 settings/serializers/feature.py:58 #: tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "チケット" -#: rbac/tree.py:126 +#: rbac/tree.py:128 msgid "Common setting" msgstr "共通設定" -#: rbac/tree.py:127 +#: rbac/tree.py:129 msgid "View permission tree" msgstr "権限ツリーの表示" #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 -#: settings/api/sms.py:153 settings/api/vault.py:40 settings/api/wecom.py:37 +#: settings/api/sms.py:160 settings/api/vault.py:40 settings/api/wecom.py:37 msgid "Test success" msgstr "テストの成功" @@ -4580,11 +4660,11 @@ msgstr "Ldapユーザーを取得するにはNone" msgid "Imported {} users successfully (Organization: {})" msgstr "{} 人のユーザーを正常にインポートしました (組織: {})" -#: settings/api/sms.py:135 +#: settings/api/sms.py:142 msgid "Invalid SMS platform" msgstr "無効なショートメッセージプラットフォーム" -#: settings/api/sms.py:141 +#: settings/api/sms.py:148 msgid "test_phone is required" msgstr "携帯番号をテストこのフィールドは必須です" @@ -4605,38 +4685,50 @@ msgid "Can change auth setting" msgstr "資格認定の設定" #: settings/models.py:162 +msgid "Can change auth ops" +msgstr "タスクセンターの設定" + +#: settings/models.py:163 +msgid "Can change auth ticket" +msgstr "製造オーダ設定" + +#: settings/models.py:164 +msgid "Can change auth announcement" +msgstr "公告の設定" + +#: settings/models.py:165 msgid "Can change vault setting" msgstr "金庫の設定を変えることができます" -#: settings/models.py:163 +#: settings/models.py:166 msgid "Can change system msg sub setting" msgstr "システムmsgサブ设定を変更できます" -#: settings/models.py:164 +#: settings/models.py:167 msgid "Can change sms setting" msgstr "Smsの設定を変えることができます" -#: settings/models.py:165 +#: settings/models.py:168 msgid "Can change security setting" msgstr "セキュリティ設定を変更できます" -#: settings/models.py:166 +#: settings/models.py:169 msgid "Can change clean setting" msgstr "きれいな設定を変えることができます" -#: settings/models.py:167 +#: settings/models.py:170 msgid "Can change interface setting" msgstr "インターフェイスの設定を変えることができます" -#: settings/models.py:168 +#: settings/models.py:171 msgid "Can change license setting" msgstr "ライセンス設定を変更できます" -#: settings/models.py:169 +#: settings/models.py:172 msgid "Can change terminal setting" msgstr "ターミナルの設定を変えることができます" -#: settings/models.py:170 +#: settings/models.py:173 msgid "Can change other setting" msgstr "他の設定を変えることができます" @@ -4990,54 +5082,58 @@ msgstr "SP プライベートキー" msgid "SP cert" msgstr "SP 証明書" -#: settings/serializers/auth/sms.py:16 +#: settings/serializers/auth/sms.py:17 msgid "Enable SMS" msgstr "SMSの有効化" -#: settings/serializers/auth/sms.py:18 +#: settings/serializers/auth/sms.py:19 msgid "SMS provider / Protocol" msgstr "SMSプロバイダ / プロトコル" -#: settings/serializers/auth/sms.py:23 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/msg.py:76 +#: settings/serializers/auth/sms.py:22 +msgid "SMS code length" +msgstr "認証コード長" + +#: settings/serializers/auth/sms.py:27 settings/serializers/auth/sms.py:49 +#: settings/serializers/auth/sms.py:57 settings/serializers/auth/sms.py:66 +#: settings/serializers/auth/sms.py:77 settings/serializers/msg.py:76 msgid "Signature" msgstr "署名" -#: settings/serializers/auth/sms.py:24 settings/serializers/auth/sms.py:46 -#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 +#: settings/serializers/auth/sms.py:28 settings/serializers/auth/sms.py:50 +#: settings/serializers/auth/sms.py:58 settings/serializers/auth/sms.py:67 msgid "Template code" msgstr "テンプレートコード" -#: settings/serializers/auth/sms.py:31 +#: settings/serializers/auth/sms.py:35 msgid "Test phone" msgstr "テスト電話" -#: settings/serializers/auth/sms.py:60 +#: settings/serializers/auth/sms.py:64 msgid "App Access Address" msgstr "アプリケーションアドレス" -#: settings/serializers/auth/sms.py:61 +#: settings/serializers/auth/sms.py:65 msgid "Signature channel number" msgstr "署名チャネル番号" -#: settings/serializers/auth/sms.py:69 +#: settings/serializers/auth/sms.py:73 msgid "Enterprise code(SP id)" msgstr "企業コード(SP id)" -#: settings/serializers/auth/sms.py:70 +#: settings/serializers/auth/sms.py:74 msgid "Shared secret(Shared secret)" msgstr "パスワードを共有する(Shared secret)" -#: settings/serializers/auth/sms.py:71 +#: settings/serializers/auth/sms.py:75 msgid "Original number(Src id)" msgstr "元の番号(Src id)" -#: settings/serializers/auth/sms.py:72 +#: settings/serializers/auth/sms.py:76 msgid "Business type(Service id)" msgstr "ビジネス・タイプ(Service id)" -#: settings/serializers/auth/sms.py:76 +#: settings/serializers/auth/sms.py:80 #, python-brace-format msgid "" "Template need contain {code} and Signature + template length does not exceed " @@ -5048,24 +5144,24 @@ msgstr "" "満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない" "でください。" -#: settings/serializers/auth/sms.py:85 +#: settings/serializers/auth/sms.py:89 #, python-brace-format msgid "The template needs to contain {code}" msgstr "テンプレートには{code}を含める必要があります" -#: settings/serializers/auth/sms.py:88 +#: settings/serializers/auth/sms.py:92 msgid "Signature + Template must not exceed 65 words" msgstr "署名+テンプレートの長さは65文字以内" -#: settings/serializers/auth/sms.py:97 +#: settings/serializers/auth/sms.py:101 msgid "URL" msgstr "URL" -#: settings/serializers/auth/sms.py:102 +#: settings/serializers/auth/sms.py:106 msgid "Request method" msgstr "請求方法です" -#: settings/serializers/auth/sms.py:111 +#: settings/serializers/auth/sms.py:117 #, python-format msgid "The value in the parameter must contain %s" msgstr "パラメータの値には必ず %s が含まれます" @@ -5136,35 +5232,39 @@ msgstr "サポートリンク" msgid "default: http://www.jumpserver.org/support/" msgstr "デフォルト: http://www.jumpserver.org/support/" -#: settings/serializers/cleaning.py:8 +#: settings/serializers/cleaning.py:11 msgid "Period clean" msgstr "定時清掃" -#: settings/serializers/cleaning.py:12 +#: settings/serializers/cleaning.py:15 msgid "Login log keep days (day)" msgstr "ログインログは日数を保持します(天)" -#: settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:19 msgid "Task log keep days (day)" msgstr "タスクログは日数を保持します(天)" -#: settings/serializers/cleaning.py:20 +#: settings/serializers/cleaning.py:23 msgid "Operate log keep days (day)" msgstr "ログ管理日を操作する(天)" -#: settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:27 msgid "FTP log keep days (day)" msgstr "ダウンロードのアップロード(天)" -#: settings/serializers/cleaning.py:28 +#: settings/serializers/cleaning.py:31 msgid "Cloud sync record keep days (day)" msgstr "クラウド同期レコードは日数を保持します(天)" -#: settings/serializers/cleaning.py:31 +#: settings/serializers/cleaning.py:35 +msgid "Activity log keep days (day)" +msgstr "活動ログは日数を保持します(天)" + +#: settings/serializers/cleaning.py:38 msgid "Session keep duration (day)" msgstr "セッション維持期間(天)" -#: settings/serializers/cleaning.py:33 +#: settings/serializers/cleaning.py:40 msgid "" "Session, record, command will be delete if more than duration, only in " "database, OSS will not be affected." @@ -5172,10 +5272,6 @@ msgstr "" "この期間を超えるセッション、録音、およびコマンド レコードは削除されます (デー" "タベースのバックアップに影響し、OSS などには影響しません)" -#: settings/serializers/cleaning.py:37 -msgid "Activity log keep days (day)" -msgstr "活動ログは日数を保持します(天)" - #: settings/serializers/feature.py:16 msgid "Subject" msgstr "件名" @@ -5705,100 +5801,100 @@ msgstr "LDAP ユーザーを定期的にインポートする" msgid "Registration periodic import ldap user task" msgstr "登録サイクルLDAPユーザータスクのインポート" -#: settings/utils/ldap.py:476 +#: settings/utils/ldap.py:494 msgid "ldap:// or ldaps:// protocol is used." msgstr "ldap:// または ldaps:// プロトコルが使用されます。" -#: settings/utils/ldap.py:487 +#: settings/utils/ldap.py:505 msgid "Host or port is disconnected: {}" msgstr "ホストまたはポートが切断されました: {}" -#: settings/utils/ldap.py:489 +#: settings/utils/ldap.py:507 msgid "The port is not the port of the LDAP service: {}" msgstr "ポートはLDAPサービスのポートではありません: {}" -#: settings/utils/ldap.py:491 +#: settings/utils/ldap.py:509 msgid "Please add certificate: {}" msgstr "証明書を追加してください: {}" -#: settings/utils/ldap.py:495 settings/utils/ldap.py:522 -#: settings/utils/ldap.py:552 settings/utils/ldap.py:580 +#: settings/utils/ldap.py:513 settings/utils/ldap.py:540 +#: settings/utils/ldap.py:570 settings/utils/ldap.py:598 msgid "Unknown error: {}" msgstr "不明なエラー: {}" -#: settings/utils/ldap.py:509 +#: settings/utils/ldap.py:527 msgid "Bind DN or Password incorrect" msgstr "DNまたはパスワードのバインドが正しくありません" -#: settings/utils/ldap.py:516 +#: settings/utils/ldap.py:534 msgid "Please enter Bind DN: {}" msgstr "バインドDN: {} を入力してください" -#: settings/utils/ldap.py:518 +#: settings/utils/ldap.py:536 msgid "Please enter Password: {}" msgstr "パスワードを入力してください: {}" -#: settings/utils/ldap.py:520 +#: settings/utils/ldap.py:538 msgid "Please enter correct Bind DN and Password: {}" msgstr "正しいバインドDNとパスワードを入力してください: {}" -#: settings/utils/ldap.py:538 +#: settings/utils/ldap.py:556 msgid "Invalid User OU or User search filter: {}" msgstr "無効なユーザー OU またはユーザー検索フィルター: {}" -#: settings/utils/ldap.py:569 +#: settings/utils/ldap.py:587 msgid "LDAP User attr map not include: {}" msgstr "LDAP ユーザーattrマップは含まれません: {}" -#: settings/utils/ldap.py:576 +#: settings/utils/ldap.py:594 msgid "LDAP User attr map is not dict" msgstr "LDAPユーザーattrマップはdictではありません" -#: settings/utils/ldap.py:595 +#: settings/utils/ldap.py:613 msgid "LDAP authentication is not enabled" msgstr "LDAP 認証が有効になっていない" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:631 msgid "Error (Invalid LDAP server): {}" msgstr "エラー (LDAPサーバーが無効): {}" -#: settings/utils/ldap.py:615 +#: settings/utils/ldap.py:633 msgid "Error (Invalid Bind DN): {}" msgstr "エラー (DNのバインドが無効): {}" -#: settings/utils/ldap.py:617 +#: settings/utils/ldap.py:635 msgid "Error (Invalid LDAP User attr map): {}" msgstr "エラー (LDAPユーザーattrマップが無効): {}" -#: settings/utils/ldap.py:619 +#: settings/utils/ldap.py:637 msgid "Error (Invalid User OU or User search filter): {}" msgstr "エラー (ユーザーOUまたはユーザー検索フィルターが無効): {}" -#: settings/utils/ldap.py:621 +#: settings/utils/ldap.py:639 msgid "Error (Not enabled LDAP authentication): {}" msgstr "エラー (LDAP認証が有効化されていません): {}" -#: settings/utils/ldap.py:623 +#: settings/utils/ldap.py:641 msgid "Error (Unknown): {}" msgstr "エラー (不明): {}" -#: settings/utils/ldap.py:626 +#: settings/utils/ldap.py:644 msgid "Succeed: Match {} s user" msgstr "成功: {} 人のユーザーに一致" -#: settings/utils/ldap.py:659 +#: settings/utils/ldap.py:677 msgid "Authentication failed (configuration incorrect): {}" msgstr "認証に失敗しました (設定が正しくありません): {}" -#: settings/utils/ldap.py:663 +#: settings/utils/ldap.py:681 msgid "Authentication failed (username or password incorrect): {}" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}" -#: settings/utils/ldap.py:665 +#: settings/utils/ldap.py:683 msgid "Authentication failed (Unknown): {}" msgstr "認証に失敗しました (不明): {}" -#: settings/utils/ldap.py:668 +#: settings/utils/ldap.py:686 msgid "Authentication success: {}" msgstr "認証成功: {}" @@ -6036,7 +6132,7 @@ msgstr "コマンドストア" msgid "Invalid" msgstr "無効" -#: terminal/api/component/storage.py:119 terminal/tasks.py:141 +#: terminal/api/component/storage.py:119 terminal/tasks.py:142 msgid "Test failure: {}" msgstr "テスト失敗: {}" @@ -6045,7 +6141,7 @@ msgid "Test successful" msgstr "テスト成功" #: terminal/api/component/storage.py:124 terminal/notifications.py:240 -#: terminal/tasks.py:145 +#: terminal/tasks.py:146 msgid "Test failure: Account invalid" msgstr "テスト失敗: アカウントが無効" @@ -6058,15 +6154,15 @@ msgstr "オンラインセッションを持つ" msgid "User %s %s session %s replay" msgstr "ユーザー%s %sこのセッション %s の録画です" -#: terminal/api/session/session.py:292 +#: terminal/api/session/session.py:297 msgid "Session does not exist: {}" msgstr "セッションが存在しません: {}" -#: terminal/api/session/session.py:295 +#: terminal/api/session/session.py:300 msgid "Session is finished or the protocol not supported" msgstr "セッションが終了したか、プロトコルがサポートされていません" -#: terminal/api/session/session.py:308 +#: terminal/api/session/session.py:313 msgid "User does not have permission" msgstr "ユーザーに権限がありません" @@ -6157,7 +6253,7 @@ msgid "SFTP" msgstr "SFTP" #: terminal/const.py:89 -msgid "Read Only" +msgid "Read only" msgstr "読み取り専用" #: terminal/const.py:90 @@ -6165,17 +6261,29 @@ msgid "Writable" msgstr "書き込み可能" #: terminal/const.py:94 -msgid "Kill Session" +msgid "Kill session" msgstr "セッションを終了する" #: terminal/const.py:95 -msgid "Lock Session" +msgid "Lock session" msgstr "セッションをロックする" #: terminal/const.py:96 -msgid "Unlock Session" +msgid "Unlock session" msgstr "セッションのロックを解除する" +#: terminal/const.py:101 +msgid "Replay create failed" +msgstr "ビデオの作成に失敗しました" + +#: terminal/const.py:102 +msgid "Replay upload failed" +msgstr "動画のアップロードに失敗しました" + +#: terminal/const.py:103 +msgid "Replay convert failed" +msgstr "ビデオのトランスコーディングに失敗しました" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "一括作成非サポート" @@ -6228,7 +6336,7 @@ msgstr "カスタムプラットフォームのみをサポート" msgid "Missing type in platform.yml" msgstr "platform.ymlにタイプがありません" -#: terminal/models/applet/applet.py:300 terminal/models/applet/host.py:35 +#: terminal/models/applet/applet.py:302 terminal/models/applet/host.py:35 #: terminal/models/applet/host.py:137 msgid "Hosting" msgstr "ホスト マシン" @@ -6399,27 +6507,31 @@ msgstr "リプレイ" msgid "Date end" msgstr "終了日" -#: terminal/models/session/session.py:47 terminal/serializers/session.py:62 +#: terminal/models/session/session.py:47 terminal/serializers/session.py:65 msgid "Command amount" msgstr "コマンド量" -#: terminal/models/session/session.py:281 +#: terminal/models/session/session.py:48 terminal/serializers/session.py:28 +msgid "Error reason" +msgstr "間違った理由" + +#: terminal/models/session/session.py:282 msgid "Session record" msgstr "セッション記録" -#: terminal/models/session/session.py:283 +#: terminal/models/session/session.py:284 msgid "Can monitor session" msgstr "セッションを監視できます" -#: terminal/models/session/session.py:284 +#: terminal/models/session/session.py:285 msgid "Can share session" msgstr "セッションを共有できます" -#: terminal/models/session/session.py:285 +#: terminal/models/session/session.py:286 msgid "Can terminate session" msgstr "セッションを終了できます" -#: terminal/models/session/session.py:286 +#: terminal/models/session/session.py:287 msgid "Can validate session action perm" msgstr "セッションアクションのパーマを検証できます" @@ -6674,31 +6786,31 @@ msgstr "" msgid "Asset IP" msgstr "資産 IP" -#: terminal/serializers/session.py:23 terminal/serializers/session.py:47 +#: terminal/serializers/session.py:23 terminal/serializers/session.py:50 msgid "Can replay" msgstr "再生できます" -#: terminal/serializers/session.py:24 terminal/serializers/session.py:48 +#: terminal/serializers/session.py:24 terminal/serializers/session.py:51 msgid "Can join" msgstr "参加できます" -#: terminal/serializers/session.py:25 terminal/serializers/session.py:51 +#: terminal/serializers/session.py:25 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "終了できます" -#: terminal/serializers/session.py:43 +#: terminal/serializers/session.py:46 msgid "User ID" msgstr "ユーザーID" -#: terminal/serializers/session.py:44 +#: terminal/serializers/session.py:47 msgid "Asset ID" msgstr "資産ID" -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:48 msgid "Login from display" msgstr "表示からのログイン" -#: terminal/serializers/session.py:52 +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "ターミナルディスプレイ" @@ -6784,23 +6896,23 @@ msgstr "端末の状態を定期的にクリーンアップする" msgid "Clean orphan session" msgstr "オフライン セッションをクリアする" -#: terminal/tasks.py:61 +#: terminal/tasks.py:62 msgid "Upload session replay to external storage" msgstr "セッションの記録を外部ストレージにアップロードする" -#: terminal/tasks.py:90 +#: terminal/tasks.py:91 msgid "Run applet host deployment" msgstr "アプリケーション マシンの展開を実行する" -#: terminal/tasks.py:100 +#: terminal/tasks.py:101 msgid "Install applet" msgstr "アプリをインストールする" -#: terminal/tasks.py:111 +#: terminal/tasks.py:112 msgid "Generate applet host accounts" msgstr "リモートアプリケーション上のアカウントを収集する" -#: terminal/tasks.py:123 +#: terminal/tasks.py:124 msgid "Check command replay storage connectivity" msgstr "チェックコマンドと録画ストレージの接続性" @@ -6830,6 +6942,10 @@ msgstr "" msgid "All available port count: {}, Already use port count: {}" msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" +#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:288 +msgid "Applicant" +msgstr "応募者" + #: tickets/apps.py:7 msgid "Tickets" msgstr "チケット" @@ -6858,10 +6974,6 @@ msgstr "拒否" msgid "Closed" msgstr "クローズ" -#: tickets/const.py:46 -msgid "Approve" -msgstr "承認" - #: tickets/const.py:50 msgid "One level" msgstr "1つのレベル" @@ -6993,6 +7105,10 @@ msgstr "実行コマンド" msgid "Command filter acl" msgstr "コマンド フィルタ" +#: tickets/models/ticket/command_confirm.py:23 +msgid "Apply Command Ticket" +msgstr "製造オーダの検討を命令" + #: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "チケットステップ" @@ -7005,10 +7121,6 @@ msgstr "割り当てられたチケット" msgid "Title" msgstr "タイトル" -#: tickets/models/ticket/general.py:288 -msgid "Applicant" -msgstr "応募者" - #: tickets/models/ticket/general.py:292 msgid "TicketFlow" msgstr "作業指示プロセス" @@ -7041,10 +7153,18 @@ msgstr "ログイン資産" msgid "Login account" msgstr "ログインアカウント" +#: tickets/models/ticket/login_asset_confirm.py:27 +msgid "Apply Login Asset Ticket" +msgstr "資産ログインレビュー製造オーダ" + #: tickets/models/ticket/login_confirm.py:12 msgid "Login datetime" msgstr "ログイン日時" +#: tickets/models/ticket/login_confirm.py:15 +msgid "Apply Login Ticket" +msgstr "ユーザーログインレビュー製造オーダ" + #: tickets/notifications.py:63 msgid "Ticket basic info" msgstr "チケット基本情報" @@ -7131,18 +7251,14 @@ msgid "Ticket information" msgstr "作業指示情報" #: tickets/templates/tickets/approve_check_password.html:29 -#: tickets/views/approve.py:40 +#: tickets/views/approve.py:40 tickets/views/approve.py:77 msgid "Ticket approval" msgstr "作業指示の承認" -#: tickets/templates/tickets/approve_check_password.html:45 +#: tickets/templates/tickets/approve_check_password.html:44 msgid "Approval" msgstr "承認" -#: tickets/templates/tickets/approve_check_password.html:54 -msgid "Go Login" -msgstr "ログイン" - #: tickets/views/approve.py:41 msgid "" "This ticket does not exist, the process has ended, or this link has expired" @@ -7150,19 +7266,19 @@ msgstr "" "このワークシートが存在しないか、ワークシートが終了したか、このリンクが無効に" "なっています" -#: tickets/views/approve.py:70 +#: tickets/views/approve.py:69 msgid "Click the button below to approve or reject" msgstr "下のボタンをクリックして同意または拒否。" -#: tickets/views/approve.py:72 +#: tickets/views/approve.py:78 msgid "After successful authentication, this ticket can be approved directly" msgstr "認証に成功した後、作業指示書は直接承認することができる。" -#: tickets/views/approve.py:97 +#: tickets/views/approve.py:95 msgid "Illegal approval action" msgstr "無効な承認アクション" -#: tickets/views/approve.py:110 +#: tickets/views/approve.py:108 msgid "This user is not authorized to approve this ticket" msgstr "このユーザーはこの作業指示を承認する権限がありません" @@ -7305,7 +7421,7 @@ msgstr "公開鍵は古いものと同じであってはなりません。" msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:173 users/models/user.py:831 +#: users/forms/profile.py:173 users/models/user.py:825 #: xpack/plugins/cloud/serializers/account_attrs.py:203 msgid "Public key" msgstr "公開キー" @@ -7314,73 +7430,77 @@ msgstr "公開キー" msgid "Preference" msgstr "ユーザー設定" -#: users/models/user.py:648 users/serializers/profile.py:94 +#: users/models/user.py:642 users/serializers/profile.py:94 msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:810 users/serializers/user.py:172 +#: users/models/user.py:804 users/serializers/user.py:169 msgid "Is service account" msgstr "サービスアカウントです" -#: users/models/user.py:812 +#: users/models/user.py:806 msgid "Avatar" msgstr "アバター" -#: users/models/user.py:815 +#: users/models/user.py:809 msgid "Wechat" msgstr "微信" -#: users/models/user.py:818 users/serializers/user.py:109 +#: users/models/user.py:812 users/serializers/user.py:106 msgid "Phone" msgstr "電話" -#: users/models/user.py:824 +#: users/models/user.py:818 msgid "OTP secret key" msgstr "OTP 秘密" -#: users/models/user.py:828 +#: users/models/user.py:822 #: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "Private key" msgstr "ssh秘密鍵" -#: users/models/user.py:836 users/serializers/profile.py:125 -#: users/serializers/user.py:169 +#: users/models/user.py:830 users/serializers/profile.py:125 +#: users/serializers/user.py:166 msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:850 +#: users/models/user.py:840 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:853 +#: users/models/user.py:843 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:977 +#: users/models/user.py:845 +msgid "Date api key used" +msgstr "Api key 最後に使用した日付" + +#: users/models/user.py:968 msgid "Can not delete admin user" msgstr "管理者ユーザーを削除できませんでした" -#: users/models/user.py:1003 +#: users/models/user.py:994 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:1004 +#: users/models/user.py:995 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:1005 +#: users/models/user.py:996 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:1014 +#: users/models/user.py:1005 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:1017 +#: users/models/user.py:1008 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:1042 +#: users/models/user.py:1033 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -7391,7 +7511,7 @@ msgstr "ユーザーパスワード履歴" msgid "Reset password" msgstr "パスワードのリセット" -#: users/notifications.py:85 users/views/profile/reset.py:194 +#: users/notifications.py:85 users/views/profile/reset.py:230 msgid "Reset password success" msgstr "パスワードのリセット成功" @@ -7419,15 +7539,19 @@ msgstr "MFAのリセット" msgid "File name conflict resolution" msgstr "ファイル名競合ソリューション" -#: users/serializers/preference/lina.py:11 +#: users/serializers/preference/koko.py:14 +msgid "Terminal theme name" +msgstr "ターミナルテーマ名" + +#: users/serializers/preference/lina.py:13 msgid "New file encryption password" msgstr "新しいファイルの暗号化パスワード" -#: users/serializers/preference/lina.py:16 +#: users/serializers/preference/lina.py:18 msgid "Confirm file encryption password" msgstr "ファイルの暗号化パスワードを確認する" -#: users/serializers/preference/lina.py:24 users/serializers/profile.py:48 +#: users/serializers/preference/lina.py:31 users/serializers/profile.py:48 msgid "The newly set password is inconsistent" msgstr "新しく設定されたパスワードが一致しない" @@ -7507,7 +7631,7 @@ msgstr "MFAフォース有効化" msgid "Login blocked" msgstr "ログインがロックされました" -#: users/serializers/user.py:99 users/serializers/user.py:178 +#: users/serializers/user.py:99 users/serializers/user.py:175 msgid "Is OTP bound" msgstr "仮想MFAがバインドされているか" @@ -7515,27 +7639,27 @@ msgstr "仮想MFAがバインドされているか" msgid "Can public key authentication" msgstr "公開鍵認証が可能" -#: users/serializers/user.py:173 +#: users/serializers/user.py:170 msgid "Is org admin" msgstr "組織管理者です" -#: users/serializers/user.py:175 +#: users/serializers/user.py:172 msgid "Avatar url" msgstr "アバターURL" -#: users/serializers/user.py:179 +#: users/serializers/user.py:176 msgid "MFA level" msgstr "MFA レベル" -#: users/serializers/user.py:285 +#: users/serializers/user.py:282 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:286 +#: users/serializers/user.py:283 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:319 +#: users/serializers/user.py:316 msgid "name not unique" msgstr "名前が一意ではない" @@ -7548,26 +7672,30 @@ msgstr "" "管理者は「既存のユーザーのみログインを許可」をオンにしており、現在のユーザー" "はユーザーリストにありません。管理者に連絡してください。" -#: users/tasks.py:23 +#: users/tasks.py:25 msgid "Check password expired" msgstr "パスワードの有効期限が切れていることを確認する" -#: users/tasks.py:37 +#: users/tasks.py:39 msgid "Periodic check password expired" msgstr "定期認証パスワードの有効期限" -#: users/tasks.py:51 +#: users/tasks.py:53 msgid "Check user expired" msgstr "ユーザーの有効期限が切れていることを確認する" -#: users/tasks.py:68 +#: users/tasks.py:70 msgid "Periodic check user expired" msgstr "ユーザーの有効期限の定期的な検出" -#: users/tasks.py:82 +#: users/tasks.py:84 msgid "Check unused users" msgstr "未使用のユーザーを確認する" +#: users/tasks.py:114 +msgid "The user has not logged in recently and has been disabled." +msgstr "ユーザーは最近ログインしておらず、無効になっています。" + #: users/templates/users/_msg_account_expire_reminder.html:7 msgid "Your account will expire in" msgstr "アカウントの有効期限は" @@ -7798,7 +7926,7 @@ msgstr "OTP無効化成功、ログインページを返す" msgid "Password invalid" msgstr "パスワード無効" -#: users/views/profile/reset.py:47 +#: users/views/profile/reset.py:64 msgid "" "Non-local users can log in only from third-party platforms and cannot change " "their passwords: {}" @@ -7806,23 +7934,23 @@ msgstr "" "ローカル以外のユーザーは、サードパーティ プラットフォームからのログインのみが" "許可され、パスワードの変更はサポートされていません: {}" -#: users/views/profile/reset.py:149 users/views/profile/reset.py:160 +#: users/views/profile/reset.py:185 users/views/profile/reset.py:196 msgid "Token invalid or expired" msgstr "トークンが無効または期限切れ" -#: users/views/profile/reset.py:165 +#: users/views/profile/reset.py:201 msgid "User auth from {}, go there change password" msgstr "ユーザー認証ソース {}, 対応するシステムにパスワードを変更してください" -#: users/views/profile/reset.py:172 +#: users/views/profile/reset.py:208 msgid "* Your password does not meet the requirements" msgstr "* パスワードが要件を満たしていない" -#: users/views/profile/reset.py:178 +#: users/views/profile/reset.py:214 msgid "* The new password cannot be the last {} passwords" msgstr "* 新しいパスワードを最後の {} パスワードにすることはできません" -#: users/views/profile/reset.py:195 +#: users/views/profile/reset.py:231 msgid "Reset password success, return to login page" msgstr "パスワードの成功をリセットし、ログインページに戻る" @@ -7950,7 +8078,7 @@ msgstr "同期済み" msgid "Released" msgstr "リリース済み" -#: xpack/plugins/cloud/manager.py:53 +#: xpack/plugins/cloud/manager.py:54 msgid "Account unavailable" msgstr "利用できないアカウント" @@ -8043,16 +8171,16 @@ msgid "Task strategy" msgstr "ミッション戦略です" #: xpack/plugins/cloud/models.py:285 -msgid "Exact" -msgstr "" +msgid "Equal" +msgstr "等しい" #: xpack/plugins/cloud/models.py:286 -msgid "Not" -msgstr "否" +msgid "Not Equal" +msgstr "不等于" #: xpack/plugins/cloud/models.py:287 msgid "In" -msgstr "イン" +msgstr "で..." #: xpack/plugins/cloud/models.py:288 msgid "Contains" @@ -8060,11 +8188,11 @@ msgstr "含む" #: xpack/plugins/cloud/models.py:289 msgid "Startswith" -msgstr "始まる" +msgstr "始まる..." #: xpack/plugins/cloud/models.py:290 msgid "Endswith" -msgstr "終わる" +msgstr "終わる..." #: xpack/plugins/cloud/models.py:296 msgid "Instance platform" @@ -8460,7 +8588,7 @@ msgstr "ライセンスのインポートに成功" msgid "License is invalid" msgstr "ライセンスが無効です" -#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:140 +#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:141 msgid "License" msgstr "ライセンス" @@ -8479,9 +8607,3 @@ msgstr "エンタープライズプロフェッショナル版" #: xpack/plugins/license/models.py:86 msgid "Ultimate edition" msgstr "エンタープライズ・フラッグシップ・エディション" - -#~ msgid "Random" -#~ msgstr "ランダム" - -#~ msgid "Enterprise edition" -#~ msgstr "エンタープライズ基本版" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 3f9d48c80..ca81a92bf 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:78fa10c674b853ebde73bbdef255beeb794a7a7b4bf5483ac1464c282aab0819 -size 131200 +oid sha256:9f331dc156c4e51b6a30249da9d342093c79da1cebe926474b7942a5d3b6a931 +size 133436 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index d6b011d9d..bdb3be1d1 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: 2023-09-19 10:39+0800\n" +"POT-Creation-Date: 2023-10-19 16:21+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,17 +17,18 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.4.3\n" -#: accounts/api/automations/base.py:79 tickets/api/ticket.py:112 +#: accounts/api/automations/base.py:79 tickets/api/ticket.py:132 msgid "The parameter 'action' must be [{}]" msgstr "参数 'action' 必须是 [{}]" #: accounts/const/account.py:6 #: accounts/serializers/automations/change_secret.py:32 -#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:35 -#: authentication/confirm/password.py:9 authentication/forms.py:32 -#: authentication/templates/authentication/login.html:286 +#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:34 +#: authentication/confirm/password.py:9 authentication/confirm/password.py:24 +#: authentication/confirm/password.py:26 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:324 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -39,7 +40,7 @@ msgstr "密码" msgid "SSH key" msgstr "SSH 密钥" -#: accounts/const/account.py:8 authentication/models/access_key.py:33 +#: accounts/const/account.py:8 authentication/models/access_key.py:37 msgid "Access key" msgstr "Access key" @@ -70,7 +71,7 @@ msgstr "同名账号" msgid "Anonymous account" msgstr "匿名账号" -#: accounts/const/account.py:25 users/models/user.py:744 +#: accounts/const/account.py:25 users/models/user.py:738 msgid "Local" msgstr "数据库" @@ -78,8 +79,8 @@ msgstr "数据库" msgid "Collected" msgstr "收集" -#: accounts/const/account.py:27 accounts/serializers/account/account.py:27 -#: settings/serializers/auth/sms.py:75 +#: accounts/const/account.py:27 accounts/serializers/account/account.py:28 +#: settings/serializers/auth/sms.py:79 msgid "Template" msgstr "模板" @@ -87,15 +88,15 @@ msgstr "模板" msgid "Skip" msgstr "跳过" -#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:234 +#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:236 #: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" #: accounts/const/account.py:33 -#: accounts/serializers/automations/change_secret.py:155 audits/const.py:55 +#: accounts/serializers/automations/change_secret.py:155 audits/const.py:62 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:62 terminal/const.py:77 xpack/plugins/cloud/const.py:43 +#: ops/const.py:74 terminal/const.py:77 xpack/plugins/cloud/const.py:43 msgid "Failed" msgstr "失败" @@ -216,15 +217,15 @@ msgstr "用户 %s 查看/导出 了密码" #: accounts/models/account.py:48 #: accounts/models/automations/gather_account.py:16 -#: accounts/serializers/account/account.py:205 -#: accounts/serializers/account/account.py:250 +#: accounts/serializers/account/account.py:210 +#: accounts/serializers/account/account.py:255 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:131 #: acls/serializers/base.py:123 assets/models/asset/common.py:93 #: assets/models/asset/common.py:334 assets/models/cmd_filter.py:36 #: assets/serializers/domain.py:19 assets/serializers/label.py:27 -#: audits/models.py:54 authentication/models/connection_token.py:36 +#: audits/models.py:57 authentication/models/connection_token.py:36 #: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 #: terminal/backends/command/models.py:17 terminal/models/session/session.py:31 #: terminal/notifications.py:155 terminal/serializers/command.py:17 @@ -236,21 +237,21 @@ msgid "Asset" msgstr "资产" #: accounts/models/account.py:52 accounts/models/template.py:15 -#: accounts/serializers/account/account.py:212 -#: accounts/serializers/account/account.py:260 -#: accounts/serializers/account/template.py:25 +#: accounts/serializers/account/account.py:217 +#: accounts/serializers/account/account.py:265 +#: accounts/serializers/account/template.py:24 #: authentication/serializers/connect_token_secret.py:49 msgid "Su from" msgstr "切换自" -#: accounts/models/account.py:54 assets/const/protocol.py:168 +#: accounts/models/account.py:54 assets/const/protocol.py:169 #: settings/serializers/auth/cas.py:20 settings/serializers/auth/feishu.py:20 #: terminal/models/applet/applet.py:35 msgid "Version" msgstr "版本" -#: accounts/models/account.py:56 accounts/serializers/account/account.py:207 -#: users/models/user.py:846 +#: accounts/models/account.py:56 accounts/serializers/account/account.py:212 +#: users/models/user.py:837 msgid "Source" msgstr "来源" @@ -261,10 +262,12 @@ msgstr "来源 ID" #: accounts/models/account.py:60 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/serializers/base.py:124 assets/serializers/asset/common.py:125 -#: assets/serializers/gateway.py:28 audits/models.py:55 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 -#: terminal/backends/command/models.py:18 terminal/models/session/session.py:33 +#: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7 +#: assets/serializers/asset/common.py:125 assets/serializers/gateway.py:28 +#: audits/models.py:58 authentication/api/connection_token.py:406 +#: ops/models/base.py:18 perms/models/asset_permission.py:70 +#: perms/serializers/permission.py:39 terminal/backends/command/models.py:18 +#: terminal/models/session/session.py:33 #: terminal/templates/terminal/_msg_command_warning.html:8 #: terminal/templates/terminal/_msg_session_sharing.html:8 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:89 @@ -305,7 +308,7 @@ msgid "Account backup plan" msgstr "账号备份计划" #: accounts/models/automations/backup_account.py:91 -#: assets/models/automations/base.py:115 audits/models.py:61 +#: assets/models/automations/base.py:115 audits/models.py:64 #: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:228 #: ops/templates/ops/celery_task_log.html:75 #: perms/models/asset_permission.py:72 terminal/models/applet/host.py:140 @@ -333,7 +336,7 @@ msgstr "账号备份快照" msgid "Trigger mode" msgstr "触发模式" -#: accounts/models/automations/backup_account.py:105 audits/models.py:195 +#: accounts/models/automations/backup_account.py:105 audits/models.py:202 #: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:205 msgid "Reason" msgstr "原因" @@ -341,7 +344,7 @@ msgstr "原因" #: accounts/models/automations/backup_account.py:107 #: accounts/serializers/automations/change_secret.py:110 #: accounts/serializers/automations/change_secret.py:133 -#: ops/serializers/job.py:56 terminal/serializers/session.py:46 +#: ops/serializers/job.py:56 terminal/serializers/session.py:49 msgid "Is success" msgstr "是否成功" @@ -419,10 +422,10 @@ msgid "Date finished" msgstr "结束日期" #: accounts/models/automations/change_secret.py:44 -#: accounts/serializers/account/account.py:252 assets/const/automation.py:8 +#: accounts/serializers/account/account.py:257 assets/const/automation.py:8 #: authentication/templates/authentication/passkey.html:173 -#: authentication/views/base.py:26 authentication/views/base.py:27 -#: authentication/views/base.py:28 common/const/choices.py:20 +#: authentication/views/base.py:27 authentication/views/base.py:28 +#: authentication/views/base.py:29 common/const/choices.py:20 msgid "Error" msgstr "错误" @@ -441,13 +444,14 @@ msgstr "最后登录日期" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:65 #: accounts/serializers/account/virtual.py:21 acls/serializers/base.py:19 -#: acls/serializers/base.py:50 assets/models/_user.py:23 audits/models.py:180 -#: authentication/forms.py:25 authentication/forms.py:27 +#: acls/serializers/base.py:50 acls/templates/acls/asset_login_reminder.html:5 +#: acls/templates/acls/user_login_reminder.html:5 assets/models/_user.py:23 +#: audits/models.py:187 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 #: users/forms/profile.py:32 users/forms/profile.py:115 -#: users/models/user.py:796 users/templates/users/_msg_user_created.html:12 +#: users/models/user.py:790 users/templates/users/_msg_user_created.html:12 #: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "用户名" @@ -475,7 +479,7 @@ msgstr "触发方式" #: accounts/models/automations/push_account.py:16 acls/models/base.py:41 #: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 -#: audits/models.py:88 audits/serializers.py:84 +#: audits/models.py:91 audits/serializers.py:84 #: authentication/serializers/connect_token_secret.py:116 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" @@ -490,7 +494,7 @@ msgid "Verify asset account" msgstr "账号验证" #: accounts/models/base.py:37 accounts/models/base.py:67 -#: accounts/serializers/account/account.py:432 +#: accounts/serializers/account/account.py:437 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:45 #: authentication/serializers/connect_token_secret.py:41 @@ -498,7 +502,7 @@ msgstr "账号验证" msgid "Secret type" msgstr "密文类型" -#: accounts/models/base.py:39 accounts/models/mixins/vault.py:48 +#: accounts/models/base.py:39 accounts/models/mixins/vault.py:49 #: accounts/serializers/account/base.py:19 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 @@ -511,7 +515,7 @@ msgstr "密钥" msgid "Secret strategy" msgstr "密文策略" -#: accounts/models/base.py:44 accounts/serializers/account/template.py:22 +#: accounts/models/base.py:44 accounts/serializers/account/template.py:21 #: accounts/serializers/automations/change_secret.py:44 msgid "Password rules" msgstr "密码规则" @@ -534,10 +538,11 @@ msgstr "密码规则" #: terminal/models/applet/applet.py:33 terminal/models/component/endpoint.py:12 #: terminal/models/component/endpoint.py:94 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:13 -#: terminal/models/component/terminal.py:84 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/preference.py:11 -#: users/models/user.py:798 xpack/plugins/cloud/models.py:32 -#: xpack/plugins/cloud/models.py:273 xpack/plugins/cloud/serializers/task.py:68 +#: terminal/models/component/terminal.py:84 tickets/api/ticket.py:87 +#: users/forms/profile.py:33 users/models/group.py:13 +#: users/models/preference.py:11 users/models/user.py:792 +#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:273 +#: xpack/plugins/cloud/serializers/task.py:68 msgid "Name" msgstr "名称" @@ -550,7 +555,7 @@ msgstr "特权账号" #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 #: terminal/models/applet/applet.py:40 -#: terminal/models/component/endpoint.py:105 users/serializers/user.py:170 +#: terminal/models/component/endpoint.py:105 users/serializers/user.py:167 msgid "Is active" msgstr "激活" @@ -647,15 +652,15 @@ msgstr "" "{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" "密密码" -#: accounts/serializers/account/account.py:30 +#: accounts/serializers/account/account.py:31 msgid "Push now" msgstr "立即推送" -#: accounts/serializers/account/account.py:37 +#: accounts/serializers/account/account.py:38 msgid "Exist policy" msgstr "账号存在策略" -#: accounts/serializers/account/account.py:185 applications/models.py:11 +#: accounts/serializers/account/account.py:190 applications/models.py:11 #: assets/models/label.py:21 assets/models/platform.py:89 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 #: assets/serializers/platform.py:133 assets/serializers/platform.py:229 @@ -664,9 +669,9 @@ msgstr "账号存在策略" msgid "Category" msgstr "类别" -#: accounts/serializers/account/account.py:186 +#: accounts/serializers/account/account.py:191 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 -#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: acls/serializers/command_acl.py:19 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 #: assets/models/cmd_filter.py:74 assets/models/platform.py:90 #: assets/serializers/asset/common.py:122 assets/serializers/platform.py:113 @@ -684,57 +689,59 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:206 msgid "Asset not found" msgstr "资产不存在" -#: accounts/serializers/account/account.py:241 +#: accounts/serializers/account/account.py:246 msgid "Has secret" msgstr "已托管密码" -#: accounts/serializers/account/account.py:251 ops/models/celery.py:60 +#: accounts/serializers/account/account.py:256 ops/models/celery.py:60 #: tickets/models/comment.py:13 tickets/models/ticket/general.py:45 #: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状态" -#: accounts/serializers/account/account.py:253 +#: accounts/serializers/account/account.py:258 msgid "Changed" msgstr "已修改" -#: accounts/serializers/account/account.py:263 +#: accounts/serializers/account/account.py:268 #: accounts/serializers/automations/base.py:22 acls/models/base.py:97 +#: acls/templates/acls/asset_login_reminder.html:6 #: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: assets/serializers/automations/base.py:20 +#: authentication/api/connection_token.py:405 ops/models/base.py:17 #: ops/models/job.py:139 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" msgstr "资产" -#: accounts/serializers/account/account.py:318 +#: accounts/serializers/account/account.py:323 msgid "Account already exists" msgstr "账号已存在" -#: accounts/serializers/account/account.py:368 +#: accounts/serializers/account/account.py:373 #, python-format msgid "Asset does not support this secret type: %s" msgstr "资产不支持账号类型: %s" -#: accounts/serializers/account/account.py:400 +#: accounts/serializers/account/account.py:405 msgid "Account has exist" msgstr "账号已存在" -#: accounts/serializers/account/account.py:433 +#: accounts/serializers/account/account.py:438 #: authentication/serializers/connect_token_secret.py:156 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:31 msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:440 acls/serializers/base.py:116 -#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:50 -#: audits/models.py:86 audits/models.py:164 audits/models.py:262 +#: accounts/serializers/account/account.py:448 acls/serializers/base.py:116 +#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:53 +#: audits/models.py:89 audits/models.py:171 audits/models.py:268 #: audits/serializers.py:171 authentication/models/connection_token.py:32 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 @@ -746,12 +753,12 @@ msgstr "ID" #: terminal/notifications.py:205 terminal/serializers/command.py:16 #: terminal/templates/terminal/_msg_command_warning.html:6 #: terminal/templates/terminal/_msg_session_sharing.html:6 -#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:996 -#: users/models/user.py:1032 users/serializers/group.py:18 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:987 +#: users/models/user.py:1023 users/serializers/group.py:18 msgid "User" msgstr "用户" -#: accounts/serializers/account/account.py:441 +#: accounts/serializers/account/account.py:449 #: authentication/templates/authentication/_access_key_modal.html:33 #: terminal/notifications.py:158 terminal/notifications.py:207 msgid "Date" @@ -759,7 +766,7 @@ msgstr "日期" #: accounts/serializers/account/backup.py:31 #: accounts/serializers/automations/base.py:36 -#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103 +#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:104 #: settings/serializers/auth/ldap.py:66 msgid "Periodic perform" msgstr "定时执行" @@ -783,7 +790,7 @@ msgid "Key password" msgstr "密钥密码" #: accounts/serializers/account/base.py:78 -#: assets/serializers/asset/common.py:311 +#: assets/serializers/asset/common.py:378 msgid "Spec info" msgstr "特殊信息" @@ -795,39 +802,35 @@ msgstr "" "提示: 如果认证时不需要用户名,可填写为 null, 如果是 AD 账号,格式为 " "username@domain" -#: accounts/serializers/account/template.py:11 -#, fuzzy -#| msgid "Password strength" -msgid "Password length" -msgstr "密码强度:" - -#: accounts/serializers/account/template.py:12 -#, fuzzy -#| msgid "Powershell" -msgid "Lowercase" -msgstr "PowerShell" - #: accounts/serializers/account/template.py:13 -msgid "Uppercase" -msgstr "" +msgid "Password length" +msgstr "密码长度" #: accounts/serializers/account/template.py:14 -msgid "Digit" -msgstr "" +msgid "Lowercase" +msgstr "小写字母" #: accounts/serializers/account/template.py:15 -msgid "Special symbol" -msgstr "" +msgid "Uppercase" +msgstr "大写字母" -#: accounts/serializers/account/template.py:36 +#: accounts/serializers/account/template.py:16 +msgid "Digit" +msgstr "数字" + +#: accounts/serializers/account/template.py:17 +msgid "Special symbol" +msgstr "特殊字符" + +#: accounts/serializers/account/template.py:35 msgid "Secret generation strategy for account creation" msgstr "密码生成策略,用于账号创建时,设置密码" -#: accounts/serializers/account/template.py:37 +#: accounts/serializers/account/template.py:36 msgid "Whether to automatically push the account to the asset" msgstr "是否自动推送账号到资产" -#: accounts/serializers/account/template.py:40 +#: accounts/serializers/account/template.py:39 msgid "" "Associated platform, you can configure push parameters. If not associated, " "default parameters will be used" @@ -838,11 +841,11 @@ msgstr "关联平台,可配置推送参数,如果不关联,将使用默认 #: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26 #: ops/models/job.py:145 ops/models/playbook.py:31 rbac/models/role.py:37 #: settings/models.py:37 terminal/models/applet/applet.py:45 -#: terminal/models/applet/applet.py:302 terminal/models/applet/host.py:142 +#: terminal/models/applet/applet.py:304 terminal/models/applet/host.py:142 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:104 #: terminal/models/session/session.py:46 tickets/models/comment.py:32 -#: tickets/models/ticket/general.py:297 users/models/user.py:834 +#: tickets/models/ticket/general.py:297 users/models/user.py:828 #: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:109 msgid "Comment" msgstr "备注" @@ -891,11 +894,11 @@ msgstr "* 密码长度范围 6-30 位" msgid "Automation task execution" msgstr "自动化任务执行历史" -#: accounts/serializers/automations/change_secret.py:154 audits/const.py:54 -#: audits/models.py:60 audits/signal_handlers/activity_log.py:33 -#: common/const/choices.py:18 ops/const.py:60 ops/serializers/celery.py:40 +#: accounts/serializers/automations/change_secret.py:154 audits/const.py:61 +#: audits/models.py:63 audits/signal_handlers/activity_log.py:33 +#: common/const/choices.py:18 ops/const.py:72 ops/serializers/celery.py:40 #: terminal/const.py:76 terminal/models/session/sharing.py:121 -#: tickets/views/approve.py:119 +#: tickets/views/approve.py:117 msgid "Success" msgstr "成功" @@ -930,6 +933,10 @@ msgstr "收集资产上的账号" msgid "Push accounts to assets" msgstr "推送账号到资产" +#: accounts/tasks/template.py:11 +msgid "Template sync info to related accounts" +msgstr "" + #: accounts/tasks/vault.py:31 msgid "Sync secret to vault" msgstr "同步密文到 vault" @@ -966,16 +973,16 @@ msgstr "密钥不合法或密钥密码错误" msgid "Acls" msgstr "访问控制" -#: acls/const.py:6 terminal/const.py:11 tickets/const.py:45 -#: tickets/templates/tickets/approve_check_password.html:49 +#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:45 +#: tickets/templates/tickets/approve_check_password.html:48 msgid "Reject" msgstr "拒绝" -#: acls/const.py:7 terminal/const.py:9 +#: acls/const.py:7 audits/const.py:33 terminal/const.py:9 msgid "Accept" msgstr "接受" -#: acls/const.py:8 +#: acls/const.py:8 audits/const.py:34 msgid "Review" msgstr "审批" @@ -983,6 +990,10 @@ msgstr "审批" msgid "Warning" msgstr "告警" +#: acls/const.py:10 audits/const.py:35 notifications/apps.py:7 +msgid "Notifications" +msgstr "通知" + #: acls/models/base.py:37 assets/models/_user.py:51 #: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:97 #: xpack/plugins/cloud/models.py:275 @@ -1000,7 +1011,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" msgid "Reviewers" msgstr "审批人" -#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: acls/models/base.py:43 authentication/models/access_key.py:20 #: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:29 @@ -1013,7 +1024,7 @@ msgid "Users" msgstr "用户管理" #: acls/models/base.py:98 assets/models/automations/base.py:17 -#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:310 +#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:377 #: rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" @@ -1045,8 +1056,8 @@ msgstr "每行一个命令" msgid "Ignore case" msgstr "忽略大小写" -#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 -#: acls/serializers/command_acl.py:28 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:97 +#: acls/serializers/command_acl.py:29 #: authentication/serializers/connect_token_secret.py:85 #: terminal/templates/terminal/_msg_command_warning.html:14 msgid "Command group" @@ -1056,12 +1067,12 @@ msgstr "命令组" msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" -#: acls/models/command_acl.py:100 +#: acls/models/command_acl.py:103 #: terminal/templates/terminal/_msg_command_warning.html:12 msgid "Command acl" msgstr "命令过滤" -#: acls/models/command_acl.py:109 tickets/const.py:11 +#: acls/models/command_acl.py:112 tickets/const.py:11 msgid "Command confirm" msgstr "命令复核" @@ -1094,6 +1105,14 @@ msgstr "登录资产访问控制" msgid "Login asset confirm" msgstr "登录资产复核" +#: acls/notifications.py:11 +msgid "User login reminder" +msgstr "用户登录提醒" + +#: acls/notifications.py:42 +msgid "Asset login reminder" +msgstr "资产登录提醒" + #: acls/serializers/base.py:11 acls/serializers/login_acl.py:11 msgid "With * indicating a match all. " msgstr "* 表示匹配所有. " @@ -1148,6 +1167,46 @@ msgstr "IP" msgid "Time Period" msgstr "时段" +#: acls/templates/acls/asset_login_reminder.html:3 +#: acls/templates/acls/user_login_reminder.html:3 +msgid "Respectful" +msgstr "尊敬的" + +#: acls/templates/acls/asset_login_reminder.html:10 +msgid "" +"The user has just logged in to the asset. Please ensure that this is an " +"authorized operation. If you suspect that this is an unauthorized access, " +"please take appropriate measures immediately." +msgstr "" +"用户刚刚在登录资产。请确保这是授权的操作。如果您怀疑这是一个未经授权的访问," +"请立即采取适当的措施。" + +#: acls/templates/acls/asset_login_reminder.html:12 +#: acls/templates/acls/user_login_reminder.html:13 +msgid "Thank you" +msgstr "谢谢" + +#: acls/templates/acls/user_login_reminder.html:7 audits/models.py:193 +#: audits/models.py:262 +#: authentication/templates/authentication/_msg_different_city.html:11 +#: tickets/models/ticket/login_confirm.py:11 +msgid "Login city" +msgstr "登录城市" + +#: acls/templates/acls/user_login_reminder.html:8 audits/models.py:196 +#: audits/models.py:263 audits/serializers.py:65 +msgid "User agent" +msgstr "用户代理" + +#: acls/templates/acls/user_login_reminder.html:11 +msgid "" +"The user has just successfully logged into the system. Please ensure that " +"this is an authorized operation. If you suspect that this is an unauthorized " +"access, please take appropriate measures immediately." +msgstr "" +"用户刚刚成功登录到系统。请确保这是授权的操作。如果您怀疑这是一个未经授权的访" +"问,请立即采取适当的措施。" + #: applications/apps.py:9 msgid "Applications" msgstr "应用管理" @@ -1165,7 +1224,7 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" -#: assets/api/asset/asset.py:157 +#: assets/api/asset/asset.py:194 msgid "Cannot create asset directly, you should create a host or other" msgstr "不能直接创建资产, 你应该创建主机或其他资产" @@ -1185,7 +1244,7 @@ msgstr "不能删除根节点 ({})" msgid "Deletion failed and the node contains assets" msgstr "删除失败,节点包含资产" -#: assets/api/tree.py:49 assets/serializers/node.py:41 +#: assets/api/tree.py:49 assets/serializers/node.py:42 msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" @@ -1216,16 +1275,16 @@ msgid "Unable to connect to port {port} on {address}" msgstr "无法连接到 {port} 上的端口 {address}" #: assets/automations/ping_gateway/manager.py:58 -#: authentication/middleware.py:92 xpack/plugins/cloud/providers/fc.py:47 +#: authentication/middleware.py:93 xpack/plugins/cloud/providers/fc.py:47 msgid "Authentication failed" msgstr "认证失败" #: assets/automations/ping_gateway/manager.py:60 -#: assets/automations/ping_gateway/manager.py:86 +#: assets/automations/ping_gateway/manager.py:86 terminal/const.py:100 msgid "Connect failed" msgstr "连接失败" -#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:37 +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:44 #: audits/signal_handlers/activity_log.py:62 common/utils/ip/geoip/utils.py:31 #: common/utils/ip/geoip/utils.py:37 common/utils/ip/utils.py:104 msgid "Unknown" @@ -1247,25 +1306,25 @@ msgstr "测试网关" msgid "Gather facts" msgstr "收集资产信息" -#: assets/const/base.py:33 audits/const.py:48 +#: assets/const/base.py:32 audits/const.py:55 #: terminal/serializers/applet_host.py:32 msgid "Disabled" msgstr "禁用" -#: assets/const/base.py:34 settings/serializers/basic.py:6 -#: users/serializers/preference/koko.py:15 -#: users/serializers/preference/lina.py:32 +#: assets/const/base.py:33 settings/serializers/basic.py:6 +#: users/serializers/preference/koko.py:19 +#: users/serializers/preference/lina.py:39 #: users/serializers/preference/luna.py:60 msgid "Basic" msgstr "基本" -#: assets/const/base.py:35 assets/const/protocol.py:245 +#: assets/const/base.py:34 assets/const/protocol.py:252 #: assets/models/asset/web.py:13 msgid "Script" msgstr "脚本" #: assets/const/category.py:10 assets/models/asset/host.py:8 -#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 +#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:71 #: settings/serializers/feature.py:47 terminal/models/component/endpoint.py:13 #: terminal/serializers/applet.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:72 @@ -1281,7 +1340,7 @@ msgid "Cloud service" msgstr "云服务" #: assets/const/category.py:14 assets/models/asset/gpt.py:11 -#: assets/models/asset/web.py:16 audits/const.py:35 +#: assets/models/asset/web.py:16 audits/const.py:42 #: terminal/models/applet/applet.py:27 users/const.py:47 msgid "Web" msgstr "Web" @@ -1327,11 +1386,11 @@ msgstr "ChatGPT" msgid "Other" msgstr "其它" -#: assets/const/protocol.py:48 +#: assets/const/protocol.py:49 msgid "SFTP root" msgstr "SFTP 根路径" -#: assets/const/protocol.py:50 +#: assets/const/protocol.py:51 #, python-brace-format msgid "" "SFTP root directory, Support variable:
- ${ACCOUNT} The connected " @@ -1341,105 +1400,105 @@ msgstr "" "SFTP根目录,支持变量:
-${ACCOUNT}已连接帐户用户名
-${HOME}连接帐户的主" "目录
-${USER}用户的用户名" -#: assets/const/protocol.py:65 +#: assets/const/protocol.py:66 msgid "Console" msgstr "控制台" -#: assets/const/protocol.py:66 +#: assets/const/protocol.py:67 msgid "Connect to console session" msgstr "连接到控制台会话" -#: assets/const/protocol.py:70 +#: assets/const/protocol.py:71 msgid "Any" msgstr "任意" -#: assets/const/protocol.py:72 settings/serializers/security.py:228 +#: assets/const/protocol.py:73 settings/serializers/security.py:228 msgid "Security" msgstr "安全" -#: assets/const/protocol.py:73 +#: assets/const/protocol.py:74 msgid "Security layer to use for the connection" msgstr "连接 RDP 使用的安全层" -#: assets/const/protocol.py:79 +#: assets/const/protocol.py:80 msgid "AD domain" msgstr "AD 网域" -#: assets/const/protocol.py:94 +#: assets/const/protocol.py:95 msgid "Username prompt" msgstr "用户名提示" -#: assets/const/protocol.py:95 +#: assets/const/protocol.py:96 msgid "We will send username when we see this prompt" msgstr "当我们看到这个提示时,我们将发送用户名" -#: assets/const/protocol.py:100 +#: assets/const/protocol.py:101 msgid "Password prompt" msgstr "密码提示" -#: assets/const/protocol.py:101 +#: assets/const/protocol.py:102 msgid "We will send password when we see this prompt" msgstr "当我们看到这个提示时,我们将发送密码" -#: assets/const/protocol.py:106 +#: assets/const/protocol.py:107 msgid "Success prompt" msgstr "成功提示" -#: assets/const/protocol.py:107 +#: assets/const/protocol.py:108 msgid "We will consider login success when we see this prompt" msgstr "当我们看到这个提示时,我们将认为登录成功" -#: assets/const/protocol.py:118 assets/models/asset/database.py:10 +#: assets/const/protocol.py:119 assets/models/asset/database.py:10 #: settings/serializers/msg.py:40 msgid "Use SSL" msgstr "使用 SSL" -#: assets/const/protocol.py:153 +#: assets/const/protocol.py:154 msgid "SYSDBA" msgstr "SYSDBA" -#: assets/const/protocol.py:154 +#: assets/const/protocol.py:155 msgid "Connect as SYSDBA" msgstr "以 SYSDBA 角色连接" -#: assets/const/protocol.py:169 +#: assets/const/protocol.py:170 msgid "" "SQL Server version, Different versions have different connection drivers" msgstr "SQL Server 版本,不同版本有不同的连接驱动" -#: assets/const/protocol.py:192 +#: assets/const/protocol.py:199 msgid "Auth username" msgstr "使用用户名认证" -#: assets/const/protocol.py:215 +#: assets/const/protocol.py:222 msgid "Safe mode" msgstr "安全模式" -#: assets/const/protocol.py:217 +#: assets/const/protocol.py:224 msgid "" "When safe mode is enabled, some operations will be disabled, such as: New " "tab, right click, visit other website, etc." msgstr "" "当安全模式启用时,一些操作将被禁用,例如:新建标签页、右键、访问其它网站 等" -#: assets/const/protocol.py:222 assets/models/asset/web.py:9 +#: assets/const/protocol.py:229 assets/models/asset/web.py:9 #: assets/serializers/asset/info/spec.py:16 msgid "Autofill" msgstr "自动代填" -#: assets/const/protocol.py:230 assets/models/asset/web.py:10 +#: assets/const/protocol.py:237 assets/models/asset/web.py:10 msgid "Username selector" msgstr "用户名选择器" -#: assets/const/protocol.py:235 assets/models/asset/web.py:11 +#: assets/const/protocol.py:242 assets/models/asset/web.py:11 msgid "Password selector" msgstr "密码选择器" -#: assets/const/protocol.py:240 assets/models/asset/web.py:12 +#: assets/const/protocol.py:247 assets/models/asset/web.py:12 msgid "Submit selector" msgstr "确认按钮选择器" -#: assets/const/protocol.py:263 +#: assets/const/protocol.py:270 msgid "API mode" msgstr "API 模式" @@ -1467,19 +1526,19 @@ msgstr "SSH公钥" # msgstr "备注" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:19 -#: audits/models.py:259 common/db/models.py:34 ops/models/base.py:54 -#: ops/models/job.py:227 users/models/user.py:1033 +#: audits/models.py:266 common/db/models.py:34 ops/models/base.py:54 +#: ops/models/job.py:227 users/models/user.py:1024 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:35 users/models/user.py:855 +#: common/db/models.py:35 users/models/user.py:846 msgid "Date updated" msgstr "更新日期" #: assets/models/_user.py:30 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:18 -#: common/db/models.py:32 users/models/user.py:841 +#: common/db/models.py:32 users/models/user.py:835 #: users/serializers/group.py:29 msgid "Created by" msgstr "创建者" @@ -1507,7 +1566,7 @@ msgstr "用户名与用户相同" #: assets/models/_user.py:52 authentication/models/connection_token.py:41 #: authentication/serializers/connect_token_secret.py:111 #: terminal/models/applet/applet.py:42 terminal/serializers/session.py:19 -#: terminal/serializers/session.py:42 terminal/serializers/storage.py:70 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:70 msgid "Protocol" msgstr "协议" @@ -1515,7 +1574,7 @@ msgstr "协议" msgid "Sudo" msgstr "Sudo" -#: assets/models/_user.py:55 ops/const.py:49 +#: assets/models/_user.py:55 ops/const.py:49 ops/const.py:59 msgid "Shell" msgstr "Shell" @@ -1556,7 +1615,7 @@ msgid "Cloud" msgstr "云服务" #: assets/models/asset/common.py:92 assets/models/platform.py:16 -#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 +#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:72 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" @@ -1582,7 +1641,7 @@ msgstr "网域" msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:312 +#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:379 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "收集资产硬件信息" @@ -1632,7 +1691,7 @@ msgid "Proxy" msgstr "代理" #: assets/models/automations/base.py:22 ops/models/job.py:223 -#: settings/serializers/auth/sms.py:99 +#: settings/serializers/auth/sms.py:103 msgid "Parameters" msgstr "参数" @@ -1644,9 +1703,9 @@ msgstr "自动化任务" msgid "Asset automation task" msgstr "资产自动化任务" -#: assets/models/automations/base.py:113 audits/models.py:200 +#: assets/models/automations/base.py:113 audits/models.py:207 #: audits/serializers.py:51 ops/models/base.py:49 ops/models/job.py:220 -#: terminal/models/applet/applet.py:301 terminal/models/applet/host.py:139 +#: terminal/models/applet/applet.py:303 terminal/models/applet/host.py:139 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:115 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1673,7 +1732,7 @@ msgstr "校验日期" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:32 users/models/group.py:25 -#: users/models/user.py:804 +#: users/models/user.py:798 msgid "User group" msgstr "用户组" @@ -1723,11 +1782,11 @@ msgstr "默认" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1018 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1009 msgid "System" msgstr "系统" -#: assets/models/label.py:19 assets/models/node.py:544 +#: assets/models/label.py:19 assets/models/node.py:539 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:29 #: authentication/serializers/connect_token_secret.py:122 @@ -1749,28 +1808,28 @@ msgstr "标签" msgid "New node" msgstr "新节点" -#: assets/models/node.py:472 audits/backends/db.py:55 audits/backends/db.py:56 +#: assets/models/node.py:467 audits/backends/db.py:55 audits/backends/db.py:56 msgid "empty" msgstr "空" -#: assets/models/node.py:543 perms/models/perm_node.py:28 +#: assets/models/node.py:538 perms/models/perm_node.py:28 msgid "Key" msgstr "键" -#: assets/models/node.py:545 assets/serializers/node.py:20 +#: assets/models/node.py:540 assets/serializers/node.py:20 msgid "Full value" msgstr "全称" -#: assets/models/node.py:549 perms/models/perm_node.py:30 +#: assets/models/node.py:544 perms/models/perm_node.py:30 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:558 perms/serializers/permission.py:35 +#: assets/models/node.py:553 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:322 msgid "Node" msgstr "节点" -#: assets/models/node.py:561 +#: assets/models/node.py:556 msgid "Can match node" msgstr "可以匹配节点" @@ -1792,7 +1851,7 @@ msgstr "开放的" msgid "Setting" msgstr "设置" -#: assets/models/platform.py:38 audits/const.py:49 +#: assets/models/platform.py:38 audits/const.py:56 #: authentication/backends/passkey/models.py:11 settings/models.py:36 #: terminal/serializers/applet_host.py:33 msgid "Enabled" @@ -1917,35 +1976,30 @@ msgid "Node path" msgstr "节点路径" #: assets/serializers/asset/common.py:145 -#: assets/serializers/asset/common.py:313 +#: assets/serializers/asset/common.py:380 msgid "Auto info" msgstr "自动化信息" -#: assets/serializers/asset/common.py:236 +#: assets/serializers/asset/common.py:238 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:272 +#: assets/serializers/asset/common.py:274 msgid "port out of range (0-65535)" msgstr "端口超出范围 (0-65535)" -#: assets/serializers/asset/common.py:279 +#: assets/serializers/asset/common.py:281 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" -#: assets/serializers/asset/database.py:13 +#: assets/serializers/asset/common.py:309 +msgid "Invalid data" +msgstr "无效的数据" + +#: assets/serializers/asset/database.py:12 msgid "Default database" msgstr "默认数据库" -#: assets/serializers/asset/database.py:28 common/db/fields.py:570 -#: common/db/fields.py:575 common/serializers/fields.py:104 -#: tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -#: xpack/plugins/cloud/serializers/account_attrs.py:79 -#: xpack/plugins/cloud/serializers/account_attrs.py:143 -msgid "This field is required." -msgstr "该字段是必填项。" - #: assets/serializers/asset/gpt.py:20 msgid "" "If the server cannot directly connect to the API address, you need set up an " @@ -2180,7 +2234,7 @@ msgstr "删除目录" #: audits/const.py:14 audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:65 -#: perms/const.py:17 rbac/tree.py:235 +#: perms/const.py:17 rbac/tree.py:237 msgid "Delete" msgstr "删除" @@ -2197,7 +2251,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/const.py:18 audits/const.py:28 perms/const.py:14 -#: terminal/api/session/session.py:141 +#: terminal/api/session/session.py:146 msgid "Download" msgstr "下载" @@ -2205,7 +2259,7 @@ msgstr "下载" msgid "Rename dir" msgstr "映射目录" -#: audits/const.py:23 rbac/tree.py:233 terminal/api/session/session.py:252 +#: audits/const.py:23 rbac/tree.py:235 terminal/api/session/session.py:257 #: terminal/templates/terminal/_msg_command_warning.html:18 #: terminal/templates/terminal/_msg_session_sharing.html:10 msgid "View" @@ -2213,7 +2267,7 @@ msgstr "查看" #: audits/const.py:26 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:232 +#: rbac/tree.py:234 msgid "Create" msgstr "创建" @@ -2221,8 +2275,8 @@ msgstr "创建" msgid "Connect" msgstr "连接" -#: audits/const.py:30 authentication/templates/authentication/login.html:252 -#: authentication/templates/authentication/login.html:325 +#: audits/const.py:30 authentication/templates/authentication/login.html:290 +#: authentication/templates/authentication/login.html:363 #: templates/_header_bar.html:95 msgid "Login" msgstr "登录" @@ -2231,30 +2285,41 @@ msgstr "登录" msgid "Change password" msgstr "改密" -#: audits/const.py:36 settings/serializers/terminal.py:6 +#: audits/const.py:37 tickets/const.py:46 +msgid "Approve" +msgstr "同意" + +#: audits/const.py:38 +#: authentication/templates/authentication/_access_key_modal.html:155 +#: authentication/templates/authentication/_mfa_confirm_modal.html:53 +#: templates/_modal.html:22 tickets/const.py:44 +msgid "Close" +msgstr "关闭" + +#: audits/const.py:43 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:164 -#: terminal/serializers/session.py:49 terminal/serializers/session.py:63 +#: terminal/serializers/session.py:52 terminal/serializers/session.py:66 msgid "Terminal" msgstr "终端" -#: audits/const.py:41 audits/models.py:128 +#: audits/const.py:48 audits/models.py:131 msgid "Operate log" msgstr "操作日志" -#: audits/const.py:42 +#: audits/const.py:49 msgid "Session log" msgstr "会话日志" -#: audits/const.py:43 +#: audits/const.py:50 msgid "Login log" msgstr "登录日志" -#: audits/const.py:44 terminal/models/applet/host.py:143 +#: audits/const.py:51 terminal/models/applet/host.py:143 #: terminal/models/component/task.py:22 msgid "Task" msgstr "任务" -#: audits/const.py:50 +#: audits/const.py:57 msgid "-" msgstr "-" @@ -2266,28 +2331,28 @@ msgstr "是" msgid "No" msgstr "否" -#: audits/models.py:43 +#: audits/models.py:46 msgid "Job audit log" msgstr "作业审计日志" -#: audits/models.py:52 audits/models.py:96 audits/models.py:167 +#: audits/models.py:55 audits/models.py:99 audits/models.py:174 #: terminal/models/session/session.py:38 terminal/models/session/sharing.py:113 msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:57 audits/serializers.py:35 +#: audits/models.py:60 audits/serializers.py:35 msgid "Operate" msgstr "操作" -#: audits/models.py:59 +#: audits/models.py:62 msgid "Filename" msgstr "文件名" -#: audits/models.py:62 common/serializers/common.py:98 +#: audits/models.py:65 common/serializers/common.py:98 msgid "File" msgstr "文件" -#: audits/models.py:63 terminal/backends/command/models.py:21 +#: audits/models.py:66 terminal/backends/command/models.py:21 #: terminal/models/session/replay.py:9 terminal/models/session/sharing.py:20 #: terminal/models/session/sharing.py:95 #: terminal/templates/terminal/_msg_command_alert.html:10 @@ -2296,103 +2361,86 @@ msgstr "文件" msgid "Session" msgstr "会话" -#: audits/models.py:66 +#: audits/models.py:69 msgid "File transfer log" msgstr "文件管理" -#: audits/models.py:90 audits/serializers.py:86 +#: audits/models.py:93 audits/serializers.py:86 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:91 audits/models.py:94 audits/models.py:140 +#: audits/models.py:94 audits/models.py:97 audits/models.py:143 #: audits/serializers.py:85 msgid "Resource" msgstr "资源" -#: audits/models.py:97 audits/models.py:143 audits/models.py:169 +#: audits/models.py:100 audits/models.py:146 audits/models.py:176 #: terminal/serializers/command.py:75 msgid "Datetime" msgstr "日期" -#: audits/models.py:136 +#: audits/models.py:139 msgid "Activity type" msgstr "活动类型" -#: audits/models.py:146 +#: audits/models.py:149 msgid "Detail" msgstr "详情" -#: audits/models.py:149 +#: audits/models.py:152 msgid "Detail ID" msgstr "详情 ID" -#: audits/models.py:153 +#: audits/models.py:156 msgid "Activity log" msgstr "活动日志" -#: audits/models.py:165 +#: audits/models.py:172 msgid "Change by" msgstr "修改者" -#: audits/models.py:175 +#: audits/models.py:182 msgid "Password change log" msgstr "改密日志" -#: audits/models.py:182 audits/models.py:257 +#: audits/models.py:189 audits/models.py:264 msgid "Login type" msgstr "登录方式" -#: audits/models.py:184 audits/models.py:253 +#: audits/models.py:191 audits/models.py:260 #: tickets/models/ticket/login_confirm.py:10 msgid "Login IP" msgstr "登录 IP" -#: audits/models.py:186 audits/models.py:255 -#: authentication/templates/authentication/_msg_different_city.html:11 -#: tickets/models/ticket/login_confirm.py:11 -msgid "Login city" -msgstr "登录城市" - -#: audits/models.py:189 audits/models.py:256 audits/serializers.py:65 -msgid "User agent" -msgstr "用户代理" - -#: audits/models.py:192 audits/serializers.py:49 +#: audits/models.py:199 audits/serializers.py:49 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:821 +#: users/forms/profile.py:65 users/models/user.py:815 #: users/serializers/profile.py:102 msgid "MFA" msgstr "MFA" -#: audits/models.py:202 +#: audits/models.py:209 msgid "Date login" msgstr "登录日期" -#: audits/models.py:204 audits/models.py:258 audits/serializers.py:67 -#: audits/serializers.py:183 +#: audits/models.py:211 audits/models.py:265 audits/serializers.py:67 +#: audits/serializers.py:184 msgid "Authentication backend" msgstr "认证方式" -#: audits/models.py:248 +#: audits/models.py:255 msgid "User login log" msgstr "用户登录日志" -#: audits/models.py:254 +#: audits/models.py:261 msgid "Session key" msgstr "会话标识" -#: audits/models.py:260 authentication/models/connection_token.py:47 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 -#: tickets/models/ticket/apply_application.py:31 -#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:839 -msgid "Date expired" -msgstr "失效日期" - -#: audits/models.py:278 +#: audits/models.py:300 msgid "User session" msgstr "用户会话" -#: audits/models.py:280 +#: audits/models.py:302 msgid "Offline ussr session" msgstr "下限用户会话" @@ -2405,6 +2453,13 @@ msgstr "原因描述" msgid "User %s %s this resource" msgstr "用户 %s %s 了当前资源" +#: audits/serializers.py:172 authentication/models/connection_token.py:47 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 +#: tickets/models/ticket/apply_application.py:31 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:833 +msgid "Date expired" +msgstr "失效日期" + #: audits/signal_handlers/activity_log.py:26 #, python-format msgid "User %s use account %s login asset %s" @@ -2420,46 +2475,46 @@ msgstr "用户 %s 登录系统 %s" msgid "User %s perform a task for this resource: %s" msgstr "用户 %s 在当前资源, 执行了任务 (%s)" -#: audits/signal_handlers/login_log.py:34 +#: audits/signal_handlers/login_log.py:33 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signal_handlers/login_log.py:36 settings/serializers/auth/sso.py:13 +#: audits/signal_handlers/login_log.py:35 settings/serializers/auth/sso.py:13 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers/login_log.py:37 +#: audits/signal_handlers/login_log.py:36 msgid "Auth Token" msgstr "认证令牌" -#: audits/signal_handlers/login_log.py:38 authentication/notifications.py:73 -#: authentication/views/login.py:77 authentication/views/wecom.py:159 +#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73 +#: authentication/views/login.py:77 authentication/views/wecom.py:160 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:751 users/models/user.py:856 +#: users/models/user.py:745 users/models/user.py:847 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers/login_log.py:39 authentication/views/feishu.py:122 +#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:123 #: authentication/views/login.py:89 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 -#: settings/serializers/auth/feishu.py:13 users/models/user.py:753 -#: users/models/user.py:858 +#: settings/serializers/auth/feishu.py:13 users/models/user.py:747 +#: users/models/user.py:849 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers/login_log.py:40 authentication/views/dingtalk.py:159 +#: audits/signal_handlers/login_log.py:39 authentication/views/dingtalk.py:160 #: authentication/views/login.py:83 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:752 -#: users/models/user.py:857 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:746 +#: users/models/user.py:848 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers/login_log.py:41 +#: audits/signal_handlers/login_log.py:40 #: authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "临时密码" -#: audits/signal_handlers/login_log.py:42 authentication/views/login.py:95 +#: audits/signal_handlers/login_log.py:41 authentication/views/login.py:95 #: settings/serializers/auth/passkey.py:8 msgid "Passkey" msgstr "" @@ -2472,31 +2527,35 @@ msgstr "清理审计会话任务日志" msgid "Upload FTP file to external storage" msgstr "上传 FTP 文件到外部存储" -#: authentication/api/confirm.py:40 +#: authentication/api/access_key.py:39 +msgid "Access keys can be created at most 10" +msgstr "" + +#: authentication/api/confirm.py:50 msgid "This action require verify your MFA" msgstr "该操作需要验证您的 MFA, 请先开启并配置" -#: authentication/api/connection_token.py:260 +#: authentication/api/connection_token.py:262 msgid "Reusable connection token is not allowed, global setting not enabled" msgstr "不允许使用可重复使用的连接令牌,未启用全局设置" -#: authentication/api/connection_token.py:351 +#: authentication/api/connection_token.py:375 msgid "Anonymous account is not supported for this asset" msgstr "匿名账号不支持当前资产" -#: authentication/api/connection_token.py:370 +#: authentication/api/connection_token.py:394 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:373 +#: authentication/api/connection_token.py:397 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:387 +#: authentication/api/connection_token.py:427 msgid "ACL action is reject: {}({})" msgstr "ACL 动作是拒绝: {}({})" -#: authentication/api/connection_token.py:391 +#: authentication/api/connection_token.py:431 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2504,23 +2563,23 @@ msgstr "ACL 动作是复核" msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" -#: authentication/api/password.py:32 terminal/api/session/session.py:300 -#: users/views/profile/reset.py:44 +#: authentication/api/password.py:34 terminal/api/session/session.py:305 +#: users/views/profile/reset.py:61 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: authentication/api/password.py:32 users/views/profile/reset.py:127 +#: authentication/api/password.py:34 users/views/profile/reset.py:163 msgid "No user matched" msgstr "没有匹配到用户" -#: authentication/api/password.py:36 +#: authentication/api/password.py:38 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" msgstr "用户来自 {} 请去相应系统修改密码" -#: authentication/api/password.py:64 -#: authentication/templates/authentication/login.html:317 +#: authentication/api/password.py:67 +#: authentication/templates/authentication/login.html:355 #: users/templates/users/forgot_password.html:27 #: users/templates/users/forgot_password.html:28 #: users/templates/users/forgot_password_previewing.html:13 @@ -2537,58 +2596,28 @@ msgstr "认证" msgid "User invalid, disabled or expired" msgstr "用户无效,已禁用或已过期" -#: authentication/backends/drf.py:54 -msgid "Invalid signature header. No credentials provided." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:57 -msgid "Invalid signature header. Signature string should not contain spaces." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:64 -msgid "Invalid signature header. Format like AccessKeyId:Signature" -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:68 -msgid "" -"Invalid signature header. Signature string should not contain invalid " -"characters." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:88 authentication/backends/drf.py:104 -msgid "Invalid signature." -msgstr "签名无效" - -#: authentication/backends/drf.py:95 -msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" -msgstr "HTTP header not valid" - -#: authentication/backends/drf.py:100 -msgid "Expired, more than 15 minutes" -msgstr "已过期,超过15分钟" - -#: authentication/backends/drf.py:107 -msgid "User disabled." -msgstr "用户已禁用" - -#: authentication/backends/drf.py:125 +#: authentication/backends/drf.py:39 msgid "Invalid token header. No credentials provided." msgstr "无效的令牌头。没有提供任何凭据。" -#: authentication/backends/drf.py:128 +#: authentication/backends/drf.py:42 msgid "Invalid token header. Sign string should not contain spaces." msgstr "无效的令牌头。符号字符串不应包含空格。" -#: authentication/backends/drf.py:135 +#: authentication/backends/drf.py:48 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "无效的令牌头。符号字符串不应包含无效字符。" -#: authentication/backends/drf.py:146 +#: authentication/backends/drf.py:61 msgid "Invalid token or cache refreshed." msgstr "刷新的令牌或缓存无效。" -#: authentication/backends/passkey/api.py:52 +#: authentication/backends/passkey/api.py:37 +msgid "Only register passkey for local user" +msgstr "" + +#: authentication/backends/passkey/api.py:65 msgid "Auth failed" msgstr "认证失败" @@ -2601,6 +2630,8 @@ msgid "Added on" msgstr "附加" #: authentication/backends/passkey/models.py:14 +#: authentication/models/access_key.py:21 +#: authentication/models/private_token.py:8 msgid "Date last used" msgstr "最后使用日期" @@ -2738,21 +2769,21 @@ msgstr "手机号没有设置" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:61 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:62 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:202 -#: authentication/views/wecom.py:244 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:204 +#: authentication/views/wecom.py:246 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:209 -#: authentication/views/dingtalk.py:251 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:211 +#: authentication/views/dingtalk.py:253 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:166 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:168 msgid "FeiShu is not bound" msgstr "没有绑定飞书" @@ -2760,15 +2791,20 @@ msgstr "没有绑定飞书" msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/errors/redirect.py:85 authentication/mixins.py:318 +#: authentication/errors/mfa.py:43 +#, python-format +msgid "Please wait for %s seconds before retry" +msgstr "请在 %s 秒后重试" + +#: authentication/errors/redirect.py:85 authentication/mixins.py:323 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors/redirect.py:93 authentication/mixins.py:325 +#: authentication/errors/redirect.py:93 authentication/mixins.py:330 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors/redirect.py:101 authentication/mixins.py:332 +#: authentication/errors/redirect.py:101 authentication/mixins.py:337 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -2847,9 +2883,9 @@ msgstr "短信验证码校验失败" #: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 -#: settings/serializers/auth/sms.py:28 users/forms/profile.py:104 +#: settings/serializers/auth/sms.py:32 users/forms/profile.py:104 #: users/forms/profile.py:109 users/templates/users/forgot_password.html:112 -#: users/views/profile/reset.py:79 +#: users/views/profile/reset.py:98 msgid "SMS" msgstr "短信" @@ -2865,21 +2901,25 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:93 settings/utils/ldap.py:661 +#: authentication/middleware.py:94 settings/utils/ldap.py:679 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败 (登录前检查失败): {}" -#: authentication/mixins.py:91 +#: authentication/mixins.py:82 +msgid "User is invalid" +msgstr "无效的用户" + +#: authentication/mixins.py:97 msgid "" "The administrator has enabled 'Only allow login from user source'. \n" " The current user source is {}. Please contact the administrator." msgstr "管理员已开启'仅允许从用户来源登录',当前用户来源为{},请联系管理员。" -#: authentication/mixins.py:268 +#: authentication/mixins.py:273 msgid "The MFA type ({}) is not enabled" msgstr "该 MFA ({}) 方式没有启用" -#: authentication/mixins.py:308 +#: authentication/mixins.py:313 msgid "Please change your password" msgstr "请修改密码" @@ -2958,7 +2998,7 @@ msgstr "可以查看超级连接令牌密文" msgid "Super connection token" msgstr "超级连接令牌" -#: authentication/models/private_token.py:9 +#: authentication/models/private_token.py:11 msgid "Private Token" msgstr "私有令牌" @@ -3016,7 +3056,7 @@ msgstr "动作" #: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:174 +#: users/serializers/user.py:97 users/serializers/user.py:171 msgid "Is expired" msgstr "已过期" @@ -3024,9 +3064,9 @@ msgstr "已过期" #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/msg.py:22 #: settings/serializers/msg.py:57 users/forms/profile.py:102 -#: users/forms/profile.py:109 users/models/user.py:800 +#: users/forms/profile.py:109 users/models/user.py:794 #: users/templates/users/forgot_password.html:117 -#: users/views/profile/reset.py:73 +#: users/views/profile/reset.py:92 msgid "Email" msgstr "邮箱" @@ -3035,9 +3075,9 @@ msgstr "邮箱" msgid "The {} cannot be empty" msgstr "{} 不能为空" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: authentication/serializers/token.py:86 perms/serializers/permission.py:37 #: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: users/serializers/user.py:168 msgid "Is valid" msgstr "是否有效" @@ -3062,13 +3102,13 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:646 users/serializers/profile.py:92 +#: users/models/user.py:640 users/serializers/profile.py:92 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:647 users/serializers/profile.py:93 +#: users/models/user.py:641 users/serializers/profile.py:93 #: users/templates/users/mfa_setting.html:26 #: users/templates/users/mfa_setting.html:68 msgid "Enable" @@ -3078,12 +3118,6 @@ msgstr "启用" msgid "Delete success" msgstr "删除成功" -#: authentication/templates/authentication/_access_key_modal.html:155 -#: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/const.py:44 -msgid "Close" -msgstr "关闭" - #: authentication/templates/authentication/_captcha_field.html:8 msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" @@ -3113,7 +3147,7 @@ msgstr "代码错误" #: 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:447 +#: jumpserver/conf.py:449 #: 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 @@ -3217,17 +3251,17 @@ msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能 msgid "Cancel" msgstr "取消" -#: authentication/templates/authentication/login.html:237 +#: authentication/templates/authentication/login.html:270 msgid "" "Configuration file has problems and cannot be logged in. Please contact the " "administrator or view latest docs" msgstr "配置文件有问题,无法登录,请联系管理员或查看最新文档" -#: authentication/templates/authentication/login.html:238 +#: authentication/templates/authentication/login.html:271 msgid "If you are administrator, you can update the config resolve it, set" msgstr "如果你是管理员,可以更新配置文件解决,设置配置项" -#: authentication/templates/authentication/login.html:332 +#: authentication/templates/authentication/login.html:370 msgid "More login options" msgstr "其他方式登录" @@ -3273,79 +3307,79 @@ msgstr "本页面未使用 HTTPS 协议,请使用 HTTPS 协议以确保您的 msgid "Do you want to retry ?" msgstr "是否重试 ?" -#: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 +#: authentication/utils.py:23 common/utils/ip/geoip/utils.py:24 #: xpack/plugins/cloud/const.py:29 msgid "LAN" msgstr "局域网" -#: authentication/views/base.py:61 +#: authentication/views/base.py:67 #: perms/templates/perms/_msg_permed_items_expire.html:21 msgid "If you have any question, please contact the administrator" msgstr "如果有疑问或需求,请联系系统管理员" -#: authentication/views/dingtalk.py:41 +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:44 authentication/views/dingtalk.py:208 +#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:210 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:50 -#: authentication/views/wecom.py:57 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:58 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:60 +#: authentication/views/dingtalk.py:61 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:128 authentication/views/wecom.py:129 +#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:130 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:144 +#: authentication/views/dingtalk.py:145 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:153 +#: authentication/views/dingtalk.py:154 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:160 +#: authentication/views/dingtalk.py:161 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:210 authentication/views/dingtalk.py:245 +#: authentication/views/dingtalk.py:212 authentication/views/dingtalk.py:247 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:252 +#: authentication/views/dingtalk.py:254 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:38 authentication/views/feishu.py:165 +#: authentication/views/feishu.py:39 authentication/views/feishu.py:167 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:66 +#: authentication/views/feishu.py:67 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:107 +#: authentication/views/feishu.py:108 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:116 +#: authentication/views/feishu.py:117 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:123 +#: authentication/views/feishu.py:124 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:167 +#: authentication/views/feishu.py:169 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" @@ -3365,7 +3399,7 @@ msgstr "登录超时,请重新登录" msgid "User email already exists ({})" msgstr "用户邮箱已存在 ({})" -#: authentication/views/login.py:361 +#: authentication/views/login.py:355 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3373,43 +3407,43 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:366 +#: authentication/views/login.py:360 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:402 +#: authentication/views/login.py:396 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:403 +#: authentication/views/login.py:397 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:42 +#: authentication/views/wecom.py:43 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:45 authentication/views/wecom.py:201 +#: authentication/views/wecom.py:46 authentication/views/wecom.py:203 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:144 +#: authentication/views/wecom.py:145 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:153 +#: authentication/views/wecom.py:154 msgid "The WeCom is already bound to another user" msgstr "该企业微信已经绑定其他用户" -#: authentication/views/wecom.py:160 +#: authentication/views/wecom.py:161 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:203 authentication/views/wecom.py:238 +#: authentication/views/wecom.py:205 authentication/views/wecom.py:240 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:245 +#: authentication/views/wecom.py:247 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" @@ -3434,7 +3468,7 @@ msgstr "准备" msgid "Pending" msgstr "待定的" -#: common/const/choices.py:17 ops/const.py:59 +#: common/const/choices.py:17 ops/const.py:71 msgid "Running" msgstr "运行中" @@ -3502,6 +3536,14 @@ msgstr "无效类型,应为 all、ids 或 attrs" msgid "Invalid ids for ids, should be a list" msgstr "无效的ID,应为列表" +#: common/db/fields.py:570 common/db/fields.py:575 +#: common/serializers/fields.py:104 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +#: xpack/plugins/cloud/serializers/account_attrs.py:79 +#: xpack/plugins/cloud/serializers/account_attrs.py:143 +msgid "This field is required." +msgstr "该字段是必填项。" + #: common/db/fields.py:573 common/db/fields.py:578 msgid "Invalid attrs, should be a list of dict" msgstr "无效的属性,应为dict列表" @@ -3518,7 +3560,7 @@ msgstr "忽略的" msgid "discard time" msgstr "忽略时间" -#: common/db/models.py:33 users/models/user.py:842 +#: common/db/models.py:33 users/models/user.py:836 msgid "Updated by" msgstr "最后更新者" @@ -3573,11 +3615,11 @@ msgstr "多对多反向是不被允许的" msgid "Is referenced by other objects and cannot be deleted" msgstr "被其他对象关联,不能删除" -#: common/exceptions.py:48 +#: common/exceptions.py:51 msgid "This action require confirm current user" msgstr "此操作需要确认当前用户" -#: common/exceptions.py:56 +#: common/exceptions.py:59 msgid "Unexpect error occur" msgstr "发生意外错误" @@ -3609,6 +3651,15 @@ msgstr "SP_id 为6位" msgid "Failed to connect to the CMPP gateway server, err: {}" msgstr "连接网关服务器错误,错误:{}" +#: common/sdk/sms/custom_file.py:41 +msgid "The custom sms file is invalid" +msgstr "自定义短信文件无效" + +#: common/sdk/sms/custom_file.py:47 +#, python-format +msgid "SMS sending failed[%s]: %s" +msgstr "短信发送失败[%s]: %s" + #: common/sdk/sms/endpoint.py:16 msgid "Alibaba cloud" msgstr "阿里云" @@ -3625,11 +3676,11 @@ msgstr "华为云" msgid "CMPP v2.0" msgstr "CMPP v2.0" -#: common/sdk/sms/endpoint.py:31 +#: common/sdk/sms/endpoint.py:32 msgid "SMS provider not support: {}" msgstr "短信服务商不支持:{}" -#: common/sdk/sms/endpoint.py:53 +#: common/sdk/sms/endpoint.py:54 msgid "SMS verification code signature or template invalid" msgstr "短信验证码签名或模版无效" @@ -3700,11 +3751,16 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: jumpserver/conf.py:446 +#: jumpserver/conf.py:444 +#, python-brace-format +msgid "The verification code is: {code}" +msgstr "验证码为: {code}" + +#: jumpserver/conf.py:448 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:448 +#: jumpserver/conf.py:450 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -3739,10 +3795,6 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: notifications/apps.py:7 -msgid "Notifications" -msgstr "通知" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "站内信" @@ -3767,15 +3819,15 @@ msgstr "系统信息" msgid "Publish the station message" msgstr "发布站内消息" -#: ops/ansible/inventory.py:92 ops/models/job.py:60 +#: ops/ansible/inventory.py:95 ops/models/job.py:60 msgid "No account available" msgstr "无可用账号" -#: ops/ansible/inventory.py:263 +#: ops/ansible/inventory.py:259 msgid "Ansible disabled" msgstr "Ansible 已禁用" -#: ops/ansible/inventory.py:279 +#: ops/ansible/inventory.py:275 msgid "Skip hosts below:" msgstr "跳过以下主机: " @@ -3791,14 +3843,34 @@ msgstr "任务 {} 不存在" msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" -#: ops/api/playbook.py:37 +#: ops/api/playbook.py:39 msgid "Currently playbook is being used in a job" msgstr "当前 playbook 正在作业中使用" -#: ops/api/playbook.py:91 +#: ops/api/playbook.py:93 msgid "Unsupported file content" msgstr "不支持的文件内容" +#: ops/api/playbook.py:95 ops/api/playbook.py:141 ops/api/playbook.py:189 +msgid "Invalid file path" +msgstr "无效的文件路径" + +#: ops/api/playbook.py:167 +msgid "This file can not be rename" +msgstr "该文件不能重命名" + +#: ops/api/playbook.py:186 +msgid "File already exists" +msgstr "文件已存在" + +#: ops/api/playbook.py:204 +msgid "File key is required" +msgstr "文件密钥该字段是必填项。" + +#: ops/api/playbook.py:207 +msgid "This file can not be delete" +msgstr "无法删除此文件" + #: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:55 msgid "App ops" msgstr "作业中心" @@ -3851,31 +3923,39 @@ msgstr "仅限特权账号" msgid "Privileged First" msgstr "特权账号优先" -#: ops/const.py:50 +#: ops/const.py:50 ops/const.py:60 msgid "Powershell" msgstr "PowerShell" -#: ops/const.py:51 +#: ops/const.py:51 ops/const.py:61 msgid "Python" msgstr "Python" -#: ops/const.py:52 +#: ops/const.py:52 ops/const.py:62 msgid "MySQL" msgstr "MySQL" -#: ops/const.py:53 +#: ops/const.py:53 ops/const.py:64 msgid "PostgreSQL" msgstr "PostgreSQL" -#: ops/const.py:54 +#: ops/const.py:54 ops/const.py:65 msgid "SQLServer" msgstr "SQLServer" -#: ops/const.py:55 +#: ops/const.py:55 ops/const.py:67 msgid "Raw" -msgstr "" +msgstr "Raw" -#: ops/const.py:61 +#: ops/const.py:63 +msgid "MariaDB" +msgstr "MariaDB" + +#: ops/const.py:66 +msgid "Oracle" +msgstr "Oracle" + +#: ops/const.py:73 msgid "Timeout" msgstr "超时" @@ -3883,28 +3963,28 @@ msgstr "超时" msgid "no valid program entry found." msgstr "没有可用程序入口" -#: ops/mixin.py:26 ops/mixin.py:89 settings/serializers/auth/ldap.py:73 +#: ops/mixin.py:26 ops/mixin.py:90 settings/serializers/auth/ldap.py:73 msgid "Cycle perform" msgstr "周期执行" -#: ops/mixin.py:30 ops/mixin.py:87 ops/mixin.py:106 +#: ops/mixin.py:30 ops/mixin.py:88 ops/mixin.py:107 #: settings/serializers/auth/ldap.py:70 msgid "Regularly perform" msgstr "定期执行" -#: ops/mixin.py:109 +#: ops/mixin.py:110 msgid "Interval" msgstr "间隔" -#: ops/mixin.py:119 +#: ops/mixin.py:120 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: ops/mixin.py:126 +#: ops/mixin.py:127 msgid "Range {} to {}" msgstr "输入在 {} - {} 范围之间" -#: ops/mixin.py:137 +#: ops/mixin.py:138 msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" @@ -3973,7 +4053,7 @@ msgstr "结束" msgid "Date published" msgstr "发布日期" -#: ops/models/celery.py:86 +#: ops/models/celery.py:87 msgid "Celery Task Execution" msgstr "Celery 任务执行" @@ -4061,7 +4141,7 @@ msgstr "保存后执行" msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:57 terminal/serializers/session.py:50 +#: ops/serializers/job.py:57 terminal/serializers/session.py:53 msgid "Is finished" msgstr "是否完成" @@ -4073,23 +4153,23 @@ msgstr "花费时间" msgid "Run ansible task" msgstr "运行 Ansible 任务" -#: ops/tasks.py:63 +#: ops/tasks.py:68 msgid "Run ansible task execution" msgstr "开始执行 Ansible 任务" -#: ops/tasks.py:85 +#: ops/tasks.py:90 msgid "Clear celery periodic tasks" msgstr "清理周期任务" -#: ops/tasks.py:106 +#: ops/tasks.py:111 msgid "Create or update periodic tasks" msgstr "创建或更新周期任务" -#: ops/tasks.py:114 +#: ops/tasks.py:119 msgid "Periodic check service performance" msgstr "周期检测服务性能" -#: ops/tasks.py:120 +#: ops/tasks.py:125 msgid "Clean up unexpected jobs" msgstr "清理异常作业" @@ -4151,7 +4231,7 @@ msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进 msgid "The organization have resource ({}) cannot be deleted" msgstr "组织存在资源 ({}) 不能被删除" -#: orgs/apps.py:7 rbac/tree.py:123 +#: orgs/apps.py:7 rbac/tree.py:125 msgid "App organizations" msgstr "组织管理" @@ -4374,7 +4454,7 @@ msgid "Scope" msgstr "范围" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:808 +#: users/models/user.py:802 msgid "Role" msgstr "角色" @@ -4477,30 +4557,30 @@ msgid "My assets" msgstr "我的资产" #: rbac/tree.py:56 terminal/models/applet/applet.py:52 -#: terminal/models/applet/applet.py:298 terminal/models/applet/host.py:29 +#: terminal/models/applet/applet.py:300 terminal/models/applet/host.py:29 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "远程应用" -#: rbac/tree.py:124 +#: rbac/tree.py:126 msgid "Ticket comment" msgstr "工单评论" -#: rbac/tree.py:125 settings/serializers/feature.py:58 +#: rbac/tree.py:127 settings/serializers/feature.py:58 #: tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "工单管理" -#: rbac/tree.py:126 +#: rbac/tree.py:128 msgid "Common setting" msgstr "一般设置" -#: rbac/tree.py:127 +#: rbac/tree.py:129 msgid "View permission tree" msgstr "查看授权树" #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 -#: settings/api/sms.py:153 settings/api/vault.py:40 settings/api/wecom.py:37 +#: settings/api/sms.py:160 settings/api/vault.py:40 settings/api/wecom.py:37 msgid "Test success" msgstr "测试成功" @@ -4528,11 +4608,11 @@ msgstr "获取 LDAP 用户为 None" msgid "Imported {} users successfully (Organization: {})" msgstr "成功导入 {} 个用户 ( 组织: {} )" -#: settings/api/sms.py:135 +#: settings/api/sms.py:142 msgid "Invalid SMS platform" msgstr "无效的短信平台" -#: settings/api/sms.py:141 +#: settings/api/sms.py:148 msgid "test_phone is required" msgstr "测试手机号 该字段是必填项。" @@ -4553,38 +4633,50 @@ msgid "Can change auth setting" msgstr "认证设置" #: settings/models.py:162 +msgid "Can change auth ops" +msgstr "任务中心设置" + +#: settings/models.py:163 +msgid "Can change auth ticket" +msgstr "工单设置" + +#: settings/models.py:164 +msgid "Can change auth announcement" +msgstr "公告设置" + +#: settings/models.py:165 msgid "Can change vault setting" msgstr "可以更改 vault 设置" -#: settings/models.py:163 +#: settings/models.py:166 msgid "Can change system msg sub setting" msgstr "消息订阅设置" -#: settings/models.py:164 +#: settings/models.py:167 msgid "Can change sms setting" msgstr "短信设置" -#: settings/models.py:165 +#: settings/models.py:168 msgid "Can change security setting" msgstr "安全设置" -#: settings/models.py:166 +#: settings/models.py:169 msgid "Can change clean setting" msgstr "定期清理" -#: settings/models.py:167 +#: settings/models.py:170 msgid "Can change interface setting" msgstr "界面设置" -#: settings/models.py:168 +#: settings/models.py:171 msgid "Can change license setting" msgstr "许可证设置" -#: settings/models.py:169 +#: settings/models.py:172 msgid "Can change terminal setting" msgstr "终端设置" -#: settings/models.py:170 +#: settings/models.py:173 msgid "Can change other setting" msgstr "其它设置" @@ -4937,54 +5029,58 @@ msgstr "SP 密钥" msgid "SP cert" msgstr "SP 证书" -#: settings/serializers/auth/sms.py:16 +#: settings/serializers/auth/sms.py:17 msgid "Enable SMS" msgstr "启用 SMS" -#: settings/serializers/auth/sms.py:18 +#: settings/serializers/auth/sms.py:19 msgid "SMS provider / Protocol" msgstr "短信服务商 / 协议" -#: settings/serializers/auth/sms.py:23 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/msg.py:76 +#: settings/serializers/auth/sms.py:22 +msgid "SMS code length" +msgstr "验证码长度" + +#: settings/serializers/auth/sms.py:27 settings/serializers/auth/sms.py:49 +#: settings/serializers/auth/sms.py:57 settings/serializers/auth/sms.py:66 +#: settings/serializers/auth/sms.py:77 settings/serializers/msg.py:76 msgid "Signature" msgstr "签名" -#: settings/serializers/auth/sms.py:24 settings/serializers/auth/sms.py:46 -#: settings/serializers/auth/sms.py:54 settings/serializers/auth/sms.py:63 +#: settings/serializers/auth/sms.py:28 settings/serializers/auth/sms.py:50 +#: settings/serializers/auth/sms.py:58 settings/serializers/auth/sms.py:67 msgid "Template code" msgstr "模板" -#: settings/serializers/auth/sms.py:31 +#: settings/serializers/auth/sms.py:35 msgid "Test phone" msgstr "测试手机号" -#: settings/serializers/auth/sms.py:60 +#: settings/serializers/auth/sms.py:64 msgid "App Access Address" msgstr "应用地址" -#: settings/serializers/auth/sms.py:61 +#: settings/serializers/auth/sms.py:65 msgid "Signature channel number" msgstr "签名通道号" -#: settings/serializers/auth/sms.py:69 +#: settings/serializers/auth/sms.py:73 msgid "Enterprise code(SP id)" msgstr "企业代码(SP id)" -#: settings/serializers/auth/sms.py:70 +#: settings/serializers/auth/sms.py:74 msgid "Shared secret(Shared secret)" msgstr "共享密码(Shared secret)" -#: settings/serializers/auth/sms.py:71 +#: settings/serializers/auth/sms.py:75 msgid "Original number(Src id)" msgstr "原始号码(Src id)" -#: settings/serializers/auth/sms.py:72 +#: settings/serializers/auth/sms.py:76 msgid "Business type(Service id)" msgstr "业务类型(Service id)" -#: settings/serializers/auth/sms.py:76 +#: settings/serializers/auth/sms.py:80 #, python-brace-format msgid "" "Template need contain {code} and Signature + template length does not exceed " @@ -4994,24 +5090,24 @@ msgstr "" "模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 " "{code}, 有效期为5分钟。请不要泄露给其他人。" -#: settings/serializers/auth/sms.py:85 +#: settings/serializers/auth/sms.py:89 #, python-brace-format msgid "The template needs to contain {code}" msgstr "模板需要包含 {code}" -#: settings/serializers/auth/sms.py:88 +#: settings/serializers/auth/sms.py:92 msgid "Signature + Template must not exceed 65 words" msgstr "模板+签名不能超过65个字" -#: settings/serializers/auth/sms.py:97 +#: settings/serializers/auth/sms.py:101 msgid "URL" msgstr "URL" -#: settings/serializers/auth/sms.py:102 +#: settings/serializers/auth/sms.py:106 msgid "Request method" msgstr "请求方式" -#: settings/serializers/auth/sms.py:111 +#: settings/serializers/auth/sms.py:117 #, python-format msgid "The value in the parameter must contain %s" msgstr "参数中的值必须包含 %s" @@ -5081,45 +5177,45 @@ msgstr "支持链接" msgid "default: http://www.jumpserver.org/support/" msgstr "默认: http://www.jumpserver.org/support/" -#: settings/serializers/cleaning.py:8 +#: settings/serializers/cleaning.py:11 msgid "Period clean" msgstr "定時清掃" -#: settings/serializers/cleaning.py:12 +#: settings/serializers/cleaning.py:15 msgid "Login log keep days (day)" msgstr "登录日志 (天)" -#: settings/serializers/cleaning.py:16 +#: settings/serializers/cleaning.py:19 msgid "Task log keep days (day)" msgstr "任务日志 (天)" -#: settings/serializers/cleaning.py:20 +#: settings/serializers/cleaning.py:23 msgid "Operate log keep days (day)" msgstr "操作日志 (天)" -#: settings/serializers/cleaning.py:24 +#: settings/serializers/cleaning.py:27 msgid "FTP log keep days (day)" msgstr "上传下载 (天)" -#: settings/serializers/cleaning.py:28 +#: settings/serializers/cleaning.py:31 msgid "Cloud sync record keep days (day)" msgstr "云同步记录 (天)" -#: settings/serializers/cleaning.py:31 +#: settings/serializers/cleaning.py:35 +msgid "Activity log keep days (day)" +msgstr "活动记录 (天)" + +#: settings/serializers/cleaning.py:38 msgid "Session keep duration (day)" msgstr "会话日志 (天)" -#: settings/serializers/cleaning.py:33 +#: settings/serializers/cleaning.py:40 msgid "" "Session, record, command will be delete if more than duration, only in " "database, OSS will not be affected." msgstr "" "会话、录像,命令记录超过该时长将会被清除 (影响数据库存储,OSS 等不受影响)" -#: settings/serializers/cleaning.py:37 -msgid "Activity log keep days (day)" -msgstr "活动记录 (天)" - #: settings/serializers/feature.py:16 msgid "Subject" msgstr "主题" @@ -5625,100 +5721,100 @@ msgstr "周期导入 LDAP 用户" msgid "Registration periodic import ldap user task" msgstr "注册周期导入 LDAP 用户 任务" -#: settings/utils/ldap.py:476 +#: settings/utils/ldap.py:494 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" -#: settings/utils/ldap.py:487 +#: settings/utils/ldap.py:505 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" -#: settings/utils/ldap.py:489 +#: settings/utils/ldap.py:507 msgid "The port is not the port of the LDAP service: {}" msgstr "端口不是LDAP服务端口: {}" -#: settings/utils/ldap.py:491 +#: settings/utils/ldap.py:509 msgid "Please add certificate: {}" msgstr "请添加证书" -#: settings/utils/ldap.py:495 settings/utils/ldap.py:522 -#: settings/utils/ldap.py:552 settings/utils/ldap.py:580 +#: settings/utils/ldap.py:513 settings/utils/ldap.py:540 +#: settings/utils/ldap.py:570 settings/utils/ldap.py:598 msgid "Unknown error: {}" msgstr "未知错误: {}" -#: settings/utils/ldap.py:509 +#: settings/utils/ldap.py:527 msgid "Bind DN or Password incorrect" msgstr "绑定DN或密码错误" -#: settings/utils/ldap.py:516 +#: settings/utils/ldap.py:534 msgid "Please enter Bind DN: {}" msgstr "请输入绑定DN: {}" -#: settings/utils/ldap.py:518 +#: settings/utils/ldap.py:536 msgid "Please enter Password: {}" msgstr "请输入密码: {}" -#: settings/utils/ldap.py:520 +#: settings/utils/ldap.py:538 msgid "Please enter correct Bind DN and Password: {}" msgstr "请输入正确的绑定DN和密码: {}" -#: settings/utils/ldap.py:538 +#: settings/utils/ldap.py:556 msgid "Invalid User OU or User search filter: {}" msgstr "不合法的用户OU或用户过滤器: {}" -#: settings/utils/ldap.py:569 +#: settings/utils/ldap.py:587 msgid "LDAP User attr map not include: {}" msgstr "LDAP属性映射没有包含: {}" -#: settings/utils/ldap.py:576 +#: settings/utils/ldap.py:594 msgid "LDAP User attr map is not dict" msgstr "LDAP属性映射不合法" -#: settings/utils/ldap.py:595 +#: settings/utils/ldap.py:613 msgid "LDAP authentication is not enabled" msgstr "LDAP认证没有启用" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:631 msgid "Error (Invalid LDAP server): {}" msgstr "错误 (不合法的LDAP服务器地址): {}" -#: settings/utils/ldap.py:615 +#: settings/utils/ldap.py:633 msgid "Error (Invalid Bind DN): {}" msgstr "错误 (不合法的绑定DN): {}" -#: settings/utils/ldap.py:617 +#: settings/utils/ldap.py:635 msgid "Error (Invalid LDAP User attr map): {}" msgstr "错误 (不合法的LDAP属性映射): {}" -#: settings/utils/ldap.py:619 +#: settings/utils/ldap.py:637 msgid "Error (Invalid User OU or User search filter): {}" msgstr "错误 (不合法的用户OU或用户过滤器): {}" -#: settings/utils/ldap.py:621 +#: settings/utils/ldap.py:639 msgid "Error (Not enabled LDAP authentication): {}" msgstr "错误 (没有启用LDAP认证): {}" -#: settings/utils/ldap.py:623 +#: settings/utils/ldap.py:641 msgid "Error (Unknown): {}" msgstr "错误 (未知): {}" -#: settings/utils/ldap.py:626 +#: settings/utils/ldap.py:644 msgid "Succeed: Match {} s user" msgstr "成功匹配 {} 个用户" -#: settings/utils/ldap.py:659 +#: settings/utils/ldap.py:677 msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败 (配置错误): {}" -#: settings/utils/ldap.py:663 +#: settings/utils/ldap.py:681 msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" -#: settings/utils/ldap.py:665 +#: settings/utils/ldap.py:683 msgid "Authentication failed (Unknown): {}" msgstr "认证失败: (未知): {}" -#: settings/utils/ldap.py:668 +#: settings/utils/ldap.py:686 msgid "Authentication success: {}" msgstr "认证成功: {}" @@ -5946,7 +6042,7 @@ msgstr "命令存储" msgid "Invalid" msgstr "无效" -#: terminal/api/component/storage.py:119 terminal/tasks.py:141 +#: terminal/api/component/storage.py:119 terminal/tasks.py:142 msgid "Test failure: {}" msgstr "测试失败: {}" @@ -5955,7 +6051,7 @@ msgid "Test successful" msgstr "测试成功" #: terminal/api/component/storage.py:124 terminal/notifications.py:240 -#: terminal/tasks.py:145 +#: terminal/tasks.py:146 msgid "Test failure: Account invalid" msgstr "测试失败: 账号无效" @@ -5968,15 +6064,15 @@ msgstr "有在线会话" msgid "User %s %s session %s replay" msgstr "用户 %s %s 了会话 %s 的录像" -#: terminal/api/session/session.py:292 +#: terminal/api/session/session.py:297 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session/session.py:295 +#: terminal/api/session/session.py:300 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session/session.py:308 +#: terminal/api/session/session.py:313 msgid "User does not have permission" msgstr "用户没有权限" @@ -6067,7 +6163,7 @@ msgid "SFTP" msgstr "SFTP" #: terminal/const.py:89 -msgid "Read Only" +msgid "Read only" msgstr "只读" #: terminal/const.py:90 @@ -6075,17 +6171,29 @@ msgid "Writable" msgstr "读写" #: terminal/const.py:94 -msgid "Kill Session" +msgid "Kill session" msgstr "终断会话" #: terminal/const.py:95 -msgid "Lock Session" +msgid "Lock session" msgstr "锁定会话" #: terminal/const.py:96 -msgid "Unlock Session" +msgid "Unlock session" msgstr "解锁会话" +#: terminal/const.py:101 +msgid "Replay create failed" +msgstr "录像创建失败" + +#: terminal/const.py:102 +msgid "Replay upload failed" +msgstr "录像上传失败" + +#: terminal/const.py:103 +msgid "Replay convert failed" +msgstr "录像转码失败" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" @@ -6138,7 +6246,7 @@ msgstr "只支持自定义平台" msgid "Missing type in platform.yml" msgstr "在 platform.yml 中缺少类型" -#: terminal/models/applet/applet.py:300 terminal/models/applet/host.py:35 +#: terminal/models/applet/applet.py:302 terminal/models/applet/host.py:35 #: terminal/models/applet/host.py:137 msgid "Hosting" msgstr "宿主机" @@ -6309,27 +6417,31 @@ msgstr "回放" msgid "Date end" msgstr "结束日期" -#: terminal/models/session/session.py:47 terminal/serializers/session.py:62 +#: terminal/models/session/session.py:47 terminal/serializers/session.py:65 msgid "Command amount" msgstr "命令数量" -#: terminal/models/session/session.py:281 +#: terminal/models/session/session.py:48 terminal/serializers/session.py:28 +msgid "Error reason" +msgstr "错误原因" + +#: terminal/models/session/session.py:282 msgid "Session record" msgstr "会话记录" -#: terminal/models/session/session.py:283 +#: terminal/models/session/session.py:284 msgid "Can monitor session" msgstr "可以监控会话" -#: terminal/models/session/session.py:284 +#: terminal/models/session/session.py:285 msgid "Can share session" msgstr "可以分享会话" -#: terminal/models/session/session.py:285 +#: terminal/models/session/session.py:286 msgid "Can terminate session" msgstr "可以终断会话" -#: terminal/models/session/session.py:286 +#: terminal/models/session/session.py:287 msgid "Can validate session action perm" msgstr "可以验证会话动作权限" @@ -6578,31 +6690,31 @@ msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" msgid "Asset IP" msgstr "资产 IP" -#: terminal/serializers/session.py:23 terminal/serializers/session.py:47 +#: terminal/serializers/session.py:23 terminal/serializers/session.py:50 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:24 terminal/serializers/session.py:48 +#: terminal/serializers/session.py:24 terminal/serializers/session.py:51 msgid "Can join" msgstr "是否可加入" -#: terminal/serializers/session.py:25 terminal/serializers/session.py:51 +#: terminal/serializers/session.py:25 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "是否可中断" -#: terminal/serializers/session.py:43 +#: terminal/serializers/session.py:46 msgid "User ID" msgstr "用户 ID" -#: terminal/serializers/session.py:44 +#: terminal/serializers/session.py:47 msgid "Asset ID" msgstr "资产 ID" -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:48 msgid "Login from display" msgstr "登录来源名称" -#: terminal/serializers/session.py:52 +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "终端显示" @@ -6688,23 +6800,23 @@ msgstr "周期清理终端状态" msgid "Clean orphan session" msgstr "清除离线会话" -#: terminal/tasks.py:61 +#: terminal/tasks.py:62 msgid "Upload session replay to external storage" msgstr "上传会话录像到外部存储" -#: terminal/tasks.py:90 +#: terminal/tasks.py:91 msgid "Run applet host deployment" msgstr "运行应用机部署" -#: terminal/tasks.py:100 +#: terminal/tasks.py:101 msgid "Install applet" msgstr "安装应用" -#: terminal/tasks.py:111 +#: terminal/tasks.py:112 msgid "Generate applet host accounts" msgstr "收集远程应用上的账号" -#: terminal/tasks.py:123 +#: terminal/tasks.py:124 msgid "Check command replay storage connectivity" msgstr "检查命令及录像存储可连接性 " @@ -6731,6 +6843,10 @@ msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听 msgid "All available port count: {}, Already use port count: {}" msgstr "所有可用端口数量:{},已使用端口数量:{}" +#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:288 +msgid "Applicant" +msgstr "申请人" + #: tickets/apps.py:7 msgid "Tickets" msgstr "工单管理" @@ -6759,10 +6875,6 @@ msgstr "已拒绝" msgid "Closed" msgstr "关闭的" -#: tickets/const.py:46 -msgid "Approve" -msgstr "同意" - #: tickets/const.py:50 msgid "One level" msgstr "1 级" @@ -6893,6 +7005,10 @@ msgstr "运行的命令" msgid "Command filter acl" msgstr "命令过滤器" +#: tickets/models/ticket/command_confirm.py:23 +msgid "Apply Command Ticket" +msgstr "命令复核工单" + #: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "工单步骤" @@ -6905,10 +7021,6 @@ msgstr "工单受理人" msgid "Title" msgstr "标题" -#: tickets/models/ticket/general.py:288 -msgid "Applicant" -msgstr "申请人" - #: tickets/models/ticket/general.py:292 msgid "TicketFlow" msgstr "工单流程" @@ -6941,10 +7053,18 @@ msgstr "登录资产" msgid "Login account" msgstr "登录账号" +#: tickets/models/ticket/login_asset_confirm.py:27 +msgid "Apply Login Asset Ticket" +msgstr "资产登录复核工单" + #: tickets/models/ticket/login_confirm.py:12 msgid "Login datetime" msgstr "登录日期" +#: tickets/models/ticket/login_confirm.py:15 +msgid "Apply Login Ticket" +msgstr "用户登录复核工单" + #: tickets/notifications.py:63 msgid "Ticket basic info" msgstr "工单基本信息" @@ -7031,36 +7151,32 @@ msgid "Ticket information" msgstr "工单信息" #: tickets/templates/tickets/approve_check_password.html:29 -#: tickets/views/approve.py:40 +#: tickets/views/approve.py:40 tickets/views/approve.py:77 msgid "Ticket approval" msgstr "工单审批" -#: tickets/templates/tickets/approve_check_password.html:45 +#: tickets/templates/tickets/approve_check_password.html:44 msgid "Approval" msgstr "同意" -#: tickets/templates/tickets/approve_check_password.html:54 -msgid "Go Login" -msgstr "去登录" - #: tickets/views/approve.py:41 msgid "" "This ticket does not exist, the process has ended, or this link has expired" msgstr "工单不存在,或者工单流程已经结束,或者此链接已经过期" -#: tickets/views/approve.py:70 +#: tickets/views/approve.py:69 msgid "Click the button below to approve or reject" msgstr "点击下方按钮同意或者拒绝" -#: tickets/views/approve.py:72 +#: tickets/views/approve.py:78 msgid "After successful authentication, this ticket can be approved directly" msgstr "认证成功后,工单可直接审批" -#: tickets/views/approve.py:97 +#: tickets/views/approve.py:95 msgid "Illegal approval action" msgstr "无效的审批动作" -#: tickets/views/approve.py:110 +#: tickets/views/approve.py:108 msgid "This user is not authorized to approve this ticket" msgstr "此用户无权审批此工单" @@ -7203,7 +7319,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:173 users/models/user.py:831 +#: users/forms/profile.py:173 users/models/user.py:825 #: xpack/plugins/cloud/serializers/account_attrs.py:203 msgid "Public key" msgstr "SSH公钥" @@ -7212,73 +7328,77 @@ msgstr "SSH公钥" msgid "Preference" msgstr "用户设置" -#: users/models/user.py:648 users/serializers/profile.py:94 +#: users/models/user.py:642 users/serializers/profile.py:94 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:810 users/serializers/user.py:172 +#: users/models/user.py:804 users/serializers/user.py:169 msgid "Is service account" msgstr "服务账号" -#: users/models/user.py:812 +#: users/models/user.py:806 msgid "Avatar" msgstr "头像" -#: users/models/user.py:815 +#: users/models/user.py:809 msgid "Wechat" msgstr "微信" -#: users/models/user.py:818 users/serializers/user.py:109 +#: users/models/user.py:812 users/serializers/user.py:106 msgid "Phone" msgstr "手机" -#: users/models/user.py:824 +#: users/models/user.py:818 msgid "OTP secret key" msgstr "OTP 密钥" -#: users/models/user.py:828 +#: users/models/user.py:822 #: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "Private key" msgstr "ssh私钥" -#: users/models/user.py:836 users/serializers/profile.py:125 -#: users/serializers/user.py:169 +#: users/models/user.py:830 users/serializers/profile.py:125 +#: users/serializers/user.py:166 msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:850 +#: users/models/user.py:840 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:853 +#: users/models/user.py:843 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:977 +#: users/models/user.py:845 +msgid "Date api key used" +msgstr "Api key 最后使用日期" + +#: users/models/user.py:968 msgid "Can not delete admin user" msgstr "无法删除管理员用户" -#: users/models/user.py:1003 +#: users/models/user.py:994 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:1004 +#: users/models/user.py:995 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:1005 +#: users/models/user.py:996 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:1014 +#: users/models/user.py:1005 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:1017 +#: users/models/user.py:1008 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:1042 +#: users/models/user.py:1033 msgid "User password history" msgstr "用户密码历史" @@ -7289,7 +7409,7 @@ msgstr "用户密码历史" msgid "Reset password" msgstr "重置密码" -#: users/notifications.py:85 users/views/profile/reset.py:194 +#: users/notifications.py:85 users/views/profile/reset.py:230 msgid "Reset password success" msgstr "重置密码成功" @@ -7317,15 +7437,19 @@ msgstr "重置 MFA" msgid "File name conflict resolution" msgstr "文件名冲突解决方案" -#: users/serializers/preference/lina.py:11 +#: users/serializers/preference/koko.py:14 +msgid "Terminal theme name" +msgstr "终端主题名称" + +#: users/serializers/preference/lina.py:13 msgid "New file encryption password" msgstr "文件加密密码" -#: users/serializers/preference/lina.py:16 +#: users/serializers/preference/lina.py:18 msgid "Confirm file encryption password" msgstr "确认文件加密密码" -#: users/serializers/preference/lina.py:24 users/serializers/profile.py:48 +#: users/serializers/preference/lina.py:31 users/serializers/profile.py:48 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -7405,7 +7529,7 @@ msgstr "强制 MFA" msgid "Login blocked" msgstr "登录被锁定" -#: users/serializers/user.py:99 users/serializers/user.py:178 +#: users/serializers/user.py:99 users/serializers/user.py:175 msgid "Is OTP bound" msgstr "是否绑定了虚拟 MFA" @@ -7413,27 +7537,27 @@ msgstr "是否绑定了虚拟 MFA" msgid "Can public key authentication" msgstr "可以使用公钥认证" -#: users/serializers/user.py:173 +#: users/serializers/user.py:170 msgid "Is org admin" msgstr "组织管理员" -#: users/serializers/user.py:175 +#: users/serializers/user.py:172 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:179 +#: users/serializers/user.py:176 msgid "MFA level" msgstr "MFA 级别" -#: users/serializers/user.py:285 +#: users/serializers/user.py:282 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:286 +#: users/serializers/user.py:283 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:319 +#: users/serializers/user.py:316 msgid "name not unique" msgstr "名称重复" @@ -7445,26 +7569,30 @@ msgid "" msgstr "" "管理员已开启'仅允许已存在用户登录',当前用户不在用户列表中,请联系管理员。" -#: users/tasks.py:23 +#: users/tasks.py:25 msgid "Check password expired" msgstr "校验密码已过期" -#: users/tasks.py:37 +#: users/tasks.py:39 msgid "Periodic check password expired" msgstr "周期校验密码过期" -#: users/tasks.py:51 +#: users/tasks.py:53 msgid "Check user expired" msgstr "校验用户已过期" -#: users/tasks.py:68 +#: users/tasks.py:70 msgid "Periodic check user expired" msgstr "周期检测用户过期" -#: users/tasks.py:82 +#: users/tasks.py:84 msgid "Check unused users" msgstr "校验用户已过期" +#: users/tasks.py:114 +msgid "The user has not logged in recently and has been disabled." +msgstr "该用户最近未登录,已被禁用。" + #: users/templates/users/_msg_account_expire_reminder.html:7 msgid "Your account will expire in" msgstr "您的账号即将过期" @@ -7685,29 +7813,29 @@ msgstr "MFA(OTP) 禁用成功,返回登录页面" msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/profile/reset.py:47 +#: users/views/profile/reset.py:64 msgid "" "Non-local users can log in only from third-party platforms and cannot change " "their passwords: {}" msgstr "非本地用户仅允许从第三方平台登录,不支持修改密码: {}" -#: users/views/profile/reset.py:149 users/views/profile/reset.py:160 +#: users/views/profile/reset.py:185 users/views/profile/reset.py:196 msgid "Token invalid or expired" msgstr "令牌错误或失效" -#: users/views/profile/reset.py:165 +#: users/views/profile/reset.py:201 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/views/profile/reset.py:172 +#: users/views/profile/reset.py:208 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/views/profile/reset.py:178 +#: users/views/profile/reset.py:214 msgid "* The new password cannot be the last {} passwords" msgstr "* 新密码不能是最近 {} 次的密码" -#: users/views/profile/reset.py:195 +#: users/views/profile/reset.py:231 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" @@ -7835,7 +7963,7 @@ msgstr "已同步" msgid "Released" msgstr "已释放" -#: xpack/plugins/cloud/manager.py:53 +#: xpack/plugins/cloud/manager.py:54 msgid "Account unavailable" msgstr "账号无效" @@ -7928,16 +8056,16 @@ msgid "Task strategy" msgstr "任务策略" #: xpack/plugins/cloud/models.py:285 -msgid "Exact" -msgstr "" +msgid "Equal" +msgstr "等于" #: xpack/plugins/cloud/models.py:286 -msgid "Not" -msgstr "否" +msgid "Not Equal" +msgstr "不等于" #: xpack/plugins/cloud/models.py:287 msgid "In" -msgstr "在..里面" +msgstr "在...中" #: xpack/plugins/cloud/models.py:288 msgid "Contains" @@ -7945,11 +8073,11 @@ msgstr "包含" #: xpack/plugins/cloud/models.py:289 msgid "Startswith" -msgstr "以..开头" +msgstr "以...开头" #: xpack/plugins/cloud/models.py:290 msgid "Endswith" -msgstr "以..结尾" +msgstr "以...结尾" #: xpack/plugins/cloud/models.py:296 msgid "Instance platform" @@ -8342,7 +8470,7 @@ msgstr "许可证导入成功" msgid "License is invalid" msgstr "无效的许可证" -#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:140 +#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:141 msgid "License" msgstr "许可证" @@ -8361,6 +8489,3 @@ msgstr "企业专业版" #: xpack/plugins/license/models.py:86 msgid "Ultimate edition" msgstr "企业旗舰版" - -#~ msgid "Random" -#~ msgstr "随机" diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index b07876224..1426ffc01 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -1,6 +1,7 @@ # ~*~ coding: utf-8 ~*~ import json import os +import re from collections import defaultdict from django.utils.translation import gettext as _ @@ -77,9 +78,11 @@ class JMSInventory: return var @staticmethod - def make_custom_become_ansible_vars(account, platform): + def make_custom_become_ansible_vars(account, su_from_auth): + su_method = su_from_auth['ansible_become_method'] var = { - 'custom_become': True, 'custom_become_method': platform.su_method, + 'custom_become': True, + 'custom_become_method': su_method, 'custom_become_user': account.su_from.username, 'custom_become_password': account.su_from.secret, 'custom_become_private_key_path': account.su_from.private_key_path @@ -98,16 +101,9 @@ class JMSInventory: su_from = account.su_from if platform.su_enabled and su_from: - host.update(self.make_account_ansible_vars(su_from)) - host.update(self.make_custom_become_ansible_vars(account, platform)) - become_method = 'sudo' if platform.su_method != 'su' else 'su' - host['ansible_become'] = True - host['ansible_become_method'] = 'sudo' - host['ansible_become_user'] = account.username - if become_method == 'sudo': - host['ansible_become_password'] = su_from.secret - else: - host['ansible_become_password'] = account.secret + su_from_auth = account.get_ansible_become_auth() + host.update(su_from_auth) + host.update(self.make_custom_become_ansible_vars(account, su_from_auth)) elif platform.su_enabled and not su_from and \ self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account): host.update(self.make_account_ansible_vars(account)) @@ -164,7 +160,7 @@ class JMSInventory: protocol = self.get_primary_protocol(ansible_config, protocols) tp, category = asset.type, asset.category - name = asset.name.replace(' ', '_').replace('[', '_').replace(']', '_') + name = re.sub(r'[ \[\]/]', '_', asset.name) secret_info = {k: v for k, v in asset.secret_info.items() if v} host = { 'name': name, diff --git a/apps/ops/ansible/modules_utils/custom_common.py b/apps/ops/ansible/modules_utils/custom_common.py index 0eb454e5a..920ba6942 100644 --- a/apps/ops/ansible/modules_utils/custom_common.py +++ b/apps/ops/ansible/modules_utils/custom_common.py @@ -63,13 +63,13 @@ class SSHClient: @staticmethod def _is_match_user(user, content): # 正常命令切割后是[命令,用户名,交互前缀] - remote_user = content.split()[1] if len(content.split()) >= 3 else None - return remote_user and remote_user == user + content_list = content.split() if len(content.split()) >= 3 else None + return content_list and user in content_list def switch_user(self): self._get_channel() if not self.module.params['become']: - return None + return method = self.module.params['become_method'] username = self.module.params['login_user'] if method == 'sudo': @@ -85,7 +85,7 @@ class SSHClient: su_output, err_msg = self.execute(commands) if err_msg: return err_msg - i_output, err_msg = self.execute(['whoami']) + i_output, err_msg = self.execute(['whoami'], delay_time=1) if err_msg: return err_msg @@ -153,14 +153,14 @@ class SSHClient: output = self.channel.recv(size).decode(encoding) return output - def execute(self, commands): + def execute(self, commands, delay_time=0.3): if not self.is_connect: self.connect() output, error_msg = '', '' try: for command in commands: self.channel.send(command + '\n') - time.sleep(0.3) + time.sleep(delay_time) output = self._get_recv() except Exception as e: error_msg = str(e) @@ -170,5 +170,5 @@ class SSHClient: try: self.channel.close() self.client.close() - except: + except Exception: pass diff --git a/apps/ops/migrations/0027_alter_celerytaskexecution_options.py b/apps/ops/migrations/0027_alter_celerytaskexecution_options.py new file mode 100644 index 000000000..0b3e07308 --- /dev/null +++ b/apps/ops/migrations/0027_alter_celerytaskexecution_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.10 on 2023-09-22 06:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0026_auto_20230810_1039'), + ] + + operations = [ + migrations.AlterModelOptions( + name='celerytaskexecution', + options={'ordering': ['-date_start'], 'verbose_name': 'Celery Task Execution'}, + ), + ] diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py index e0f566ff1..f17ffe685 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # -import uuid import os +import uuid -from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.db import models +from django.utils.translation import gettext_lazy as _ from ops.celery import app @@ -83,4 +83,5 @@ class CeleryTaskExecution(models.Model): return "{}: {}".format(self.name, self.id) class Meta: + ordering = ['-date_start'] verbose_name = _("Celery Task Execution") diff --git a/apps/rbac/const.py b/apps/rbac/const.py index fac97b5db..01c8bd915 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -79,7 +79,6 @@ exclude_permissions = ( ('rbac', 'systemrolebinding', 'change', 'systemrolebinding'), ('rbac', 'orgrolebinding', 'change', 'orgrolebinding'), ('rbac', 'menupermission', '*', 'menupermission'), - ('rbac', 'role', '*', '*'), ('ops', 'adhocexecution', 'view,add,delete,change', '*'), ('ops', 'jobexecution', 'change,delete', 'jobexecution'), ('ops', 'historicaljob', '*', '*'), @@ -120,8 +119,6 @@ exclude_permissions = ( ('terminal', 'sessionsharing', 'view,add,change,delete', 'sessionsharing'), ('terminal', 'session', 'delete,share', 'session'), ('terminal', 'session', 'delete,change', 'command'), - ('terminal', 'appletpublication', '*', '*'), - ('terminal', 'applethostdeployment', '*', '*'), ('applications', '*', '*', '*'), ) @@ -132,7 +129,7 @@ only_system_permissions = ( ('rbac', 'systemrole', '*', '*'), ('rbac', 'rolebinding', '*', '*'), ('rbac', 'systemrolebinding', '*', '*'), - ('rbac', 'orgrole', 'delete,add,change', '*'), + ('rbac', 'orgrole', 'delete,add,change', 'orgrole'), ('orgs', 'organization', '*', '*'), ('xpack', 'license', '*', '*'), ('settings', 'setting', '*', '*'), @@ -153,6 +150,8 @@ only_system_permissions = ( ('orgs', 'organization', 'view', 'rootorg'), ('terminal', 'applet', '*', '*'), ('terminal', 'applethost', '*', '*'), + ('terminal', 'appletpublication', '*', '*'), + ('terminal', 'applethostdeployment', '*', '*'), ('acls', 'loginacl', '*', '*'), ('acls', 'connectmethodacl', '*', '*') ) diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index d608a869f..ef1571a1f 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -75,6 +75,8 @@ special_pid_mapper = { 'xpack.strategyrule': 'cloud_import', 'terminal.applet': 'remote_application', 'terminal.applethost': 'remote_application', + 'terminal.appletpublication': 'remote_application', + 'terminal.applethostdeployment': 'remote_application', 'accounts.accountbackupautomation': "backup_account_node", 'accounts.accountbackupexecution': "backup_account_node", "accounts.pushaccountautomation": "push_account_node", diff --git a/apps/settings/api/public.py b/apps/settings/api/public.py index f2628a5d4..81abfeb99 100644 --- a/apps/settings/api/public.py +++ b/apps/settings/api/public.py @@ -2,10 +2,9 @@ from django.conf import settings from rest_framework import generics from rest_framework.permissions import AllowAny -from common.permissions import IsValidUserOrConnectionToken +from authentication.permissions import IsValidUserOrConnectionToken from common.utils import get_logger, lazyproperty from common.utils.timezone import local_now -from jumpserver.utils import has_valid_xpack_license, get_xpack_license_info from .. import serializers from ..utils import get_interface_setting_or_default @@ -36,8 +35,8 @@ class PublicSettingApi(OpenPublicSettingApi): def get_object(self): values = super().get_object() values.update({ - "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(), - "XPACK_LICENSE_INFO": get_xpack_license_info(), + "XPACK_LICENSE_IS_VALID": settings.XPACK_LICENSE_IS_VALID, + "XPACK_LICENSE_INFO": settings.XPACK_LICENSE_INFO, "PASSWORD_RULE": { 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH, diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index 4a8f6cc3d..6b5f096cf 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -64,7 +64,15 @@ class SettingsApi(generics.RetrieveUpdateAPIView): rbac_category_permissions = { 'basic': 'settings.view_setting', 'terminal': 'settings.change_terminal', + 'ops': 'settings.change_ops', + 'ticket': 'settings.change_ticket', + 'announcement': 'settings.change_announcement', 'security': 'settings.change_security', + 'security_basic': 'settings.change_security', + 'security_auth': 'settings.change_security', + 'security_session': 'settings.change_security', + 'security_password': 'settings.change_security', + 'security_login_limit': 'settings.change_security', 'ldap': 'settings.change_auth', 'email': 'settings.change_email', 'email_content': 'settings.change_email', diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py index ec767828d..ecf7bd343 100644 --- a/apps/settings/api/sms.py +++ b/apps/settings/api/sms.py @@ -2,6 +2,7 @@ import importlib from collections import OrderedDict from django.utils.translation import gettext_lazy as _ +from django.conf import settings from rest_framework import status from rest_framework.exceptions import APIException from rest_framework.generics import ListAPIView, GenericAPIView @@ -28,7 +29,6 @@ class SMSBackendAPI(ListAPIView): } for b in BACKENDS.choices ] - return Response(data) @@ -39,11 +39,16 @@ class SMSTestingAPI(GenericAPIView): 'huawei': serializers.HuaweiSMSSettingSerializer, 'cmpp2': serializers.CMPP2SMSSettingSerializer, 'custom': serializers.CustomSMSSettingSerializer, + 'custom_file': serializers.BaseSMSSettingSerializer, } rbac_perms = { 'POST': 'settings.change_sms' } + @property + def test_code(self): + return '6' * settings.SMS_CODE_LENGTH + @staticmethod def get_or_from_setting(key, value=''): if not value: @@ -63,7 +68,7 @@ class SMSTestingAPI(GenericAPIView): send_sms_params = { 'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'], 'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'], - 'template_param': {'code': '666666'} + 'template_param': {'code': self.test_code} } return init_params, send_sms_params @@ -78,7 +83,7 @@ class SMSTestingAPI(GenericAPIView): send_sms_params = { 'sign_name': data['TENCENT_VERIFY_SIGN_NAME'], 'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'], - 'template_param': OrderedDict(code='666666') + 'template_param': OrderedDict(code=self.test_code) } return init_params, send_sms_params @@ -94,7 +99,7 @@ class SMSTestingAPI(GenericAPIView): send_sms_params = { 'sign_name': data['HUAWEI_VERIFY_SIGN_NAME'], 'template_code': data['HUAWEI_VERIFY_TEMPLATE_CODE'], - 'template_param': OrderedDict(code='666666') + 'template_param': OrderedDict(code=self.test_code) } return init_params, send_sms_params @@ -110,16 +115,18 @@ class SMSTestingAPI(GenericAPIView): send_sms_params = { 'sign_name': data['CMPP2_VERIFY_SIGN_NAME'], 'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'], - 'template_param': OrderedDict(code='666666') + 'template_param': OrderedDict(code=self.test_code) } return init_params, send_sms_params - @staticmethod - def get_custom_params(data): + def get_custom_params(self, data): init_params = {} - send_sms_params = {'template_param': OrderedDict(code='666666')} + send_sms_params = {'template_param': OrderedDict(code=self.test_code)} return init_params, send_sms_params + def get_custom_file_params(self, data): + return self.get_custom_params(data) + def get_params_by_backend(self, backend, data): """ 返回两部分参数 diff --git a/apps/settings/migrations/0010_alter_setting_options.py b/apps/settings/migrations/0010_alter_setting_options.py new file mode 100644 index 000000000..3d52a64da --- /dev/null +++ b/apps/settings/migrations/0010_alter_setting_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.10 on 2023-10-19 07:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0009_alter_cas_username_attribute'), + ] + + operations = [ + migrations.AlterModelOptions( + name='setting', + options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_ops', 'Can change auth ops'), ('change_ticket', 'Can change auth ticket'), ('change_announcement', 'Can change auth announcement'), ('change_vault', 'Can change vault setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'}, + ), + ] diff --git a/apps/settings/models.py b/apps/settings/models.py index 6aebf79b1..dc599bbe4 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -159,6 +159,9 @@ class Setting(models.Model): permissions = [ ('change_email', _('Can change email setting')), ('change_auth', _('Can change auth setting')), + ('change_ops', _('Can change auth ops')), + ('change_ticket', _('Can change auth ticket')), + ('change_announcement', _('Can change auth announcement')), ('change_vault', _('Can change vault setting')), ('change_systemmsgsubscription', _('Can change system msg sub setting')), ('change_sms', _('Can change sms setting')), diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index 40881e52a..1011c9aa6 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -7,6 +7,7 @@ from common.serializers.fields import EncryptedField, PhoneField from common.validators import PhoneValidator __all__ = [ + 'BaseSMSSettingSerializer', 'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer', 'HuaweiSMSSettingSerializer', 'CMPP2SMSSettingSerializer', 'CustomSMSSettingSerializer', ] @@ -17,6 +18,9 @@ class SMSSettingSerializer(serializers.Serializer): SMS_BACKEND = serializers.ChoiceField( choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol') ) + SMS_CODE_LENGTH = serializers.IntegerField( + default=4, min_value=4, max_value=16, label=_('SMS code length') + ) class SignTmplPairSerializer(serializers.Serializer): @@ -102,12 +106,14 @@ class CustomSMSSettingSerializer(BaseSMSSettingSerializer): default=RequestType.get, choices=RequestType.choices, label=_("Request method") ) - @staticmethod - def validate(attrs): + def validate(self, attrs): need_params = {'{phone_numbers}', '{code}'} params = attrs.get('CUSTOM_SMS_API_PARAMS', {}) - if len(set(params.values()) & need_params) != len(need_params): - raise serializers.ValidationError( + # 这里用逗号分隔是保证需要的参数必须是完整的,不能分开在不同的参数中首位相连 + params_string = ','.join(params.values()) + for param in need_params: + if param not in params_string: + raise serializers.ValidationError( _('The value in the parameter must contain %s') % ','.join(need_params) ) return attrs diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py index eb1841561..e5f572e77 100644 --- a/apps/settings/serializers/cleaning.py +++ b/apps/settings/serializers/cleaning.py @@ -1,38 +1,41 @@ +from django.conf import settings from django.utils.translation import gettext_lazy as _ from rest_framework import serializers __all__ = ['CleaningSerializer'] +MIN_VALUE = 180 if settings.LIMIT_SUPER_PRIV else 1 + class CleaningSerializer(serializers.Serializer): PREFIX_TITLE = _('Period clean') LOGIN_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, + min_value=MIN_VALUE, max_value=9999, label=_("Login log keep days (day)"), ) TASK_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, + min_value=MIN_VALUE, max_value=9999, label=_("Task log keep days (day)"), ) OPERATE_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, + min_value=MIN_VALUE, max_value=9999, label=_("Operate log keep days (day)"), ) FTP_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, + min_value=MIN_VALUE, max_value=9999, label=_("FTP log keep days (day)"), ) CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, + min_value=MIN_VALUE, max_value=9999, label=_("Cloud sync record keep days (day)"), ) + ACTIVITY_LOG_KEEP_DAYS = serializers.IntegerField( + min_value=MIN_VALUE, max_value=9999, + label=_("Activity log keep days (day)"), + ) TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField( - min_value=1, max_value=99999, required=True, label=_('Session keep duration (day)'), + min_value=MIN_VALUE, max_value=99999, required=True, label=_('Session keep duration (day)'), help_text=_( 'Session, record, command will be delete if more than duration, only in database, OSS will not be affected.') ) - ACTIVITY_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, - label=_("Activity log keep days (day)"), - ) diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 7c9bf676a..1fca3be3d 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -128,7 +128,7 @@ class SecurityAuthSerializer(serializers.Serializer): required=False, max_length=16, label=_('OTP issuer name'), ) OTP_VALID_WINDOW = serializers.IntegerField( - min_value=1, max_value=10, + min_value=0, max_value=10, label=_("OTP valid window") ) SECURITY_MFA_VERIFY_TTL = serializers.IntegerField( diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index d130ac41a..d5fba8ce9 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -350,12 +350,12 @@ class LDAPSyncUtil(object): try: self.pre_sync() self.sync() - self.post_sync() except Exception as e: error_msg = str(e) logger.error(error_msg) self.set_task_error_msg(error_msg) finally: + self.post_sync() logger.info('End perform sync ldap users from server to cache') close_old_connections() @@ -426,7 +426,21 @@ class LDAPImportUtil(object): return errors @staticmethod - def bind_org(org, users, group_users_mapper): + def exit_user_group(user_groups_mapper): + # 通过对比查询本次导入用户需要移除的用户组 + group_remove_users_mapper = defaultdict(set) + for user, current_groups in user_groups_mapper.items(): + old_groups = set(user.groups.all()) + exit_groups = old_groups - current_groups + logger.debug(f'Ldap user {user} exits user groups {exit_groups}') + for g in exit_groups: + group_remove_users_mapper[g].add(user) + + # 根据用户组统一移除用户 + for g, rm_users in group_remove_users_mapper.items(): + g.users.remove(*rm_users) + + def bind_org(self, org, users, group_users_mapper): if not org: return if org.is_root(): @@ -436,11 +450,15 @@ class LDAPImportUtil(object): org.add_member(user) # add user to group with tmp_to_org(org): + user_groups_mapper = defaultdict(set) for group_name, users in group_users_mapper.items(): group, created = UserGroup.objects.get_or_create( name=group_name, defaults={'name': group_name} ) + for user in users: + user_groups_mapper[user].add(group) group.users.add(*users) + self.exit_user_group(user_groups_mapper) class LDAPTestUtil(object): diff --git a/apps/static/img/authenticator_iphone.png b/apps/static/img/authenticator_iphone.png index ed31d38b3..57f9e6910 100644 Binary files a/apps/static/img/authenticator_iphone.png and b/apps/static/img/authenticator_iphone.png differ diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index e69634564..e1e49f124 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -15,7 +15,7 @@ p {
-

JumpServer {% trans 'Client' %} v2.0.1

+

JumpServer {% trans 'Client' %} v2.0.2

{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}

diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index 90b4f162c..9e656487b 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -61,7 +61,7 @@ class DownloadUploadMixin: update = request.query_params.get('update') is_enterprise = manifest.get('edition') == Applet.Edition.enterprise - if is_enterprise and not settings.XPACK_ENABLED: + if is_enterprise and not settings.XPACK_LICENSE_IS_VALID: raise ValidationError({'error': _('This is enterprise edition applet')}) instance = Applet.objects.filter(name=name).first() diff --git a/apps/terminal/api/component/endpoint.py b/apps/terminal/api/component/endpoint.py index e2eeaf548..b40aba9aa 100644 --- a/apps/terminal/api/component/endpoint.py +++ b/apps/terminal/api/component/endpoint.py @@ -6,8 +6,8 @@ from rest_framework.request import Request from rest_framework.response import Response from assets.models import Asset +from authentication.permissions import IsValidUserOrConnectionToken from common.api import JMSBulkModelViewSet -from common.permissions import IsValidUserOrConnectionToken from orgs.utils import tmp_to_root_org from terminal import serializers from terminal.models import Session, Endpoint, EndpointRule diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 756fb5a91..c286dee83 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -92,7 +92,12 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): rbac_perms = { 'download': ['terminal.download_sessionreplay'], } - permission_classes = [RBACPermission | IsSessionAssignee] + permission_classes = [RBACPermission] + + def get_permissions(self): + if self.action == 'retrieve': + self.permission_classes = [RBACPermission | IsSessionAssignee] + return super().get_permissions() @staticmethod def prepare_offline_file(session, local_path): diff --git a/apps/terminal/api/session/task.py b/apps/terminal/api/session/task.py index f9dc8a2f2..fd09fb2ae 100644 --- a/apps/terminal/api/session/task.py +++ b/apps/terminal/api/session/task.py @@ -50,14 +50,13 @@ class TaskViewSet(JMSBulkModelViewSet): serializer.is_valid(raise_exception=True) session_id = serializer.validated_data['session_id'] task_name = serializer.validated_data['task_name'] - session_ids = [session_id, ] user_id = request.user.id - for session_id in session_ids: - if not is_session_approver(session_id, user_id): - return Response({}, status=status.HTTP_403_FORBIDDEN) - with tmp_to_root_org(): - validated_session = create_sessions_tasks(session_ids, request.user, task_name=task_name) + if not is_session_approver(session_id, user_id): + return Response({}, status=status.HTTP_403_FORBIDDEN) + + with tmp_to_root_org(): + validated_session = create_sessions_tasks([session_id], request.user, task_name=task_name) return Response({"ok": validated_session}) diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index 3f290a0c0..9c7ee989a 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -75,7 +75,7 @@ class NativeClient(TextChoices): xpack_protocols = Protocol.xpack_protocols() for protocol, _clients in clients_map.items(): - if not settings.XPACK_ENABLED and protocol in xpack_protocols: + if not settings.XPACK_LICENSE_IS_VALID and protocol in xpack_protocols: continue if isinstance(_clients, dict): if os == 'all': @@ -83,7 +83,7 @@ class NativeClient(TextChoices): else: _clients = _clients.get(os, _clients['default']) for client in _clients: - if not settings.XPACK_ENABLED and client in cls.xpack_methods(): + if not settings.XPACK_LICENSE_IS_VALID and client in cls.xpack_methods(): continue methods[protocol].append({ 'value': client.value, @@ -144,7 +144,7 @@ class ConnectMethodUtil: 'support': [ Protocol.mysql, Protocol.postgresql, Protocol.oracle, Protocol.sqlserver, - Protocol.mariadb + Protocol.mariadb, Protocol.db2 ], 'match': 'm2m' }, diff --git a/apps/terminal/const.py b/apps/terminal/const.py index c047343a4..9f0b8a947 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -86,11 +86,18 @@ class SessionType(TextChoices): class ActionPermission(TextChoices): - readonly = "readonly", _('Read Only') + readonly = "readonly", _('Read only') writable = "writable", _('Writable') class TaskNameType(TextChoices): - kill_session = "kill_session", _('Kill Session') - lock_session = "lock_session", _('Lock Session') - unlock_session = "unlock_session", _('Unlock Session') + kill_session = "kill_session", _('Kill session') + lock_session = "lock_session", _('Lock session') + unlock_session = "unlock_session", _('Unlock session') + + +class SessionErrorReason(TextChoices): + connect_failed = 'connect_failed', _('Connect failed') + replay_create_failed = 'replay_create_failed', _('Replay create failed') + replay_upload_failed = 'replay_upload_failed', _('Replay upload failed') + replay_convert_failed = 'replay_convert_failed', _('Replay convert failed') diff --git a/apps/terminal/migrations/0062_applet_edition.py b/apps/terminal/migrations/0062_applet_edition.py index bd5118edf..8ca93ce2f 100644 --- a/apps/terminal/migrations/0062_applet_edition.py +++ b/apps/terminal/migrations/0062_applet_edition.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='applet', name='edition', - field=models.CharField(choices=[('community', 'Community'), ('enterprise', 'Enterprise')], + field=models.CharField(choices=[('community', 'Community edition'), ('enterprise', 'Enterprise')], default='community', max_length=128, verbose_name='Edition'), ), ] diff --git a/apps/terminal/migrations/0064_auto_20230728_1001.py b/apps/terminal/migrations/0064_auto_20230728_1001.py index 900abb5ba..93af187c4 100644 --- a/apps/terminal/migrations/0064_auto_20230728_1001.py +++ b/apps/terminal/migrations/0064_auto_20230728_1001.py @@ -33,6 +33,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='task', name='name', - field=models.CharField(choices=[('kill_session', 'Kill Session'), ('lock_session', 'Lock Session'), ('unlock_session', 'Unlock Session')], max_length=128, verbose_name='Name'), + field=models.CharField(choices=[('kill_session', 'Kill session'), ('lock_session', 'Lock session'), ('unlock_session', 'Unlock session')], max_length=128, verbose_name='Name'), ), ] diff --git a/apps/terminal/migrations/0065_session_error_reason.py b/apps/terminal/migrations/0065_session_error_reason.py new file mode 100644 index 000000000..1a3bf8f67 --- /dev/null +++ b/apps/terminal/migrations/0065_session_error_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-10-10 06:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0064_auto_20230728_1001'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='error_reason', + field=models.CharField(blank=True, max_length=128, verbose_name='Error reason'), + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index 99f4e1689..23450aff3 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -163,7 +163,7 @@ class Applet(JMSBaseModel): counts[host_id] += 1 hosts = list(sorted(hosts, key=lambda h: counts[h.id])) - return hosts[0] + return hosts[0] if hosts else None def select_host(self, user, asset): hosts = self.hosts.filter(is_active=True) @@ -186,6 +186,8 @@ class Applet(JMSBaseModel): host = pref_host[0] else: host = self._select_by_load(hosts) + if host is None: + return cache.set(prefer_key, str(host.id), timeout=None) return host diff --git a/apps/terminal/models/session/session.py b/apps/terminal/models/session/session.py index 09f2df9e1..8530c3c63 100644 --- a/apps/terminal/models/session/session.py +++ b/apps/terminal/models/session/session.py @@ -45,6 +45,7 @@ class Session(OrgModelMixin): date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) cmd_amount = models.IntegerField(default=-1, verbose_name=_("Command amount")) + error_reason = models.CharField(max_length=128, blank=True, verbose_name=_("Error reason")) upload_to = 'replay' ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' diff --git a/apps/terminal/permissions.py b/apps/terminal/permissions.py index e2e72e572..288d839eb 100644 --- a/apps/terminal/permissions.py +++ b/apps/terminal/permissions.py @@ -9,7 +9,7 @@ __all__ = ['IsSessionAssignee'] class IsSessionAssignee(permissions.IsAuthenticated): def has_permission(self, request, view): - return False + return True def has_object_permission(self, request, view, obj): try: diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 6e5476c3e..ba8ecfdf8 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -5,7 +5,7 @@ from common.serializers.fields import LabeledChoiceField from common.utils import pretty_string from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .terminal import TerminalSmallSerializer -from ..const import SessionType +from ..const import SessionType, SessionErrorReason from ..models import Session __all__ = [ @@ -24,6 +24,9 @@ class SessionSerializer(BulkOrgResourceModelSerializer): can_join = serializers.BooleanField(read_only=True, label=_("Can join")) can_terminate = serializers.BooleanField(read_only=True, label=_("Can terminate")) asset = serializers.CharField(label=_("Asset"), style={'base_template': 'textarea.html'}) + error_reason = LabeledChoiceField( + choices=SessionErrorReason.choices, label=_("Error reason"), required=False + ) class Meta: model = Session @@ -33,7 +36,7 @@ class SessionSerializer(BulkOrgResourceModelSerializer): "protocol", 'type', "login_from", "remote_addr", "is_success", "is_finished", "has_replay", "has_command", "date_start", "date_end", "comment", "terminal_display", "is_locked", - 'command_amount', + 'command_amount', 'error_reason' ] fields_fk = ["terminal", ] fields_custom = ["can_replay", "can_join", "can_terminate"] diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 65fae3ded..38ca1941a 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -43,6 +43,7 @@ def delete_terminal_status_period(): @register_as_period_task(interval=600) @after_app_ready_start @after_app_shutdown_clean_periodic +@tmp_to_root_org() def clean_orphan_session(): active_sessions = Session.objects.filter(is_finished=False) for session in active_sessions: diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 1e44c3818..da60aefe8 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -6,9 +6,10 @@ from rest_framework.decorators import action from rest_framework.exceptions import MethodNotAllowed from rest_framework.response import Response +from audits.handler import create_or_update_operate_log from common.api import CommonApiMixin from common.const.http import POST, PUT, PATCH -from orgs.utils import tmp_to_root_org +from orgs.utils import tmp_to_root_org, tmp_to_org from rbac.permissions import RBACPermission from tickets import filters from tickets import serializers @@ -17,6 +18,7 @@ from tickets.models import ( ApplyLoginAssetTicket, ApplyCommandTicket ) from tickets.permissions.ticket import IsAssignee, IsApplicant +from ..const import TicketAction __all__ = [ 'TicketViewSet', 'ApplyAssetTicketViewSet', @@ -77,6 +79,21 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): with tmp_to_root_org(): return super().create(request, *args, **kwargs) + @staticmethod + def _record_operate_log(ticket, action): + with tmp_to_org(ticket.org_id): + after = { + 'ID': str(ticket.id), + str(_('Name')): ticket.title, + str(_('Applicant')): str(ticket.applicant), + } + object_name = ticket._meta.object_name + resource_type = ticket._meta.verbose_name + create_or_update_operate_log( + action, resource_type, resource=ticket, + after=after, object_name=object_name + ) + @action(detail=True, methods=[PUT, PATCH], permission_classes=[IsAssignee, ]) def approve(self, request, *args, **kwargs): self.ticket_not_allowed() @@ -88,18 +105,21 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): serializer.is_valid(raise_exception=True) instance = serializer.save() instance.approve(processor=request.user) + self._record_operate_log(instance, TicketAction.approve) return Response('ok') @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) def reject(self, request, *args, **kwargs): instance = self.get_object() instance.reject(processor=request.user) + self._record_operate_log(instance, TicketAction.reject) return Response('ok') @action(detail=True, methods=[PUT], permission_classes=[IsAssignee | IsApplicant, ]) def close(self, request, *args, **kwargs): instance = self.get_object() instance.close() + self._record_operate_log(instance, TicketAction.close) return Response('ok') @action(detail=False, methods=[PUT], permission_classes=[RBACPermission, ]) diff --git a/apps/tickets/models/ticket/command_confirm.py b/apps/tickets/models/ticket/command_confirm.py index f261bb147..62103eb1e 100644 --- a/apps/tickets/models/ticket/command_confirm.py +++ b/apps/tickets/models/ticket/command_confirm.py @@ -18,3 +18,6 @@ class ApplyCommandTicket(Ticket): 'acls.CommandFilterACL', on_delete=models.SET_NULL, null=True, verbose_name=_('Command filter acl') ) + + class Meta: + verbose_name = _('Apply Command Ticket') diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py index ffcbf869e..ce2f7f6fd 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -22,3 +22,6 @@ class ApplyLoginAssetTicket(Ticket): return self.connection_token.is_active = True self.connection_token.save() + + class Meta: + verbose_name = _('Apply Login Asset Ticket') diff --git a/apps/tickets/models/ticket/login_confirm.py b/apps/tickets/models/ticket/login_confirm.py index 024761ac7..7b17dd1f8 100644 --- a/apps/tickets/models/ticket/login_confirm.py +++ b/apps/tickets/models/ticket/login_confirm.py @@ -10,3 +10,6 @@ class ApplyLoginTicket(Ticket): apply_login_ip = models.GenericIPAddressField(verbose_name=_('Login IP'), null=True) apply_login_city = models.CharField(max_length=64, verbose_name=_('Login city'), null=True) apply_login_datetime = models.DateTimeField(verbose_name=_('Login datetime'), null=True) + + class Meta: + verbose_name = _('Apply Login Ticket') diff --git a/apps/tickets/templates/tickets/approve_check_password.html b/apps/tickets/templates/tickets/approve_check_password.html index 9fd7e0862..fe256bc1c 100644 --- a/apps/tickets/templates/tickets/approve_check_password.html +++ b/apps/tickets/templates/tickets/approve_check_password.html @@ -39,7 +39,6 @@
{% csrf_token %}
- {% if user.is_authenticated %} - {% else %} - - {% trans 'Go Login' %} - - {% endif %}
diff --git a/apps/tickets/views/approve.py b/apps/tickets/views/approve.py index 8dcda595f..2f3e715b4 100644 --- a/apps/tickets/views/approve.py +++ b/apps/tickets/views/approve.py @@ -64,30 +64,28 @@ class TicketDirectApproveView(TemplateView): def get_context_data(self, **kwargs): # 放入工单信息 - token = kwargs.get('token') - content = cache.get(token, {}).get('content', []) - if self.request.user.is_authenticated: - prompt_msg = _('Click the button below to approve or reject') - else: - prompt_msg = _('After successful authentication, this ticket can be approved directly') kwargs.update({ - 'content': content, 'prompt_msg': prompt_msg, - 'login_url': '%s&next=%s' % ( - self.login_url, - reverse('tickets:direct-approve', kwargs={'token': token}) - ), + 'content': kwargs['ticket_info'].get('content', []), + 'prompt_msg': _('Click the button below to approve or reject'), }) return super().get_context_data(**kwargs) def get(self, request, *args, **kwargs): if not request.user.is_authenticated: - return HttpResponse(status=401) + direct_url = reverse('tickets:direct-approve', kwargs={'token': kwargs['token']}) + message_data = { + 'title': _('Ticket approval'), + 'message': _('After successful authentication, this ticket can be approved directly'), + 'redirect_url': f'{self.login_url}&{self.redirect_field_name}={direct_url}', + 'auto_redirect': True, + } + redirect_url = FlashMessageUtil.gen_message_url(message_data) + return redirect(redirect_url) - token = kwargs.get('token') - ticket_info = cache.get(token) + ticket_info = cache.get(kwargs['token']) if not ticket_info: return self.redirect_message_response(redirect_url=self.login_url) - return super().get(request, *args, **kwargs) + return super().get(request, ticket_info=ticket_info, *args, **kwargs) def post(self, request, **kwargs): user = request.user diff --git a/apps/users/api/profile.py b/apps/users/api/profile.py index 727c87314..974fc136c 100644 --- a/apps/users/api/profile.py +++ b/apps/users/api/profile.py @@ -5,7 +5,7 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated from authentication.models import ConnectionToken -from common.permissions import IsValidUserOrConnectionToken +from authentication.permissions import IsValidUserOrConnectionToken from common.utils import get_object_or_none from orgs.utils import tmp_to_root_org from users.notifications import ( diff --git a/apps/users/migrations/0046_auto_20230927_1456.py b/apps/users/migrations/0046_auto_20230927_1456.py new file mode 100644 index 000000000..d32569026 --- /dev/null +++ b/apps/users/migrations/0046_auto_20230927_1456.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.10 on 2023-09-27 06:56 + +from django.db import migrations + + +def migrate_user_default_email(apps, *args): + user_cls = apps.get_model('users', 'User') + user_cls.objects\ + .filter(email='admin@mycomany.com')\ + .update(email='admin@example.com') + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0045_delete_usersession'), + ] + + operations = [ + migrations.RunPython(migrate_user_default_email), + ] diff --git a/apps/users/migrations/0047_user_date_api_key_last_used.py b/apps/users/migrations/0047_user_date_api_key_last_used.py new file mode 100644 index 000000000..21ccd13cb --- /dev/null +++ b/apps/users/migrations/0047_user_date_api_key_last_used.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-10-08 04:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0046_auto_20230927_1456'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='date_api_key_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date api key used'), + ), + ] diff --git a/apps/users/migrations/0048_wechat_phone_encrypt.py b/apps/users/migrations/0048_wechat_phone_encrypt.py new file mode 100644 index 000000000..7db87e09e --- /dev/null +++ b/apps/users/migrations/0048_wechat_phone_encrypt.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.10 on 2023-10-10 06:57 + +from django.db import migrations + +import common.db.fields + +users_bulked = [] + + +def get_encrypt_fields_value(apps, *args): + global users_bulked + user_model = apps.get_model('users', 'User') + bulk_size = 2000 + users = user_model.objects.all() + users_bulked = [ + users[i:i + bulk_size] + for i in range(0, users.count(), bulk_size) + ] + + +def migrate_encrypt_fields(apps, *args): + user_model = apps.get_model('users', 'User') + for _users in users_bulked: + user_model.objects.bulk_update(_users, ['phone', 'wechat']) + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0047_user_date_api_key_last_used'), + ] + + operations = [ + migrations.RunPython(get_encrypt_fields_value), + migrations.AlterField( + model_name='user', + name='wechat', + field=common.db.fields.EncryptCharField(blank=True, max_length=128, verbose_name='Wechat'), + ), + migrations.AlterField( + model_name='user', + name='phone', + field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True, verbose_name='Phone'), + ), + migrations.RunPython(migrate_encrypt_fields), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 98facb346..15054aa4f 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -606,7 +606,8 @@ class TokenMixin: def generate_reset_token(self): token = random_string(50) - self.set_cache(token) + key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) + cache.set(key, {'id': self.id, 'email': self.email}, 3600) return token @classmethod @@ -626,10 +627,6 @@ class TokenMixin: logger.error(e, exc_info=True) return None - def set_cache(self, token): - key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) - cache.set(key, {'id': self.id, 'email': self.email}, 3600) - @classmethod def expired_reset_password_token(cls, token): key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) @@ -808,11 +805,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract avatar = models.ImageField( upload_to="avatar", null=True, verbose_name=_('Avatar') ) - wechat = models.CharField( + wechat = fields.EncryptCharField( max_length=128, blank=True, verbose_name=_('Wechat') ) - phone = models.CharField( - max_length=20, blank=True, null=True, verbose_name=_('Phone') + phone = fields.EncryptCharField( + max_length=128, blank=True, null=True, verbose_name=_('Phone') ) mfa_level = models.SmallIntegerField( default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA') @@ -837,11 +834,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract ) created_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Created by')) updated_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Updated by')) - source = models.CharField( - max_length=30, default=Source.local, - choices=Source.choices, - verbose_name=_('Source') - ) + source = models.CharField(max_length=30, default=Source.local, choices=Source.choices, verbose_name=_('Source')) date_password_last_updated = models.DateTimeField( auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') @@ -849,6 +842,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract need_update_password = models.BooleanField( default=False, verbose_name=_('Need update password') ) + date_api_key_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date api key used')) date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom')) dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk')) diff --git a/apps/users/serializers/preference/koko.py b/apps/users/serializers/preference/koko.py index 5c7ac2e8d..12c497d47 100644 --- a/apps/users/serializers/preference/koko.py +++ b/apps/users/serializers/preference/koko.py @@ -9,6 +9,10 @@ class BasicSerializer(serializers.Serializer): FileNameConflictResolution.choices, default=FileNameConflictResolution.REPLACE, required=False, label=_('File name conflict resolution') ) + terminal_theme_name = serializers.CharField( + max_length=128, required=False, default='Default', + label=_('Terminal theme name') + ) class KokoSerializer(serializers.Serializer): diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 82ce8f0da..59ae59b52 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -101,10 +101,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer source="can_use_ssh_key_login", label=_("Can public key authentication"), read_only=True ) - password = EncryptedField( - label=_("Password"), required=False, allow_blank=True, - allow_null=True, max_length=1024, - ) + password = EncryptedField(label=_("Password"), required=False, allow_blank=True, allow_null=True, max_length=1024, ) phone = PhoneField( validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, label=_("Phone") ) @@ -128,8 +125,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer "created_by", "updated_by", "comment", # 通用字段 ] fields_date = [ - "date_expired", "date_joined", - "last_login", "date_updated" # 日期字段 + "date_expired", "date_joined", "last_login", + "date_updated", "date_api_key_last_used", ] fields_bool = [ "is_superuser", "is_org_admin", @@ -155,7 +152,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer read_only_fields = [ "date_joined", "last_login", "created_by", "is_first_login", "wecom_id", "dingtalk_id", - "feishu_id", + "feishu_id", "date_api_key_last_used", ] disallow_self_update_fields = ["is_active", "system_roles", "org_roles"] extra_kwargs = { @@ -296,8 +293,8 @@ class ServiceAccountSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - from authentication.serializers import AccessKeySerializer - self.fields["access_key"] = AccessKeySerializer(read_only=True) + from authentication.serializers import AccessKeyCreateSerializer + self.fields["access_key"] = AccessKeyCreateSerializer(read_only=True) def get_username(self): return self.initial_data.get("name") diff --git a/apps/users/tasks.py b/apps/users/tasks.py index add7a9ed7..ecc59c3bc 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- # +import uuid from datetime import timedelta -from celery import shared_task +from celery import shared_task, current_task from django.conf import settings +from django.db.models import Q from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _, gettext_noop +from audits.const import ActivityChoices from common.const.crontab import CRONTAB_AT_AM_TEN, CRONTAB_AT_PM_TWO from common.utils import get_logger -from common.utils.timezone import utc_now from ops.celery.decorator import after_app_ready_start, register_as_period_task from ops.celery.utils import create_or_update_celery_periodic_tasks from orgs.utils import tmp_to_root_org @@ -85,5 +87,29 @@ def check_user_expired_periodic(): def check_unused_users(): uncommon_users_ttl = settings.SECURITY_UNCOMMON_USERS_TTL seconds_to_subtract = uncommon_users_ttl * 24 * 60 * 60 - t = utc_now() - timedelta(seconds=seconds_to_subtract) - User.objects.filter(last_login__lte=t).update(is_active=False) + t = timezone.now() - timedelta(seconds=seconds_to_subtract) + last_login_q = Q(last_login__lte=t) | Q(last_login__isnull=True) + api_key_q = Q(date_api_key_last_used__lte=t) | Q(date_api_key_last_used__isnull=True) + + users = User.objects \ + .filter(date_joined__lt=t) \ + .filter(is_active=True) \ + .filter(last_login_q) \ + .filter(api_key_q) + + if not users: + return + print("Some users are not used for a long time, and they will be disabled.") + resource_ids = [] + for user in users: + resource_ids.append(user.id) + print(' - {}'.format(user.name)) + + users.update(is_active=False) + from audits.signal_handlers import create_activities + if current_task: + task_id = current_task.request.id + else: + task_id = str(uuid.uuid4()) + detail = gettext_noop('The user has not logged in recently and has been disabled.') + create_activities(resource_ids, detail, task_id, action=ActivityChoices.task, org_id='') diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index b8e18a647..96d407302 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals +import time + from django.conf import settings from django.core.cache import cache from django.shortcuts import redirect, reverse @@ -9,6 +11,7 @@ from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import FormView, RedirectView +from authentication.errors import IntervalTooShort from common.utils import FlashMessageUtil, get_object_or_none, random_string from common.utils.verify_code import SendAndVerifyCodeUtil from users.notifications import ResetPasswordSuccessMsg @@ -37,6 +40,20 @@ class UserForgotPasswordPreviewingView(FormView): def get_redirect_url(token): return reverse('authentication:forgot-password') + '?token=%s' % token + @staticmethod + def generate_previewing_token(user): + sent_ttl = 60 + token_sent_at_key = '{}_send_at'.format(user.username) + token_sent_at = cache.get(token_sent_at_key, 0) + + if token_sent_at: + raise IntervalTooShort(sent_ttl) + token = random_string(36) + user_info = {'username': user.username, 'phone': user.phone, 'email': user.email} + cache.set(token, user_info, 5 * 60) + cache.set(token_sent_at_key, time.time(), sent_ttl) + return token + def form_valid(self, form): username = form.cleaned_data['username'] user = get_object_or_none(User, username=username) @@ -49,9 +66,11 @@ class UserForgotPasswordPreviewingView(FormView): form.add_error('username', error) return super().form_invalid(form) - token = random_string(36) - user_map = {'username': user.username, 'phone': user.phone, 'email': user.email} - cache.set(token, user_map, 5 * 60) + try: + token = self.generate_previewing_token(user) + except IntervalTooShort as e: + form.add_error('username', e) + return super().form_invalid(form) return redirect(self.get_redirect_url(token)) @@ -71,7 +90,7 @@ class UserForgotPasswordView(FormView): @staticmethod def get_validate_backends_context(has_phone): validate_backends = [{'name': _('Email'), 'is_active': True, 'value': 'email'}] - if settings.XPACK_ENABLED: + if settings.XPACK_LICENSE_IS_VALID: if settings.SMS_ENABLED and has_phone: is_active = True else: @@ -103,25 +122,42 @@ class UserForgotPasswordView(FormView): reset_password_url = reverse('authentication:reset-password') return reset_password_url + query_params + @staticmethod + def safe_verify_code(token, target, form_type, code): + token_verified_key = '{}_verified'.format(token) + token_verified_times = cache.get(token_verified_key, 0) + + if token_verified_times >= 3: + cache.delete(token) + raise ValueError('Verification code has been used more than 3 times, please re-verify') + cache.set(token_verified_key, token_verified_times + 1, 5 * 60) + sender_util = SendAndVerifyCodeUtil(target, backend=form_type) + return sender_util.verify(code) + def form_valid(self, form): token = self.request.GET.get('token') - userinfo = cache.get(token) - if not userinfo: + user_info = cache.get(token) + if not user_info: return redirect(self.get_redirect_url(return_previewing=True)) - username = userinfo.get('username') + username = user_info.get('username') form_type = form.cleaned_data['form_type'] target = form.cleaned_data[form_type] code = form.cleaned_data['code'] + query_key = form_type + if form_type == 'sms': + query_key = 'phone' + target = target.lstrip('+') + try: - sender_util = SendAndVerifyCodeUtil(target, backend=form_type) - sender_util.verify(code) + self.safe_verify_code(token, target, form_type, code) + except ValueError as e: + return redirect(self.get_redirect_url(return_previewing=True)) except Exception as e: form.add_error('code', str(e)) return super().form_invalid(form) - query_key = 'phone' if form_type == 'sms' else form_type user = get_object_or_none(User, **{'username': username, query_key: target}) if not user: form.add_error('code', _('No user matched')) diff --git a/poetry.lock b/poetry.lock index d969ce3cf..674e0aa36 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "adal" @@ -2655,14 +2655,8 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" -grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] +grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -3345,12 +3339,12 @@ reference = "tsinghua" [[package]] name = "jms-storage" -version = "0.0.51" +version = "0.0.52" description = "Jumpserver storage python sdk tools" optional = false python-versions = "*" files = [ - {file = "jms-storage-0.0.51.tar.gz", hash = "sha256:47a50ac4d952a21693b0e2f926f42fa0d02bc1fa8e507a8284059743b2b81911"}, + {file = "jms-storage-0.0.52.tar.gz", hash = "sha256:15303281a1d1a3ac24a5a9fb0d78abda3aa1f752590aab867923647a485ccfbd"}, ] [package.dependencies] @@ -7276,4 +7270,4 @@ reference = "tsinghua" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b7d8e793f247e91e1bd22404559ed495e619fe691fa92712cacf7bd146c0eb8f" +content-hash = "bf72acdbac5e62c239033fd629835ad30788a3fcb07ac9a2dc2f15f321da6c30" diff --git a/pyproject.toml b/pyproject.toml index 160c0e974..5a1cd585b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ pynacl = "1.5.0" python-dateutil = "2.8.2" pyyaml = "6.0.1" requests = "2.31.0" -jms-storage = "0.0.51" +jms-storage = "0.0.52" simplejson = "3.19.1" six = "1.16.0" sshtunnel = "0.4.0" diff --git a/utils/add_china_db_platform.py b/utils/add_china_db_platform.py new file mode 100644 index 000000000..1a0d855bb --- /dev/null +++ b/utils/add_china_db_platform.py @@ -0,0 +1,8 @@ +#!/usr/bin/python +# +databases = [ + { + 'name': 'TiDB', + 'type': 'mysql', + } +]