diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 index 8fdb38705..2de68d742 100644 --- a/Dockerfile.loong64 +++ b/Dockerfile.loong64 @@ -77,7 +77,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \ && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \ && pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl \ - && pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.0-cp39-cp39-linux_loongarch64.whl \ + && pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl \ && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ && pip install -r requirements/requirements.txt diff --git a/README.md b/README.md index ca31cce08..71872e4a2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,14 @@ -------------------------- -JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。 +JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括: + +- **SSH**: Linux / Unix / 网络设备 等; +- **Windows**: Web 方式连接 / 原生 RDP 连接; +- **数据库**: MySQL / Oracle / SQLServer / PostgreSQL 等; +- **Kubernetes**: 支持连接到 K8s 集群中的 Pods; +- **Web 站点**: 各类系统的 Web 管理后台; +- **应用**: 通过 Remote App 连接各类应用。 ## 产品特色 @@ -33,8 +40,6 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 - **多云支持**: 一套系统,同时管理不同云上面的资产; - **多租户**: 一套系统,多个子公司或部门同时使用; - **云端存储**: 审计录像云端存储,永不丢失; -- **多应用支持**: 全面支持各类资产,包括服务器、数据库、Windows RemoteApp、Kubernetes 等; -- **安全可靠**: 被广泛使用、验证和信赖,连续 9 年的持续研发投入和产品更新升级。 ## UI 展示 @@ -72,12 +77,13 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 - [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687) - [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666) -## 社区 +## 社区交流 -如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose) -或加入到我们的社区当中进行进一步交流沟通。 +如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。 -### 微信交流群 +您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 及微信交流群当中进行交流沟通。 + +**微信交流群** 微信群二维码 @@ -107,7 +113,7 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju - 邮箱:support@fit2cloud.com - 电话:400-052-0755 -## 致谢 +## 致谢开源 - [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目; - [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。 diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index b25dafb9f..6fb699721 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -32,6 +32,7 @@ class AccountViewSet(OrgBulkModelViewSet): 'su_from_accounts': 'accounts.view_account', 'clear_secret': 'accounts.change_account', } + export_as_zip = True @action(methods=['get'], detail=False, url_path='su-from-accounts') def su_from_accounts(self, request, *args, **kwargs): diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index 11675368f..0aecb5143 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,4 +1,6 @@ from django_filters import rest_framework as drf_filters +from rest_framework.decorators import action +from rest_framework.response import Response from accounts import serializers from accounts.models import AccountTemplate @@ -38,8 +40,20 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): filterset_class = AccountTemplateFilterSet search_fields = ('username', 'name') serializer_classes = { - 'default': serializers.AccountTemplateSerializer + 'default': serializers.AccountTemplateSerializer, } + rbac_perms = { + 'su_from_account_templates': 'accounts.view_accounttemplate', + } + + @action(methods=['get'], detail=False, url_path='su-from-account-templates') + def su_from_account_templates(self, request, *args, **kwargs): + pk = request.query_params.get('template_id') + template = AccountTemplate.objects.filter(pk=pk).first() + templates = AccountTemplate.get_su_from_account_templates(template) + templates = self.filter_queryset(templates) + serializer = self.get_serializer(templates, many=True) + return Response(data=serializer.data) class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml index 4bb571f62..b51ddf69e 100644 --- a/apps/accounts/automations/change_secret/host/aix/main.yml +++ b/apps/accounts/automations/change_secret/host/aix/main.yml @@ -9,6 +9,7 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('des') }}" update_password: always + ignore_errors: true when: account.secret_type == "password" - name: create user If it already exists, no operation will be performed diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml index 8dea25c12..80f0aa01c 100644 --- a/apps/accounts/automations/change_secret/host/posix/main.yml +++ b/apps/accounts/automations/change_secret/host/posix/main.yml @@ -9,6 +9,7 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always + ignore_errors: true when: account.secret_type == "password" - name: create user If it already exists, no operation will be performed diff --git a/apps/accounts/automations/change_secret/host/windows/main.yml b/apps/accounts/automations/change_secret/host/windows/main.yml index 66efb0801..86ea7a81f 100644 --- a/apps/accounts/automations/change_secret/host/windows/main.yml +++ b/apps/accounts/automations/change_secret/host/windows/main.yml @@ -21,6 +21,7 @@ groups: "{{ user_info.groups[0].name }}" groups_action: add update_password: always + ignore_errors: true when: account.secret_type == "password" - name: Refresh connection diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index 05e2b1349..9d1c2f441 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -72,14 +72,14 @@ class ChangeSecretManager(AccountBasePlaybookManager): return [] asset = privilege_account.asset - accounts = asset.accounts.exclude(username=privilege_account.username) + accounts = asset.accounts.all() accounts = accounts.filter(id__in=self.account_ids) if self.secret_type: accounts = accounts.filter(secret_type=self.secret_type) if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED: accounts = accounts.filter(privileged=False).exclude( - username__in=['root', 'administrator'] + username__in=['root', 'administrator', privilege_account.username] ) return accounts diff --git a/apps/accounts/automations/gather_accounts/filter.py b/apps/accounts/automations/gather_accounts/filter.py index af7cdefa4..c6db6dbd4 100644 --- a/apps/accounts/automations/gather_accounts/filter.py +++ b/apps/accounts/automations/gather_accounts/filter.py @@ -13,8 +13,8 @@ class GatherAccountsFilter: def mysql_filter(info): result = {} for _, user_dict in info.items(): - for username, data in user_dict.items(): - if data.get('account_locked') == 'N': + for username, _ in user_dict.items(): + if len(username.split('.')) == 1: result[username] = {} return result diff --git a/apps/accounts/automations/push_account/host/aix/main.yml b/apps/accounts/automations/push_account/host/aix/main.yml index 9ac68d20e..7c43c5220 100644 --- a/apps/accounts/automations/push_account/host/aix/main.yml +++ b/apps/accounts/automations/push_account/host/aix/main.yml @@ -43,6 +43,7 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always + ignore_errors: true when: account.secret_type == "password" - name: remove jumpserver ssh key diff --git a/apps/accounts/automations/push_account/host/posix/main.yml b/apps/accounts/automations/push_account/host/posix/main.yml index 9ac68d20e..7c43c5220 100644 --- a/apps/accounts/automations/push_account/host/posix/main.yml +++ b/apps/accounts/automations/push_account/host/posix/main.yml @@ -43,6 +43,7 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always + ignore_errors: true when: account.secret_type == "password" - name: remove jumpserver ssh key diff --git a/apps/accounts/automations/push_account/host/windows/main.yml b/apps/accounts/automations/push_account/host/windows/main.yml index 8a2a0aef0..17f68b660 100644 --- a/apps/accounts/automations/push_account/host/windows/main.yml +++ b/apps/accounts/automations/push_account/host/windows/main.yml @@ -17,6 +17,7 @@ groups: "{{ params.groups }}" groups_action: add update_password: always + ignore_errors: true when: account.secret_type == "password" - name: Refresh connection diff --git a/apps/accounts/automations/verify_account/custom/main.yml b/apps/accounts/automations/verify_account/custom/main.yml index 6ad8cd98b..cf4a937a7 100644 --- a/apps/accounts/automations/verify_account/custom/main.yml +++ b/apps/accounts/automations/verify_account/custom/main.yml @@ -10,5 +10,5 @@ login_port: "{{ jms_asset.port }}" login_user: "{{ account.username }}" login_password: "{{ account.secret }}" - login_secret_type: "{{ jms_account.secret_type }}" - login_private_key_path: "{{ jms_account.private_key_path }}" + login_secret_type: "{{ account.secret_type }}" + login_private_key_path: "{{ account.private_key_path }}" diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index fa29bcbfc..67e243e1c 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -5,7 +5,6 @@ from django_filters import rest_framework as drf_filters from assets.models import Node from common.drf.filters import BaseFilterSet - from .models import Account, GatheredAccount @@ -46,7 +45,7 @@ class AccountFilterSet(BaseFilterSet): class Meta: model = Account - fields = ['id', 'asset_id'] + fields = ['id', 'asset_id', 'source_id'] class GatheredAccountFilterSet(BaseFilterSet): diff --git a/apps/accounts/migrations/0011_auto_20230506_1443.py b/apps/accounts/migrations/0011_auto_20230506_1443.py new file mode 100644 index 000000000..3460376bd --- /dev/null +++ b/apps/accounts/migrations/0011_auto_20230506_1443.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.17 on 2023-05-06 06:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_gatheraccountsautomation_is_sync_account'), + ] + + operations = [ + migrations.AddField( + model_name='accounttemplate', + name='su_from', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='accounts.accounttemplate', verbose_name='Su from'), + ), + migrations.AlterField( + model_name='changesecretautomation', + name='ssh_key_change_strategy', + field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), + ), + migrations.AlterField( + model_name='pushaccountautomation', + name='ssh_key_change_strategy', + field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), + ), + ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 4094018e1..30eb853e3 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models import Count, Q +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords @@ -106,6 +108,11 @@ class Account(AbsConnectivity, BaseAccount): class AccountTemplate(BaseAccount): + su_from = models.ForeignKey( + 'self', related_name='su_to', null=True, + on_delete=models.SET_NULL, verbose_name=_("Su from") + ) + class Meta: verbose_name = _('Account template') unique_together = ( @@ -116,5 +123,62 @@ class AccountTemplate(BaseAccount): ('change_accounttemplatesecret', _('Can change asset account template secret')), ] + @classmethod + def get_su_from_account_templates(cls, instance=None): + if not instance: + return cls.objects.all() + return cls.objects.exclude(Q(id=instance.id) | Q(su_from=instance)) + + def get_su_from_account(self, asset): + su_from = self.su_from + if su_from and asset.platform.su_enabled: + account = asset.accounts.filter( + username=su_from.username, + secret_type=su_from.secret_type + ).first() + return account + def __str__(self): return self.username + + @staticmethod + def bulk_update_accounts(accounts, data): + history_model = Account.history.model + account_ids = accounts.values_list('id', flat=True) + history_accounts = history_model.objects.filter(id__in=account_ids) + account_id_count_map = { + str(i['id']): i['count'] + for i in history_accounts.values('id').order_by('id') + .annotate(count=Count(1)).values('id', 'count') + } + + 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.objects.bulk_update(accounts, ['version', 'secret']) + + @staticmethod + def bulk_create_history_accounts(accounts, user_id): + history_model = Account.history.model + history_account_objs = [] + for account in accounts: + history_account_objs.append( + history_model( + id=account.id, + version=account.version, + secret=account.secret, + secret_type=account.secret_type, + history_user_id=user_id, + history_date=timezone.now() + ) + ) + history_model.objects.bulk_create(history_account_objs) + + def bulk_sync_account_secret(self, accounts, user_id): + """ 批量同步账号密码 """ + if not accounts: + return + self.bulk_update_accounts(accounts, {'secret': self.secret}) + 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 c76cb1788..7b4abefa2 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -1,4 +1,5 @@ import uuid +from copy import deepcopy from django.db import IntegrityError from django.db.models import Q @@ -21,8 +22,8 @@ logger = get_logger(__name__) class AccountCreateUpdateSerializerMixin(serializers.Serializer): template = serializers.PrimaryKeyRelatedField( - queryset=AccountTemplate.objects, - required=False, label=_("Template"), write_only=True + queryset=AccountTemplate.objects, required=False, + label=_("Template"), write_only=True, allow_null=True ) push_now = serializers.BooleanField( default=False, label=_("Push now"), write_only=True @@ -32,9 +33,10 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): ) on_invalid = LabeledChoiceField( choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR, - write_only=True, label=_('Exist policy') + write_only=True, allow_null=True, label=_('Exist policy'), ) _template = None + clean_auth_fields: callable class Meta: fields = ['template', 'push_now', 'params', 'on_invalid'] @@ -91,7 +93,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): self._template = template # Set initial data from template - ignore_fields = ['id', 'date_created', 'date_updated', 'org_id'] + ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id'] field_names = [ field.name for field in template._meta.fields if field.name not in ignore_fields @@ -151,12 +153,14 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): template = self._template if template is None: return + validated_data['source'] = Source.TEMPLATE validated_data['source_id'] = str(template.id) def create(self, validated_data): push_now = validated_data.pop('push_now', None) params = validated_data.pop('params', None) + self.clean_auth_fields(validated_data) self.generate_source_data(validated_data) instance, stat = self.do_create(validated_data) self.push_account_if_need(instance, push_now, params, stat) @@ -238,14 +242,18 @@ class AssetAccountBulkSerializerResultSerializer(serializers.Serializer): class AssetAccountBulkSerializer( AccountCreateUpdateSerializerMixin, AuthValidateMixin, serializers.ModelSerializer ): + su_from_username = serializers.CharField( + max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"), + allow_blank=True, + ) assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets')) class Meta: model = Account fields = [ - 'name', 'username', 'secret', 'secret_type', + 'name', 'username', 'secret', 'secret_type', 'passphrase', 'privileged', 'is_active', 'comment', 'template', - 'on_invalid', 'push_now', 'assets', + 'on_invalid', 'push_now', 'assets', 'su_from_username' ] extra_kwargs = { 'name': {'required': False}, @@ -293,8 +301,21 @@ class AssetAccountBulkSerializer( raise serializers.ValidationError(_('Account already exists')) return instance, True, 'created' + def generate_su_from_data(self, validated_data): + template = self._template + asset = validated_data['asset'] + su_from = validated_data.get('su_from') + su_from_username = validated_data.pop('su_from_username', None) + if template: + su_from = template.get_su_from_account(asset) + elif su_from_username: + su_from = asset.accounts.filter(username=su_from_username).first() + validated_data['su_from'] = su_from + def perform_create(self, vd, handler): lookup = self.get_filter_lookup(vd) + vd = deepcopy(vd) + self.generate_su_from_data(vd) try: instance, changed, state = handler(vd, lookup) except IntegrityError: @@ -335,6 +356,7 @@ class AssetAccountBulkSerializer( vd = vd.copy() vd['asset'] = asset try: + self.clean_auth_fields(vd) instance, changed, state = self.perform_create(vd, create_handler) _results[asset] = { 'changed': changed, 'instance': instance.id, 'state': state diff --git a/apps/accounts/serializers/account/backup.py b/apps/accounts/serializers/account/backup.py index af07ca04d..712bdd095 100644 --- a/apps/accounts/serializers/account/backup.py +++ b/apps/accounts/serializers/account/backup.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from accounts.models import AccountBackupAutomation, AccountBackupExecution diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py index 4e2b1a1df..b79dd51be 100644 --- a/apps/accounts/serializers/account/base.py +++ b/apps/accounts/serializers/account/base.py @@ -78,4 +78,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): ] extra_kwargs = { 'spec_info': {'label': _('Spec info')}, + 'username': {'help_text': _("Tip: If no username is required for authentication, fill in `null`")} } diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index 1de4d7c1d..d2d992ea6 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,86 +1,44 @@ -from django.db.transaction import atomic -from django.db.utils import IntegrityError +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers from accounts.models import AccountTemplate, Account -from assets.models import Asset from common.serializers import SecretReadableMixin +from common.serializers.fields import ObjectRelatedField from .base import BaseAccountSerializer class AccountTemplateSerializer(BaseAccountSerializer): + is_sync_account = serializers.BooleanField(default=False, write_only=True) + _is_sync_account = False + + su_from = ObjectRelatedField( + required=False, queryset=AccountTemplate.objects, allow_null=True, + allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username') + ) + class Meta(BaseAccountSerializer.Meta): model = AccountTemplate + fields = BaseAccountSerializer.Meta.fields + ['is_sync_account', 'su_from'] - @staticmethod - def account_save(data, account): - for field, value in data.items(): - setattr(account, field, value) - try: - account.save(update_fields=list(data.keys())) - except IntegrityError: - pass - - # TODO 数据库访问的太多了 后期优化 - @atomic() - def bulk_update_accounts(self, instance, diff): - accounts = Account.objects.filter(source_id=instance.id) - if not accounts: + def sync_accounts_secret(self, instance, diff): + if not self._is_sync_account or 'secret' not in diff: return - diff.pop('secret', None) - name = diff.pop('name', None) - username = diff.pop('username', None) - secret_type = diff.pop('secret_type', None) - update_accounts = [] - for account in accounts: - for field, value in diff.items(): - setattr(account, field, value) - update_accounts.append(account) + accounts = Account.objects.filter(source_id=instance.id) + instance.bulk_sync_account_secret(accounts, self.context['request'].user.id) - if update_accounts: - Account.objects.bulk_update(update_accounts, diff.keys()) - - if name: - for account in accounts: - data = {'name': name} - self.account_save(data, account) - - if secret_type and username: - asset_ids_supports = self.get_asset_ids_supports(accounts, secret_type) - for account in accounts: - asset_id = account.asset_id - if asset_id not in asset_ids_supports: - data = {'username': username} - self.account_save(data, account) - continue - data = {'username': username, 'secret_type': secret_type, 'secret': instance.secret} - self.account_save(data, account) - elif secret_type: - asset_ids_supports = self.get_asset_ids_supports(accounts, secret_type) - for account in accounts: - asset_id = account.asset_id - if asset_id not in asset_ids_supports: - continue - data = {'secret_type': secret_type, 'secret': instance.secret} - self.account_save(data, account) - elif username: - for account in accounts: - data = {'username': username} - self.account_save(data, account) - - @staticmethod - def get_asset_ids_supports(accounts, secret_type): - asset_ids = accounts.values_list('asset_id', flat=True) - secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type) - return [asset.id for asset in secret_type_supports] + def validate(self, attrs): + self._is_sync_account = attrs.pop('is_sync_account', None) + attrs = super().validate(attrs) + return attrs def update(self, instance, validated_data): - # diff = { - # k: v for k, v in validated_data.items() - # if getattr(instance, k) != v - # } + diff = { + k: v for k, v in validated_data.items() + if getattr(instance, k, None) != v + } instance = super().update(instance, validated_data) - # self.bulk_update_accounts(instance, diff) + self.sync_accounts_secret(instance, diff) return instance diff --git a/apps/accounts/serializers/automations/base.py b/apps/accounts/serializers/automations/base.py index cdb08bf36..1468ecf58 100644 --- a/apps/accounts/serializers/automations/base.py +++ b/apps/accounts/serializers/automations/base.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from accounts.models import AutomationExecution diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index 069a92a90..c1a2f0a1f 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from acls.models.base import ActionChoices +from jumpserver.utils import has_valid_xpack_license from common.serializers.fields import JSONManyToManyField, ObjectRelatedField, LabeledChoiceField from orgs.models import Organization from users.models import User @@ -51,7 +52,26 @@ class ACLAccountsSerializer(serializers.Serializer): ) -class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): +class ActionAclSerializer(serializers.Serializer): + action = LabeledChoiceField( + choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action") + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_action_choices() + + def set_action_choices(self): + action = self.fields.get("action") + if not action: + return + choices = action.choices + if not has_valid_xpack_license(): + choices.pop(ActionChoices.review, None) + action._choices = choices + + +class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Serializer): users = JSONManyToManyField(label=_('User')) assets = JSONManyToManyField(label=_('Asset')) accounts = serializers.ListField(label=_('Account')) @@ -61,9 +81,6 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): reviewers_amount = serializers.IntegerField( read_only=True, source="reviewers.count", label=_('Reviewers amount') ) - action = LabeledChoiceField( - choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action") - ) class Meta: fields_mini = ["id", "name"] diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index 0a180d9e5..07e499ed6 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -2,12 +2,11 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from common.serializers import BulkModelSerializer, MethodSerializer -from common.serializers.fields import ObjectRelatedField, LabeledChoiceField -from jumpserver.utils import has_valid_xpack_license +from common.serializers.fields import ObjectRelatedField from users.models import User +from .base import ActionAclSerializer from .rules import RuleSerializer from ..models import LoginACL -from ..models.base import ActionChoices __all__ = [ "LoginACLSerializer", @@ -18,12 +17,11 @@ common_help_text = _( ) -class LoginACLSerializer(BulkModelSerializer): +class LoginACLSerializer(ActionAclSerializer, BulkModelSerializer): user = ObjectRelatedField(queryset=User.objects, label=_("User")) reviewers = ObjectRelatedField( queryset=User.objects, label=_("Reviewers"), many=True, required=False ) - action = LabeledChoiceField(choices=ActionChoices.choices, label=_('Action')) reviewers_amount = serializers.IntegerField( read_only=True, source="reviewers.count", label=_("Reviewers amount") ) @@ -45,18 +43,5 @@ class LoginACLSerializer(BulkModelSerializer): "is_active": {"default": True}, } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_action_choices() - - def set_action_choices(self): - action = self.fields.get("action") - if not action: - return - choices = action.choices - if not has_valid_xpack_license(): - choices.pop(LoginACL.ActionChoices.review, None) - action._choices = choices - def get_rules_serializer(self): return RuleSerializer() diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 71d912188..ce5d8f9ac 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -25,7 +25,7 @@ from ...notifications import BulkUpdatePlatformSkipAssetUserMsg logger = get_logger(__file__) __all__ = [ "AssetViewSet", "AssetTaskCreateApi", - "AssetsTaskCreateApi", 'AssetFilterSet' + "AssetsTaskCreateApi", 'AssetFilterSet', ] @@ -95,7 +95,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): """ model = Asset filterset_class = AssetFilterSet - search_fields = ("name", "address") + search_fields = ("name", "address", "comment") ordering_fields = ('name', 'connectivity', 'platform', 'date_updated') serializer_classes = ( ("default", serializers.AssetSerializer), diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 13ae0f283..39d51197b 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -69,7 +69,7 @@ class SerializeToTreeNodeMixin: return 'file' @timeit - def serialize_assets(self, assets, node_key=None): + def serialize_assets(self, assets, node_key=None, pid=None): sftp_enabled_platform = PlatformProtocol.objects \ .filter(name='ssh', setting__sftp_enabled=True) \ .values_list('platform', flat=True) \ @@ -83,8 +83,10 @@ class SerializeToTreeNodeMixin: { 'id': str(asset.id), 'name': asset.name, - 'title': f'{asset.address}\n{asset.comment}', - 'pId': get_pid(asset), + 'title': + f'{asset.address}\n{asset.comment}' + if asset.comment else asset.address, + 'pId': pid or get_pid(asset), 'isParent': False, 'open': False, 'iconSkin': self.get_icon(asset), diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py index 9c9ffa8c9..f27afd019 100644 --- a/apps/assets/api/tree.py +++ b/apps/assets/api/tree.py @@ -163,8 +163,10 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): # 资源数量统计可选项 (asset, account) count_resource = self.request.query_params.get('count_resource', 'asset') - if include_asset and self.request.query_params.get('key'): + if not self.request.query_params.get('key'): + nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource) + elif include_asset: nodes = self.get_assets() else: - nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource) + nodes = [] return Response(data=nodes) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index ae9740347..aa31c886f 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -166,6 +166,7 @@ class BasePlaybookManager: account_prefer=self.ansible_account_prefer, account_policy=self.ansible_account_policy, host_callback=self.host_callback, + task_type=self.__class__.method_type(), ) inventory.write_to_file(inventory_path) @@ -223,7 +224,8 @@ class BasePlaybookManager: pass def on_host_error(self, host, error, result): - print('host error: {} -> {}'.format(host, error)) + if settings.DEBUG_DEV: + print('host error: {} -> {}'.format(host, error)) def on_runner_success(self, runner, cb): summary = cb.summary diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 8194c5c14..69fa36bb3 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -193,15 +193,38 @@ class AllTypes(ChoicesMixin): } return node - @classmethod - def to_tree_nodes(cls, include_asset, count_resource='asset'): - from accounts.models import Account - from ..models import Asset, Platform - if count_resource == 'account': - resource_platforms = Account.objects.all().values_list('asset__platform_id', flat=True) - else: - resource_platforms = Asset.objects.all().values_list('platform_id', flat=True) + @classmethod + def asset_to_node(cls, asset, pid): + node = { + 'id': '{}'.format(asset.id), + 'name': asset.name, + 'title': f'{asset.address}\n{asset.comment}', + 'pId': pid, + 'isParent': False, + 'open': False, + 'iconSkin': asset.type, + 'chkDisabled': not asset.is_active, + 'meta': { + 'type': 'platform', + 'data': { + 'platform_type': asset.platform.type, + 'org_name': asset.org_name, + # 'sftp': asset.platform_id in sftp_enabled_platform, + 'name': asset.name, + 'address': asset.address + }, + } + } + return node + + @classmethod + def get_root_nodes(cls): + return dict(id='ROOT', name=_('All types'), title=_('All types'), open=True, isParent=True) + + @classmethod + def get_tree_nodes(cls, resource_platforms, include_asset=False): + from ..models import Platform platform_count = defaultdict(int) for platform_id in resource_platforms: platform_count[platform_id] += 1 @@ -215,8 +238,7 @@ class AllTypes(ChoicesMixin): category_type_mapper[p.category] += platform_count[p.id] tp_platforms[p.category + '_' + p.type].append(p) - root = dict(id='ROOT', name=_('All types'), title=_('All types'), open=True, isParent=True) - nodes = [root] + nodes = [cls.get_root_nodes()] for category, type_cls in cls.category_types(): # Category 格式化 meta = {'type': 'category', 'category': category.value} @@ -244,6 +266,16 @@ class AllTypes(ChoicesMixin): nodes.append(platform_node) return nodes + @classmethod + def to_tree_nodes(cls, include_asset, count_resource='asset'): + from accounts.models import Account + from ..models import Asset + if count_resource == 'account': + resource_platforms = Account.objects.all().values_list('asset__platform_id', flat=True) + else: + resource_platforms = Asset.objects.all().values_list('platform_id', flat=True) + return cls.get_tree_nodes(resource_platforms, include_asset) + @classmethod def get_type_default_platform(cls, category, tp): constraints = cls.get_constraints(category, tp) diff --git a/apps/assets/migrations/0098_auto_20220430_2126.py b/apps/assets/migrations/0098_auto_20220430_2126.py index 5fd333262..64a85ecb6 100644 --- a/apps/assets/migrations/0098_auto_20220430_2126.py +++ b/apps/assets/migrations/0098_auto_20220430_2126.py @@ -20,12 +20,13 @@ def get_prop_name_id(apps, app, category): def migrate_database_to_asset(apps, *args): + node_model = apps.get_model('assets', 'Node') app_model = apps.get_model('applications', 'Application') db_model = apps.get_model('assets', 'Database') platform_model = apps.get_model('assets', 'Platform') applications = app_model.objects.filter(category='db') - platforms = platform_model.objects.all().filter(internal=True) + platforms = platform_model.objects.all().filter(internal=True).exclude(name='Redis6+') platforms_map = {p.type: p for p in platforms} print() @@ -84,11 +85,18 @@ def create_app_nodes(apps, org_id): node_keys = node_model.objects.filter(org_id=org_id) \ .filter(key__regex=child_pattern) \ .values_list('key', flat=True) - if not node_keys: - return - node_key_split = [key.split(':') for key in node_keys] - next_value = max([int(k[1]) for k in node_key_split]) + 1 - parent_key = node_key_split[0][0] + if node_keys: + node_key_split = [key.split(':') for key in node_keys] + next_value = max([int(k[1]) for k in node_key_split]) + 1 + parent_key = node_key_split[0][0] + else: + root_node = node_model.objects.filter(org_id=org_id)\ + .filter(parent_key='', key__regex=r'^[0-9]+$').exclude(key__startswith='-').first() + if not root_node: + return + parent_key = root_node.key + next_value = 0 + next_key = '{}:{}'.format(parent_key, next_value) name = 'Apps' parent = node_model.objects.get(key=parent_key) diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index 87eed5fb0..4e57be633 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -161,11 +161,12 @@ def migrate_db_accounts(apps, schema_editor): name = f'{username}(token)' else: secret_type = attr - name = username + name = username or f'{username}(password)' auth_infos.append((name, secret_type, secret)) if not auth_infos: - auth_infos.append((username, 'password', '')) + name = username or f'{username}(password)' + auth_infos.append((name, 'password', '')) for name, secret_type, secret in auth_infos: values['name'] = name diff --git a/apps/assets/migrations/0117_alter_baseautomation_params.py b/apps/assets/migrations/0117_alter_baseautomation_params.py new file mode 100644 index 000000000..1fc93bdd3 --- /dev/null +++ b/apps/assets/migrations/0117_alter_baseautomation_params.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-05-06 06:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0116_auto_20230418_1726'), + ] + + operations = [ + migrations.AlterField( + model_name='baseautomation', + name='params', + field=models.JSONField(default=dict, verbose_name='Parameters'), + ), + ] diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index 030e54de3..f50177642 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -26,14 +26,6 @@ class PlatformProtocol(models.Model): def secret_types(self): return Protocol.settings().get(self.name, {}).get('secret_types', ['password']) - def set_public(self): - private_protocol_set = ('winrm',) - self.public = self.name not in private_protocol_set - - def save(self, **kwargs): - self.set_public() - return super().save(**kwargs) - class PlatformAutomation(models.Model): ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index 0fd14eec9..88a6cfef2 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -23,7 +23,7 @@ __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', 'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', - 'AccountSecretSerializer', 'AssetProtocolsPermsSerializer' + 'AccountSecretSerializer', 'AssetProtocolsPermsSerializer', 'AssetLabelSerializer' ] diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py index da6c9574a..386cb87e9 100644 --- a/apps/assets/serializers/asset/database.py +++ b/apps/assets/serializers/asset/database.py @@ -1,5 +1,6 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.serializers import ValidationError +from rest_framework import serializers from assets.models import Database from assets.serializers.gateway import GatewayWithAccountSecretSerializer @@ -9,6 +10,8 @@ __all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer'] class DatabaseSerializer(AssetSerializer): + db_name = serializers.CharField(max_length=1024, label=_('Default database'), required=True) + class Meta(AssetSerializer.Meta): model = Database extra_fields = [ diff --git a/apps/assets/serializers/automations/base.py b/apps/assets/serializers/automations/base.py index 804bcabc9..527f71628 100644 --- a/apps/assets/serializers/automations/base.py +++ b/apps/assets/serializers/automations/base.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from ops.mixin import PeriodTaskSerializerMixin from assets.models import Asset, Node, BaseAutomation, AutomationExecution -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.utils import get_logger from common.const.choices import Trigger from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.utils import get_logger +from ops.mixin import PeriodTaskSerializerMixin +from orgs.mixins.serializers import BulkOrgResourceModelSerializer logger = get_logger(__file__) @@ -48,9 +48,12 @@ class AutomationExecutionSerializer(serializers.ModelSerializer): @staticmethod def get_snapshot(obj): + from assets.const import AutomationTypes as AssetTypes + from accounts.const import AutomationTypes as AccountTypes + tp_dict = dict(AssetTypes.choices) | dict(AccountTypes.choices) tp = obj.snapshot['type'] snapshot = { - 'type': tp, + 'type': {'value': tp, 'label': tp_dict.get(tp, tp)}, 'name': obj.snapshot['name'], 'comment': obj.snapshot['comment'], 'accounts': obj.snapshot['accounts'], diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 3313168cc..80421d41b 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -82,7 +82,7 @@ class PlatformProtocolSerializer(serializers.ModelSerializer): model = PlatformProtocol fields = [ "id", "name", "port", "primary", - "required", "default", + "required", "default", "public", "secret_types", "setting", ] @@ -122,6 +122,7 @@ class PlatformSerializer(WritableNestedModelSerializer): fields_small = fields_mini + [ "category", "type", "charset", ] + fields_unexport = ['automation'] read_only_fields = [ 'internal', 'date_created', 'date_updated', 'created_by', 'updated_by' @@ -156,20 +157,6 @@ class PlatformSerializer(WritableNestedModelSerializer): constraints = AllTypes.get_constraints(category, tp) return constraints - def validate(self, attrs): - domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False) - su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False) - automation = attrs.get('automation', {}) - automation['ansible_enabled'] = automation.get('ansible_enabled', False) \ - and self.constraints['automation'].get('ansible_enabled', False) - attrs.update({ - 'domain_enabled': domain_enabled, - 'su_enabled': su_enabled, - 'automation': automation, - }) - self.initial_data['automation'] = automation - return attrs - @classmethod def setup_eager_loading(cls, queryset): queryset = queryset.prefetch_related( @@ -187,6 +174,18 @@ class PlatformSerializer(WritableNestedModelSerializer): self.initial_data['protocols'] = protocols return protocols + def validate_su_enabled(self, su_enabled): + return su_enabled and self.constraints.get('su_enabled', False) + + def validate_domain_enabled(self, domain_enabled): + return domain_enabled and self.constraints.get('domain_enabled', False) + + def validate_automation(self, automation): + automation = automation or {} + automation = automation.get('ansible_enabled', False) \ + and self.constraints['automation'].get('ansible_enabled', False) + return automation + class PlatformOpsMethodSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) diff --git a/apps/audits/handler.py b/apps/audits/handler.py index 6491c69da..5c069ac09 100644 --- a/apps/audits/handler.py +++ b/apps/audits/handler.py @@ -45,10 +45,8 @@ class OperatorLogHandler(metaclass=Singleton): pre_value, value = self._consistent_type_to_str(pre_value, value) if sorted(str(value)) == sorted(str(pre_value)): continue - if pre_value: - before[key] = pre_value - if value: - after[key] = value + before[key] = pre_value + after[key] = value return before, after def cache_instance_before_data(self, instance_dict): diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 27cdd6e28..3865e9326 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -70,8 +70,10 @@ def _get_instance_field_value( if getattr(f, 'primary_key', False): f.verbose_name = 'id' - elif isinstance(value, (list, dict)): + elif isinstance(value, list): value = copy.deepcopy(value) + elif isinstance(value, dict): + value = dict(copy.deepcopy(value)) elif isinstance(value, datetime): value = as_current_tz(value).strftime('%Y-%m-%d %H:%M:%S') elif isinstance(f, models.OneToOneField) and isinstance(value, models.Model): diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 5b5dcd700..0bc69b025 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -3,6 +3,7 @@ import json import os import urllib.parse +from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -12,14 +13,12 @@ from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ValidationError -from assets.const import CloudTypes from common.api import JMSModelViewSet from common.exceptions import JMSException from common.utils import random_string, get_logger from common.utils.django import get_request_os -from common.utils.http import is_true +from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil @@ -27,7 +26,8 @@ from terminal.models import EndpointRule from ..models import ConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer + SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer, + ConnectionTokenUpdateSerializer ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -88,7 +88,8 @@ class RDPFileClientProtocolURLMixin: if width and height: rdp_options['desktopwidth:i'] = width rdp_options['desktopheight:i'] = height - rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}' + rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}' + rdp_options['dynamic resolution:i'] = '0' # 设置其他选项 rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') @@ -99,6 +100,10 @@ class RDPFileClientProtocolURLMixin: remote_app_options = token.get_remote_app_option() rdp_options.update(remote_app_options) + rdp = token.asset.platform.protocols.filter(name='rdp').first() + if rdp and rdp.setting.get('console'): + rdp_options['administrative session:i'] = '1' + # 文件名 name = token.asset.name prefix_name = f'{token.user.username}-{name}' @@ -226,10 +231,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView search_fields = filterset_fields serializer_classes = { 'default': ConnectionTokenSerializer, + 'update': ConnectionTokenUpdateSerializer, + 'partial_update': ConnectionTokenUpdateSerializer, } + http_method_names = ['get', 'post', 'patch', 'head', 'options', 'trace'] rbac_perms = { 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', + 'update': 'authentication.change_connectiontoken', 'create': 'authentication.add_connectiontoken', 'exchange': 'authentication.add_connectiontoken', 'expire': 'authentication.change_connectiontoken', @@ -366,19 +375,26 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): token_id = request.data.get('id') or '' token = get_object_or_404(ConnectionToken, pk=token_id) - if token.is_expired: - raise ValidationError({'id': 'Token is expired'}) - token.is_valid() serializer = self.get_serializer(instance=token) - expire_now = request.data.get('expire_now', True) - # TODO 暂时特殊处理 k8s 不过期 - if token.asset.type == CloudTypes.K8S: - expire_now = False + expire_now = request.data.get('expire_now', None) + asset_type = token.asset.type + # 设置默认值 + if expire_now is None: + # TODO 暂时特殊处理 k8s 不过期 + if asset_type in ['k8s', 'kubernetes']: + expire_now = False + else: + expire_now = not settings.CONNECTION_TOKEN_REUSABLE - if expire_now: + if is_false(expire_now): + logger.debug('Api specified, now expire now') + elif token.is_reusable and settings.CONNECTION_TOKEN_REUSABLE: + logger.debug('Token is reusable, not expire now') + else: token.expire() + return Response(serializer.data, status=status.HTTP_200_OK) @action(methods=['POST'], detail=False, url_path='applet-option') diff --git a/apps/authentication/backends/oauth2/backends.py b/apps/authentication/backends/oauth2/backends.py index 2214ba628..9f3b04ac3 100644 --- a/apps/authentication/backends/oauth2/backends.py +++ b/apps/authentication/backends/oauth2/backends.py @@ -104,7 +104,10 @@ class OAuth2Backend(JMSModelBackend): headers = { 'Accept': 'application/json' } - access_token_response = requests_func(access_token_url, headers=headers) + if token_method == 'post': + access_token_response = requests_func(access_token_url, headers=headers, json=query_dict) + else: + access_token_response = requests_func(access_token_url, headers=headers) try: access_token_response.raise_for_status() access_token_response_data = access_token_response.json() diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index d03798f94..43f7bdc14 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -1,17 +1,16 @@ import base64 -import time +from django.conf import settings +from django.contrib.auth import logout as auth_logout +from django.http import HttpResponse from django.shortcuts import redirect, reverse, render from django.utils.deprecation import MiddlewareMixin -from django.http import HttpResponse -from django.conf import settings from django.utils.translation import ugettext as _ -from django.contrib.auth import logout as auth_logout from apps.authentication import mixins +from authentication.signals import post_auth_failed from common.utils import gen_key_pair from common.utils import get_request_ip -from .signals import post_auth_failed class MFAMiddleware: @@ -76,12 +75,18 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin): ip = get_request_ip(request) try: self.request = request + self._check_third_party_login_acl() self._check_login_acl(request.user, ip) except Exception as e: - post_auth_failed.send( - sender=self.__class__, username=request.user.username, - request=self.request, reason=e.msg - ) + if getattr(request, 'user_need_delete', False): + request.user.delete() + else: + error_message = getattr(e, 'msg', None) + error_message = error_message or str(e) + post_auth_failed.send( + sender=self.__class__, username=request.user.username, + request=self.request, reason=error_message + ) auth_logout(request) context = { 'title': _('Authentication failed'), diff --git a/apps/authentication/migrations/0019_connectiontoken_is_reusable.py b/apps/authentication/migrations/0019_connectiontoken_is_reusable.py new file mode 100644 index 000000000..92ccc892a --- /dev/null +++ b/apps/authentication/migrations/0019_connectiontoken_is_reusable.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-05-08 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0018_alter_connectiontoken_input_secret'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='is_reusable', + field=models.BooleanField(default=False, verbose_name='Reusable'), + ), + ] diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 14e5fd35f..9fce76e57 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -54,6 +54,7 @@ def authenticate(request=None, **credentials): """ username = credentials.get('username') + temp_user = None for backend, backend_path in _get_backends(return_tuples=True): # 检查用户名是否允许认证 (预先检查,不浪费认证时间) logger.info('Try using auth backend: {}'.format(str(backend))) @@ -77,11 +78,19 @@ def authenticate(request=None, **credentials): # 检查用户是否允许认证 if not backend.user_allow_authenticate(user): + temp_user = user + temp_user.backend = backend_path continue # Annotate the user object with the path of the backend. user.backend = backend_path return user + else: + if temp_user is not None: + source_display = temp_user.source_display + request.error_message = _('''The administrator has enabled 'Only allow login from user source'. + The current user source is {}. Please contact the administrator.''').format(source_display) + return temp_user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request) @@ -212,7 +221,8 @@ class MFAMixin: self._do_check_user_mfa(code, mfa_type, user=user) def check_user_mfa_if_need(self, user): - if self.request.session.get('auth_mfa'): + if self.request.session.get('auth_mfa') and \ + self.request.session.get('auth_mfa_username') == user.username: return if not user.mfa_enabled: return @@ -220,15 +230,16 @@ class MFAMixin: active_mfa_names = user.active_mfa_backends_mapper.keys() raise errors.MFARequiredError(mfa_types=tuple(active_mfa_names)) - def mark_mfa_ok(self, mfa_type): + def mark_mfa_ok(self, mfa_type, user): self.request.session['auth_mfa'] = 1 + self.request.session['auth_mfa_username'] = user.username self.request.session['auth_mfa_time'] = time.time() self.request.session['auth_mfa_required'] = 0 self.request.session['auth_mfa_type'] = mfa_type - MFABlockUtils(self.request.user.username, self.get_request_ip()).clean_failed_count() + MFABlockUtils(user.username, self.get_request_ip()).clean_failed_count() def clean_mfa_mark(self): - keys = ['auth_mfa', 'auth_mfa_time', 'auth_mfa_required', 'auth_mfa_type'] + keys = ['auth_mfa', 'auth_mfa_time', 'auth_mfa_required', 'auth_mfa_type', 'auth_mfa_username'] for k in keys: self.request.session.pop(k, '') @@ -263,7 +274,7 @@ class MFAMixin: ok, msg = mfa_backend.check_code(code) if ok: - self.mark_mfa_ok(mfa_type) + self.mark_mfa_ok(mfa_type, user) return raise errors.MFAFailedError( @@ -345,6 +356,13 @@ class AuthACLMixin: 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) + if not error_message: + return + raise ValueError(error_message) + def check_user_login_confirm_if_need(self, user): if not self.request.session.get("auth_confirm_required"): return diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 63049170b..3604d49e5 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -40,6 +40,7 @@ class ConnectionToken(JMSOrgBaseModel): connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) + is_reusable = models.BooleanField(default=False, verbose_name=_("Reusable")) date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) from_ticket = models.OneToOneField( 'tickets.ApplyLoginAssetTicket', related_name='connection_token', @@ -74,7 +75,7 @@ class ConnectionToken(JMSOrgBaseModel): def expire(self): self.date_expired = timezone.now() - self.save() + self.save(update_fields=['date_expired']) def renewal(self): """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ @@ -108,9 +109,8 @@ class ConnectionToken(JMSOrgBaseModel): error = _('No user or invalid user') raise PermissionDenied(error) if not self.asset or not self.asset.is_active: - is_valid = False error = _('No asset or inactive asset') - return is_valid, error + raise PermissionDenied(error) if not self.account: error = _('No account') raise PermissionDenied(error) @@ -160,6 +160,7 @@ class ConnectionToken(JMSOrgBaseModel): 'remoteapplicationname:s': app, 'alternate shell:s': app, 'remoteapplicationcmdline:s': cmdline_b64, + 'disableconnectionsharing:i': '1', } return options @@ -172,7 +173,7 @@ class ConnectionToken(JMSOrgBaseModel): if not applet: return None - host_account = applet.select_host_account() + host_account = applet.select_host_account(self.user) if not host_account: raise JMSException({'error': 'No host account available'}) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 22545115f..7d3de1bb7 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,13 +1,16 @@ +from django.conf import settings +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from perms.serializers.permission import ActionChoicesField -from orgs.mixins.serializers import OrgResourceModelSerializerMixin from common.serializers.fields import EncryptedField +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from perms.serializers.permission import ActionChoicesField from ..models import ConnectionToken __all__ = [ 'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer', + 'ConnectionTokenUpdateSerializer', ] @@ -25,13 +28,13 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_small = fields_mini + [ 'user', 'asset', 'account', 'input_username', 'input_secret', 'connect_method', 'protocol', 'actions', - 'is_active', 'from_ticket', 'from_ticket_info', + 'is_active', 'is_reusable', 'from_ticket', 'from_ticket_info', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] read_only_fields = [ # 普通 Token 不支持指定 user - 'user', 'expire_time', 'is_expired', + 'user', 'expire_time', 'is_expired', 'date_expired', 'user_display', 'asset_display', ] fields = fields_small + read_only_fields @@ -57,6 +60,32 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): return info +class ConnectionTokenUpdateSerializer(ConnectionTokenSerializer): + class Meta(ConnectionTokenSerializer.Meta): + can_update_fields = ['is_reusable'] + read_only_fields = list(set(ConnectionTokenSerializer.Meta.fields) - set(can_update_fields)) + + def _get_date_expired(self): + delta = self.instance.date_expired - self.instance.date_created + if delta.total_seconds() > 3600 * 24: + return self.instance.date_expired + + seconds = settings.CONNECTION_TOKEN_EXPIRATION_MAX + return timezone.now() + timezone.timedelta(seconds=seconds) + + @staticmethod + def validate_is_reusable(value): + if value and not settings.CONNECTION_TOKEN_REUSABLE: + raise serializers.ValidationError(_('Reusable connection token is not allowed, global setting not enabled')) + return value + + def validate(self, attrs): + reusable = attrs.get('is_reusable', False) + if reusable: + attrs['date_expired'] = self._get_date_expired() + return attrs + + class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): read_only_fields = list(set(ConnectionTokenSerializer.Meta.read_only_fields) - {'user'}) diff --git a/apps/authentication/views/base.py b/apps/authentication/views/base.py new file mode 100644 index 000000000..e98e41e70 --- /dev/null +++ b/apps/authentication/views/base.py @@ -0,0 +1,98 @@ +from functools import lru_cache + +from rest_framework.request import Request +from django.utils.translation import ugettext_lazy as _ +from django.utils.module_loading import import_string +from django.conf import settings +from django.db.utils import IntegrityError +from django.views import View + +from authentication import errors +from authentication.mixins import AuthMixin +from users.models import User +from common.utils.django import reverse, get_object_or_none +from common.utils import get_logger + +from .mixins import FlashMessageMixin + + +logger = get_logger(__file__) + + +class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, View): + + client_type_path = '' + client_auth_params = {} + user_type = '' + auth_backend = None + # 提示信息 + msg_client_err = _('Error') + msg_user_not_bound_err = _('Error') + msg_not_found_user_from_client_err = _('Error') + + def verify_state(self): + raise NotImplementedError + + def get_verify_state_failed_response(self, redirect_uri): + raise NotImplementedError + + @property + @lru_cache(maxsize=1) + def client(self): + if not all([self.client_type_path, self.client_auth_params]): + raise NotImplementedError + client_init = {k: getattr(settings, v) for k, v in self.client_auth_params.items()} + client_type = import_string(self.client_type_path) + return client_type(**client_init) + + def create_user_if_not_exist(self, user_id, **kwargs): + user = None + user_attr = self.client.get_user_detail(user_id, **kwargs) + try: + user, create = User.objects.get_or_create( + username=user_attr['username'], defaults=user_attr + ) + setattr(user, f'{self.user_type}_id', user_id) + if create: + setattr(user, 'source', self.user_type) + user.save() + except IntegrityError as err: + logger.error(f'{self.msg_client_err}: create user error: {err}') + + if user is None: + title = self.msg_client_err + msg = _('If you have any question, please contact the administrator') + return user, (title, msg) + + return user, None + + def get(self, request: Request): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user_id, other_info = self.client.get_user_id_by_code(code) + if not user_id: + # 正常流程不会出这个错误,hack 行为 + err = self.msg_not_found_user_from_client_err + response = self.get_failed_response(login_url, title=err, msg=err) + return response + + user = get_object_or_none(User, **{f'{self.user_type}_id': user_id}) + if user is None: + user, err = self.create_user_if_not_exist(user_id, other_info=other_info) + if err is not None: + response = self.get_failed_response(login_url, title=err[0], msg=err[1]) + return response + + try: + self.check_oauth2_auth(user, getattr(settings, self.auth_backend)) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_response(login_url, title=msg, msg=msg) + return response + return self.redirect_to_guard_view() diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index e0e76f966..77cec63fd 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -16,21 +16,22 @@ from authentication.notifications import OAuthBindMessage from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk -from common.utils import FlashMessageUtil, get_logger +from common.utils import get_logger from common.utils.common import get_request_ip from common.utils.django import get_object_or_none, reverse from common.utils.random import random_string from users.models import User from users.views import UserVerifyPasswordView -from .mixins import METAMixin +from .base import BaseLoginCallbackView +from .mixins import METAMixin, FlashMessageMixin logger = get_logger(__file__) DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' -class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): +class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): def dispatch(self, request, *args, **kwargs): try: return super().dispatch(request, *args, **kwargs) @@ -56,26 +57,6 @@ class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, Vie msg = _("The system configuration is incorrect. Please contact your administrator") return self.get_failed_response(redirect_uri, msg, msg) - @staticmethod - def get_success_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'message': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - - @staticmethod - def get_failed_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'error': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - def get_already_bound_response(self, redirect_url): msg = _('DingTalk is already bound') response = self.get_failed_response(redirect_url, msg, msg) @@ -158,7 +139,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View): appsecret=settings.DINGTALK_APPSECRET, agentid=settings.DINGTALK_AGENTID ) - userid = dingtalk.get_userid_by_code(code) + userid, __ = dingtalk.get_user_id_by_code(code) if not userid: msg = _('DingTalk query user failed') @@ -214,45 +195,20 @@ class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): return HttpResponseRedirect(url) -class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): +class DingTalkQRLoginCallbackView(DingTalkQRMixin, BaseLoginCallbackView): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - login_url = reverse('authentication:login') + client_type_path = 'common.sdk.im.dingtalk.DingTalk' + client_auth_params = { + 'appid': 'DINGTALK_APPKEY', 'appsecret': 'DINGTALK_APPSECRET', + 'agentid': 'DINGTALK_AGENTID' + } + user_type = 'dingtalk' + auth_backend = 'AUTH_BACKEND_DINGTALK' - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - dingtalk = DingTalk( - appid=settings.DINGTALK_APPKEY, - appsecret=settings.DINGTALK_APPSECRET, - agentid=settings.DINGTALK_AGENTID - ) - userid = dingtalk.get_userid_by_code(code) - if not userid: - # 正常流程不会出这个错误,hack 行为 - msg = _('Failed to get user from DingTalk') - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - user = get_object_or_none(User, dingtalk_id=userid) - if user is None: - title = _('DingTalk is not bound') - msg = _('Please login with a password and then bind the DingTalk') - response = self.get_failed_response(login_url, title=title, msg=msg) - return response - - try: - self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK) - except errors.AuthFailedError as e: - self.set_login_failed_mark() - msg = e.msg - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() + msg_client_err = _('DingTalk Error') + msg_user_not_bound_err = _('DingTalk is not bound') + msg_not_found_user_from_client_err = _('Failed to get user from DingTalk') class DingTalkOAuthLoginView(DingTalkOAuthMixin, View): @@ -284,7 +240,7 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View): appsecret=settings.DINGTALK_APPSECRET, agentid=settings.DINGTALK_AGENTID ) - userid = dingtalk.get_userid_by_code(code) + userid, __ = dingtalk.get_user_id_by_code(code) if not userid: # 正常流程不会出这个错误,hack 行为 msg = _('Failed to get user from DingTalk') diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index e9734b170..62660764a 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -9,26 +9,26 @@ from django.views import View from rest_framework.exceptions import APIException from rest_framework.permissions import AllowAny, IsAuthenticated -from authentication import errors from authentication.const import ConfirmType -from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.feishu import URL, FeiShu -from common.utils import FlashMessageUtil, get_logger +from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import get_object_or_none, reverse +from common.utils.django import reverse from common.utils.random import random_string -from users.models import User from users.views import UserVerifyPasswordView +from .base import BaseLoginCallbackView +from .mixins import FlashMessageMixin + logger = get_logger(__file__) FEISHU_STATE_SESSION_KEY = '_feishu_state' -class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): +class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): def dispatch(self, request, *args, **kwargs): try: return super().dispatch(request, *args, **kwargs) @@ -63,26 +63,6 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): url = URL().authen + '?' + urlencode(params) return url - @staticmethod - def get_success_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'message': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - - @staticmethod - def get_failed_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'error': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - def get_already_bound_response(self, redirect_url): msg = _('FeiShu is already bound') response = self.get_failed_response(redirect_url, msg, msg) @@ -93,7 +73,6 @@ class FeiShuQRBindView(FeiShuQRMixin, View): permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) def get(self, request: HttpRequest): - user = request.user redirect_url = request.GET.get('redirect_url') redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True) @@ -123,7 +102,7 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View): app_id=settings.FEISHU_APP_ID, app_secret=settings.FEISHU_APP_SECRET ) - user_id = feishu.get_user_id_by_code(code) + user_id, __ = feishu.get_user_id_by_code(code) if not user_id: msg = _('FeiShu query user failed') @@ -176,41 +155,15 @@ class FeiShuQRLoginView(FeiShuQRMixin, View): return HttpResponseRedirect(url) -class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View): +class FeiShuQRLoginCallbackView(FeiShuQRMixin, BaseLoginCallbackView): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - login_url = reverse('authentication:login') + client_type_path = 'common.sdk.im.feishu.FeiShu' + client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'} + user_type = 'feishu' + auth_backend = 'AUTH_BACKEND_FEISHU' - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) + msg_client_err = _('FeiShu Error') + msg_user_not_bound_err = _('FeiShu is not bound') + msg_not_found_user_from_client_err = _('Failed to get user from FeiShu') - feishu = FeiShu( - app_id=settings.FEISHU_APP_ID, - app_secret=settings.FEISHU_APP_SECRET - ) - user_id = feishu.get_user_id_by_code(code) - if not user_id: - # 正常流程不会出这个错误,hack 行为 - msg = _('Failed to get user from FeiShu') - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - user = get_object_or_none(User, feishu_id=user_id) - if user is None: - title = _('FeiShu is not bound') - msg = _('Please login with a password and then bind the FeiShu') - response = self.get_failed_response(login_url, title=title, msg=msg) - return response - - try: - self.check_oauth2_auth(user, settings.AUTH_BACKEND_FEISHU) - except errors.AuthFailedError as e: - self.set_login_failed_mark() - msg = e.msg - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() diff --git a/apps/authentication/views/mixins.py b/apps/authentication/views/mixins.py index 3571dfac7..d56206dcd 100644 --- a/apps/authentication/views/mixins.py +++ b/apps/authentication/views/mixins.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +from common.utils import FlashMessageUtil + class METAMixin: def get_next_url_from_meta(self): @@ -10,3 +12,16 @@ class METAMixin: if len(next_url_item) > 1: next_url = next_url_item[-1] return next_url + + +class FlashMessageMixin: + @staticmethod + def get_response(redirect_url, title, msg, m_type='message'): + message_data = {'title': title, 'interval': 5, 'redirect_url': redirect_url, m_type: msg} + return FlashMessageUtil.gen_and_redirect_to(message_data) + + def get_success_response(self, redirect_url, title, msg): + return self.get_response(redirect_url, title, msg) + + def get_failed_response(self, redirect_url, title, msg): + return self.get_response(redirect_url, title, msg, 'error') diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index c764c2138..e127dbeee 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -10,7 +10,7 @@ from rest_framework.exceptions import APIException from users.models import User from users.views import UserVerifyPasswordView -from common.utils import get_logger, FlashMessageUtil +from common.utils import get_logger from common.utils.random import random_string from common.utils.django import reverse, get_object_or_none from common.sdk.im.wecom import URL @@ -22,14 +22,16 @@ from authentication import errors from authentication.mixins import AuthMixin from authentication.const import ConfirmType from authentication.notifications import OAuthBindMessage -from .mixins import METAMixin + +from .base import BaseLoginCallbackView +from .mixins import METAMixin, FlashMessageMixin logger = get_logger(__file__) WECOM_STATE_SESSION_KEY = '_wecom_state' -class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): +class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): def dispatch(self, request, *args, **kwargs): try: return super().dispatch(request, *args, **kwargs) @@ -55,26 +57,6 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): msg = _("The system configuration is incorrect. Please contact your administrator") return self.get_failed_response(redirect_uri, msg, msg) - @staticmethod - def get_success_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'message': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - - @staticmethod - def get_failed_response(redirect_url, title, msg): - message_data = { - 'title': title, - 'error': msg, - 'interval': 5, - 'redirect_url': redirect_url, - } - return FlashMessageUtil.gen_and_redirect_to(message_data) - def get_already_bound_response(self, redirect_url): msg = _('WeCom is already bound') response = self.get_failed_response(redirect_url, msg, msg) @@ -208,45 +190,17 @@ class WeComQRLoginView(WeComQRMixin, METAMixin, View): return HttpResponseRedirect(url) -class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): +class WeComQRLoginCallbackView(WeComQRMixin, BaseLoginCallbackView): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - login_url = reverse('authentication:login') + client_type_path = 'common.sdk.im.wecom.WeCom' + client_auth_params = {'corpid': 'WECOM_CORPID', 'corpsecret': 'WECOM_SECRET', 'agentid': 'WECOM_AGENTID'} + user_type = 'wecom' + auth_backend = 'AUTH_BACKEND_WECOM' - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - wecom = WeCom( - corpid=settings.WECOM_CORPID, - corpsecret=settings.WECOM_SECRET, - agentid=settings.WECOM_AGENTID - ) - wecom_userid, __ = wecom.get_user_id_by_code(code) - if not wecom_userid: - # 正常流程不会出这个错误,hack 行为 - msg = _('Failed to get user from WeCom') - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - user = get_object_or_none(User, wecom_id=wecom_userid) - if user is None: - title = _('WeCom is not bound') - msg = _('Please login with a password and then bind the WeCom') - response = self.get_failed_response(login_url, title=title, msg=msg) - return response - - try: - self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM) - except errors.AuthFailedError as e: - self.set_login_failed_mark() - msg = e.msg - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() + msg_client_err = _('WeCom Error') + msg_user_not_bound_err = _('WeCom is not bound') + msg_not_found_user_from_client_err = _('Failed to get user from WeCom') class WeComOAuthLoginView(WeComOAuthMixin, View): diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py index 5b2aa5612..a9b3b5c8a 100644 --- a/apps/common/drf/renders/base.py +++ b/apps/common/drf/renders/base.py @@ -1,6 +1,10 @@ import abc +import io +import re from datetime import datetime +import pyzipper +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from rest_framework.renderers import BaseRenderer from rest_framework.utils import encoders, json @@ -181,8 +185,35 @@ class BaseFileRenderer(BaseRenderer): self.write_rows(rows) self.after_render() value = self.get_rendered_value() + if getattr(view, 'export_as_zip', False) and self.template == 'export': + value = self.compress_into_zip_file(value, request, response) except Exception as e: logger.debug(e, exc_info=True) value = 'Render error! ({})'.format(self.media_type).encode('utf-8') return value return value + + def compress_into_zip_file(self, value, request, response): + filename_pattern = re.compile(r'filename="([^"]+)"') + content_disposition = response['Content-Disposition'] + match = filename_pattern.search(content_disposition) + filename = match.group(1) + response['Content-Disposition'] = content_disposition.replace(self.format, 'zip') + + contents_io = io.BytesIO() + secret_key = request.user.secret_key + if not secret_key: + content = _("{} - The encryption password has not been set - " + "please go to personal information -> file encryption password " + "to set the encryption password").format(request.user.name) + + response['Content-Disposition'] = content_disposition.replace(self.format, 'txt') + contents_io.write(content.encode('utf-8')) + return contents_io.getvalue() + + with pyzipper.AESZipFile( + contents_io, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES + ) as zf: + zf.setpassword(secret_key.encode('utf8')) + zf.writestr(filename, value) + return contents_io.getvalue() diff --git a/apps/common/management/commands/services/services/flower.py b/apps/common/management/commands/services/services/flower.py index bb1452827..94dd8e2f0 100644 --- a/apps/common/management/commands/services/services/flower.py +++ b/apps/common/management/commands/services/services/flower.py @@ -11,7 +11,7 @@ class FlowerService(BaseService): @property def db_file(self): - return os.path.join(BASE_DIR, 'data', 'flower') + return os.path.join(BASE_DIR, 'data', 'flower.db') @property def cmd(self): diff --git a/apps/common/sdk/im/dingtalk/__init__.py b/apps/common/sdk/im/dingtalk/__init__.py index d41e73221..4f54e1ea2 100644 --- a/apps/common/sdk/im/dingtalk/__init__.py +++ b/apps/common/sdk/im/dingtalk/__init__.py @@ -5,6 +5,7 @@ import base64 from common.utils import get_logger from common.sdk.im.utils import digest, as_request from common.sdk.im.mixin import BaseRequest +from users.utils import construct_user_email logger = get_logger(__file__) @@ -35,6 +36,7 @@ class URL: SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2' GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress' GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid' + GET_USER_INFO_BY_USER_ID = 'https://oapi.dingtalk.com/topapi/v2/user/get' class DingTalkRequests(BaseRequest): @@ -129,11 +131,11 @@ class DingTalk: data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True) return data['user_info'] - def get_userid_by_code(self, code): + def get_user_id_by_code(self, code): user_info = self.get_userinfo_bycode(code) unionid = user_info['unionid'] userid = self.get_userid_by_unionid(unionid) - return userid + return userid, None def get_userid_by_unionid(self, unionid): body = { @@ -195,3 +197,18 @@ class DingTalk: data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) return data + + def get_user_detail(self, user_id, **kwargs): + # https://open.dingtalk.com/document/orgapp/query-user-details + body = {'userid': user_id} + data = self._request.post( + URL.GET_USER_INFO_BY_USER_ID, json=body, with_token=True + ) + data = data['result'] + username = user_id + name = data.get('name', username) + email = data.get('email') or data.get('org_email') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email + } diff --git a/apps/common/sdk/im/feishu/__init__.py b/apps/common/sdk/im/feishu/__init__.py index cb01b66da..087a2fb88 100644 --- a/apps/common/sdk/im/feishu/__init__.py +++ b/apps/common/sdk/im/feishu/__init__.py @@ -1,9 +1,9 @@ import json -from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException from django.conf import settings +from users.utils import construct_user_email from common.utils.common import get_logger from common.sdk.im.utils import digest from common.sdk.im.mixin import RequestMixin, BaseRequest @@ -37,6 +37,9 @@ class URL: def send_message(self): return f'{self.host}/open-apis/im/v1/messages' + def get_user_detail(self, user_id): + return f'{self.host}/open-apis/contact/v3/users/{user_id}' + class ErrorCode: INVALID_APP_ACCESS_TOKEN = 99991664 @@ -106,7 +109,7 @@ class FeiShu(RequestMixin): data = self._requests.post(URL().get_user_info_by_code, json=body, check_errcode_is_0=False) self._requests.check_errcode_is_0(data) - return data['data']['user_id'] + return data['data']['user_id'], data['data'] def send_text(self, user_ids, msg): params = { @@ -130,3 +133,15 @@ class FeiShu(RequestMixin): logger.exception(e) invalid_users.append(user_id) return invalid_users + + @staticmethod + def get_user_detail(user_id, **kwargs): + # get_user_id_by_code 已经返回个人信息,这里直接解析 + data = kwargs['other_info'] + username = user_id + name = data.get('name', username) + email = data.get('email') or data.get('enterprise_email') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email + } diff --git a/apps/common/sdk/im/wecom/__init__.py b/apps/common/sdk/im/wecom/__init__.py index bc925508e..5d2d6a4c1 100644 --- a/apps/common/sdk/im/wecom/__init__.py +++ b/apps/common/sdk/im/wecom/__init__.py @@ -3,8 +3,9 @@ from typing import Iterable, AnyStr from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException +from users.utils import construct_user_email from common.utils.common import get_logger -from common.sdk.im.utils import digest, DictWrapper, update_values, set_default +from common.sdk.im.utils import digest, update_values from common.sdk.im.mixin import RequestMixin, BaseRequest logger = get_logger(__name__) @@ -151,10 +152,7 @@ class WeCom(RequestMixin): def get_user_id_by_code(self, code): # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 - - params = { - 'code': code, - } + params = {'code': code} data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False) errcode = data['errcode'] @@ -175,12 +173,15 @@ class WeCom(RequestMixin): logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') raise WeComError - def get_user_detail(self, id): + def get_user_detail(self, user_id, **kwargs): # https://open.work.weixin.qq.com/api/doc/90000/90135/90196 - - params = { - 'userid': id, + params = {'userid': user_id} + data = self._requests.get(URL.GET_USER_DETAIL, params) + username = data.get('userid') + name = data.get('name', username) + email = data.get('email') or data.get('biz_mail') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email } - data = self._requests.get(URL.GET_USER_DETAIL, params) - return data diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index 51b97b8b6..767645c88 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -175,6 +175,8 @@ def _parse_ssh_private_key(text, password=None): dsa.DSAPrivateKey, ed25519.Ed25519PrivateKey, """ + if not bool(password): + password = None if isinstance(text, str): try: text = text.encode("utf-8") diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index b684f004a..baf741407 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -45,3 +45,7 @@ def get_remote_addr(request): def is_true(value): return value in BooleanField.TRUE_VALUES + + +def is_false(value): + return value in BooleanField.FALSE_VALUES diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 343224675..18872058a 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -229,7 +229,9 @@ class Config(dict): 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'LOGIN_URL': reverse_lazy('authentication:login'), - 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, + 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, # 默认 + 'CONNECTION_TOKEN_EXPIRATION_MAX': 60 * 60 * 24 * 30, # 最大 + 'CONNECTION_TOKEN_REUSABLE': False, # Custom Config 'AUTH_CUSTOM': False, diff --git a/apps/jumpserver/rewriting/storage/permissions.py b/apps/jumpserver/rewriting/storage/permissions.py index 492a6d06f..51b1c9c40 100644 --- a/apps/jumpserver/rewriting/storage/permissions.py +++ b/apps/jumpserver/rewriting/storage/permissions.py @@ -2,6 +2,7 @@ path_perms_map = { 'xpack': '*', + 'settings': '*', 'replay': 'default', 'applets': 'terminal.view_applet', 'playbooks': 'ops.view_playbook' diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 62ba143ac..7312bd7eb 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -131,6 +131,9 @@ TICKETS_ENABLED = CONFIG.TICKETS_ENABLED REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED +CONNECTION_TOKEN_REUSABLE = CONFIG.CONNECTION_TOKEN_REUSABLE +CONNECTION_TOKEN_EXPIRATION_MAX = CONFIG.CONNECTION_TOKEN_EXPIRATION_MAX + FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 7d2f91c61..04807dc75 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:24858bf247f7af58abda5adb5be733b7b995df2e26de7c91caf43f7aa0dd3be0 -size 139654 +oid sha256:523a93e9703e62c39440d2e172c96fea7d8d04965cab43095fc8a378d157bf59 +size 141798 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index c113ce8ee..722d482d8 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-04-21 14:05+0800\n" +"POT-Creation-Date: 2023-05-16 18:32+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -70,7 +70,7 @@ msgstr "ローカル" msgid "Collected" msgstr "集めました" -#: accounts/const/account.py:21 accounts/serializers/account/account.py:25 +#: accounts/const/account.py:21 accounts/serializers/account/account.py:26 #: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "テンプレート" @@ -87,7 +87,7 @@ msgstr "更新" #: accounts/const/account.py:27 #: accounts/serializers/automations/change_secret.py:156 audits/const.py:53 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +#: ops/const.py:58 terminal/const.py:61 xpack/plugins/cloud/const.py:41 msgid "Failed" msgstr "失敗しました" @@ -179,14 +179,14 @@ msgstr "作成してプッシュ" msgid "Only create" msgstr "作成のみ" -#: accounts/models/account.py:47 +#: accounts/models/account.py:49 #: accounts/models/automations/gather_account.py:16 -#: accounts/serializers/account/account.py:199 -#: accounts/serializers/account/account.py:232 +#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:234 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/models/base.py:100 acls/serializers/base.py:56 +#: acls/models/base.py:100 acls/serializers/base.py:76 #: assets/models/asset/common.py:92 assets/models/asset/common.py:306 #: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19 #: assets/serializers/label.py:27 audits/models.py:48 @@ -198,29 +198,32 @@ msgstr "作成のみ" msgid "Asset" msgstr "資産" -#: accounts/models/account.py:51 accounts/serializers/account/account.py:204 +#: accounts/models/account.py:53 accounts/models/account.py:113 +#: accounts/serializers/account/account.py:206 +#: accounts/serializers/account/account.py:244 +#: accounts/serializers/account/template.py:16 #: authentication/serializers/connect_token_secret.py:49 msgid "Su from" msgstr "から切り替え" -#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: accounts/models/account.py:55 settings/serializers/auth/cas.py:20 #: settings/serializers/auth/feishu.py:20 terminal/models/applet/applet.py:29 msgid "Version" msgstr "バージョン" -#: accounts/models/account.py:55 accounts/serializers/account/account.py:200 -#: users/models/user.py:768 +#: accounts/models/account.py:57 accounts/serializers/account/account.py:202 +#: users/models/user.py:778 msgid "Source" msgstr "ソース" -#: accounts/models/account.py:56 +#: accounts/models/account.py:58 msgid "Source ID" msgstr "ソース ID" -#: accounts/models/account.py:59 +#: accounts/models/account.py:61 #: accounts/serializers/automations/change_secret.py:113 #: accounts/serializers/automations/change_secret.py:133 -#: acls/models/base.py:102 acls/serializers/base.py:57 +#: acls/models/base.py:102 acls/serializers/base.py:77 #: assets/serializers/asset/common.py:125 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 #: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 @@ -229,35 +232,35 @@ msgstr "ソース ID" msgid "Account" msgstr "アカウント" -#: accounts/models/account.py:65 +#: accounts/models/account.py:67 msgid "Can view asset account secret" msgstr "資産アカウントの秘密を表示できます" -#: accounts/models/account.py:66 +#: accounts/models/account.py:68 msgid "Can view asset history account" msgstr "資産履歴アカウントを表示できます" -#: accounts/models/account.py:67 +#: accounts/models/account.py:69 msgid "Can view asset history account secret" msgstr "資産履歴アカウントパスワードを表示できます" -#: accounts/models/account.py:68 +#: accounts/models/account.py:70 msgid "Can verify account" msgstr "アカウントを確認できます" -#: accounts/models/account.py:69 +#: accounts/models/account.py:71 msgid "Can push account" msgstr "アカウントをプッシュできます" -#: accounts/models/account.py:110 +#: accounts/models/account.py:117 msgid "Account template" msgstr "アカウント テンプレート" -#: accounts/models/account.py:115 +#: accounts/models/account.py:122 msgid "Can view asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを表示できます" -#: accounts/models/account.py:116 +#: accounts/models/account.py:123 msgid "Can change asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを変更できます" @@ -276,7 +279,8 @@ msgstr "アカウントバックアップ計画" #: accounts/models/automations/backup_account.py:83 #: assets/models/automations/base.py:115 audits/models.py:55 #: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:192 -#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:109 +#: ops/templates/ops/celery_task_log.html:75 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:137 #: terminal/models/session/session.py:45 #: tickets/models/ticket/apply_application.py:30 #: tickets/models/ticket/apply_asset.py:19 @@ -309,7 +313,7 @@ msgstr "理由" #: accounts/models/automations/backup_account.py:99 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:134 -#: ops/serializers/job.py:72 terminal/serializers/session.py:45 +#: ops/serializers/job.py:56 terminal/serializers/session.py:45 msgid "Is success" msgstr "成功は" @@ -354,7 +358,7 @@ msgid "Can add push account execution" msgstr "プッシュ アカウントの作成の実行" #: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 -#: accounts/serializers/account/account.py:394 +#: accounts/serializers/account/account.py:412 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 #: authentication/serializers/connect_token_secret.py:41 @@ -399,13 +403,14 @@ msgstr "開始日" #: accounts/models/automations/change_secret.py:91 #: assets/models/automations/base.py:116 ops/models/base.py:56 #: ops/models/celery.py:64 ops/models/job.py:193 -#: terminal/models/applet/host.py:110 +#: terminal/models/applet/host.py:138 msgid "Date finished" msgstr "終了日" #: accounts/models/automations/change_secret.py:93 -#: accounts/serializers/account/account.py:234 assets/const/automation.py:8 -#: common/const/choices.py:20 +#: accounts/serializers/account/account.py:236 assets/const/automation.py:8 +#: authentication/views/base.py:29 authentication/views/base.py:30 +#: authentication/views/base.py:31 common/const/choices.py:20 msgid "Error" msgstr "間違い" @@ -423,13 +428,13 @@ msgstr "最終ログイン日" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 -#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: acls/serializers/base.py:19 acls/serializers/base.py:50 #: assets/models/_user.py:23 audits/models.py:157 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:112 -#: users/models/user.py:715 users/templates/users/_msg_user_created.html:12 +#: users/models/user.py:725 users/templates/users/_msg_user_created.html:12 #: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "ユーザー名" @@ -456,8 +461,8 @@ msgid "Triggers" msgstr "トリガー方式" #: accounts/models/automations/push_account.py:16 acls/models/base.py:81 -#: acls/serializers/base.py:81 acls/serializers/login_acl.py:26 -#: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82 +#: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 +#: audits/models.py:65 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:108 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" @@ -472,14 +477,14 @@ msgid "Verify asset account" msgstr "アカウントの確認" #: accounts/models/base.py:33 acls/models/base.py:75 -#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: acls/models/command_acl.py:21 acls/serializers/base.py:35 #: applications/models.py:9 assets/models/_user.py:22 #: assets/models/asset/common.py:90 assets/models/asset/common.py:123 #: assets/models/cmd_filter.py:21 assets/models/domain.py:18 #: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:13 assets/models/platform.py:89 +#: assets/models/platform.py:13 assets/models/platform.py:81 #: assets/serializers/asset/common.py:145 assets/serializers/platform.py:92 -#: assets/serializers/platform.py:193 +#: assets/serializers/platform.py:194 #: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:92 ops/models/playbook.py:23 ops/serializers/job.py:20 @@ -489,7 +494,7 @@ msgstr "アカウントの確認" #: terminal/models/component/endpoint.py:90 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 #: terminal/models/component/terminal.py:84 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:717 +#: users/models/group.py:13 users/models/user.py:727 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名前" @@ -547,28 +552,28 @@ msgstr "" "{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" "情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" -#: accounts/serializers/account/account.py:28 +#: accounts/serializers/account/account.py:29 msgid "Push now" msgstr "今すぐプッシュ" -#: accounts/serializers/account/account.py:35 +#: accounts/serializers/account/account.py:36 msgid "Exist policy" msgstr "アカウントの存在ポリシー" -#: accounts/serializers/account/account.py:179 applications/models.py:11 -#: assets/models/label.py:21 assets/models/platform.py:90 +#: accounts/serializers/account/account.py:181 applications/models.py:11 +#: assets/models/label.py:21 assets/models/platform.py:82 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:110 assets/serializers/platform.py:194 +#: assets/serializers/platform.py:110 assets/serializers/platform.py:195 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "カテゴリ" -#: accounts/serializers/account/account.py:180 +#: accounts/serializers/account/account.py:182 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 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:91 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:83 #: assets/serializers/asset/common.py:122 assets/serializers/platform.py:94 #: assets/serializers/platform.py:109 audits/serializers.py:48 #: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:103 @@ -583,27 +588,27 @@ msgstr "カテゴリ" msgid "Type" msgstr "タイプ" -#: accounts/serializers/account/account.py:195 +#: accounts/serializers/account/account.py:197 msgid "Asset not found" msgstr "資産が存在しません" -#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:203 #: accounts/serializers/account/base.py:64 msgid "Has secret" msgstr "エスクローされたパスワード" -#: accounts/serializers/account/account.py:233 ops/models/celery.py:60 +#: accounts/serializers/account/account.py:235 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:235 +#: accounts/serializers/account/account.py:237 msgid "Changed" msgstr "編集済み" -#: accounts/serializers/account/account.py:241 +#: accounts/serializers/account/account.py:246 #: accounts/serializers/automations/base.py:22 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 ops/models/base.py:17 @@ -612,29 +617,29 @@ msgstr "編集済み" msgid "Assets" msgstr "資産" -#: accounts/serializers/account/account.py:293 +#: accounts/serializers/account/account.py:298 msgid "Account already exists" msgstr "アカウントはすでに存在しています" -#: accounts/serializers/account/account.py:330 +#: accounts/serializers/account/account.py:348 #, python-format msgid "Asset does not support this secret type: %s" msgstr "アセットはアカウント タイプをサポートしていません: %s" -#: accounts/serializers/account/account.py:361 +#: accounts/serializers/account/account.py:379 msgid "Account has exist" msgstr "アカウントはすでに存在しています" -#: accounts/serializers/account/account.py:395 +#: accounts/serializers/account/account.py:413 #: authentication/serializers/connect_token_secret.py:146 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:33 msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:402 acls/models/base.py:98 -#: acls/models/login_acl.py:13 acls/serializers/base.py:55 -#: acls/serializers/login_acl.py:22 assets/models/cmd_filter.py:24 +#: accounts/serializers/account/account.py:420 acls/models/base.py:98 +#: acls/models/login_acl.py:13 acls/serializers/base.py:75 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24 #: assets/models/label.py:16 audits/models.py:44 audits/models.py:63 #: audits/models.py:141 authentication/models/connection_token.py:30 #: authentication/models/sso_token.py:16 @@ -645,12 +650,12 @@ msgstr "ID" #: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 #: terminal/notifications.py:96 terminal/notifications.py:144 #: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 +#: users/const.py:14 users/models/user.py:921 users/models/user.py:952 #: users/serializers/group.py:18 msgid "User" msgstr "ユーザー" -#: accounts/serializers/account/account.py:403 +#: accounts/serializers/account/account.py:421 #: authentication/templates/authentication/_access_key_modal.html:33 #: terminal/notifications.py:98 terminal/notifications.py:146 msgid "Date" @@ -682,10 +687,14 @@ msgid "Key password" msgstr "キーパスワード" #: accounts/serializers/account/base.py:80 -#: assets/serializers/asset/common.py:305 +#: assets/serializers/asset/common.py:306 msgid "Spec info" msgstr "特別情報" +#: accounts/serializers/account/base.py:81 +msgid "Tip: If no username is required for authentication, fill in `null`" +msgstr "ヒント: 認証にユーザー名が必要ない場合は、null を入力してください" + #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:129 assets/models/automations/base.py:18 #: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 @@ -722,8 +731,8 @@ msgstr "自動タスク実行履歴" #: accounts/serializers/automations/change_secret.py:155 audits/const.py:52 #: audits/models.py:54 audits/signal_handlers/activity_log.py:33 -#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39 -#: terminal/const.py:59 terminal/models/session/sharing.py:107 +#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:40 +#: terminal/const.py:60 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" @@ -795,14 +804,14 @@ msgstr "優先順位" msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" -#: acls/models/base.py:82 acls/serializers/base.py:75 -#: acls/serializers/login_acl.py:24 assets/models/cmd_filter.py:86 +#: acls/models/base.py:82 acls/serializers/base.py:95 +#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:80 msgid "Reviewers" msgstr "レビュー担当者" #: acls/models/base.py:83 authentication/models/access_key.py:17 -#: authentication/models/connection_token.py:49 +#: authentication/models/connection_token.py:50 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 @@ -810,7 +819,7 @@ msgid "Active" msgstr "アクティブ" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:71 terminal/const.py:67 +#: ops/serializers/job.py:55 terminal/const.py:68 #: terminal/models/session/session.py:43 terminal/serializers/command.py:18 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -852,7 +861,7 @@ msgstr "コマンドフィルタリング" msgid "Command confirm" msgstr "コマンドの確認" -#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:30 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:28 msgid "Rule" msgstr "ルール" @@ -872,11 +881,11 @@ msgstr "ログインasset acl" msgid "Login asset confirm" msgstr "ログイン資産の確認" -#: acls/serializers/base.py:10 acls/serializers/login_acl.py:17 +#: acls/serializers/base.py:11 acls/serializers/login_acl.py:16 msgid "With * indicating a match all. " msgstr "* はすべて一致することを示します。" -#: acls/serializers/base.py:25 +#: acls/serializers/base.py:26 msgid "" "With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name " @@ -886,35 +895,35 @@ msgstr "" "10.1.1.1-10.1.1.20、2001:db8:2de::e13、2001:db8:1a:1110:::/64 (ドメイン名サ" "ポート)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:19 +#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19 msgid "IP/Host" msgstr "IP/ホスト" -#: acls/serializers/base.py:60 +#: acls/serializers/base.py:80 msgid "User (username)" msgstr "ユーザー (ユーザー名)" -#: acls/serializers/base.py:64 +#: acls/serializers/base.py:84 msgid "Asset (name)" msgstr "資産(名前)" -#: acls/serializers/base.py:68 +#: acls/serializers/base.py:88 msgid "Asset (address)" msgstr "資産(住所)" -#: acls/serializers/base.py:72 +#: acls/serializers/base.py:92 msgid "Account (username)" msgstr "アカウント (ユーザー名)" -#: acls/serializers/base.py:78 acls/serializers/login_acl.py:28 +#: acls/serializers/base.py:98 acls/serializers/login_acl.py:26 msgid "Reviewers amount" msgstr "承認者数" -#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 +#: acls/serializers/base.py:126 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" -#: acls/serializers/base.py:115 +#: acls/serializers/base.py:132 msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" @@ -1009,7 +1018,7 @@ msgid "Unable to connect to port {port} on {address}" msgstr "{port} のポート {address} に接続できません" #: assets/automations/ping_gateway/manager.py:58 -#: authentication/middleware.py:87 xpack/plugins/cloud/providers/fc.py:48 +#: authentication/middleware.py:92 xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "認証に失敗しました" @@ -1102,7 +1111,7 @@ msgstr "ファイアウォール" msgid "Other" msgstr "その他" -#: assets/const/types.py:218 +#: assets/const/types.py:223 msgid "All types" msgstr "いろんなタイプ" @@ -1140,11 +1149,11 @@ msgstr "SSHパブリックキー" #: assets/models/cmd_filter.py:88 assets/models/group.py:23 #: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:111 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:184 -#: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24 +#: terminal/models/applet/applet.py:37 terminal/models/applet/applet.py:218 +#: terminal/models/applet/host.py:139 terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:47 tickets/models/comment.py:32 -#: tickets/models/ticket/general.py:297 users/models/user.py:756 +#: tickets/models/ticket/general.py:297 users/models/user.py:766 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:111 msgid "Comment" msgstr "コメント" @@ -1152,18 +1161,18 @@ msgstr "コメント" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 #: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:191 -#: users/models/user.py:943 +#: users/models/user.py:953 msgid "Date created" msgstr "作成された日付" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:36 users/models/user.py:777 +#: common/db/models.py:36 users/models/user.py:787 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:21 -#: common/db/models.py:33 users/models/user.py:763 +#: common/db/models.py:33 users/models/user.py:773 #: users/serializers/group.py:31 msgid "Created by" msgstr "によって作成された" @@ -1253,7 +1262,7 @@ msgstr "ポート" msgid "Address" msgstr "アドレス" -#: assets/models/asset/common.py:125 assets/models/platform.py:120 +#: assets/models/asset/common.py:125 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:196 @@ -1262,7 +1271,7 @@ msgstr "プラットフォーム" #: assets/models/asset/common.py:127 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 -#: perms/serializers/user_permission.py:28 +#: perms/serializers/user_permission.py:29 msgid "Domain" msgstr "ドメイン" @@ -1270,7 +1279,7 @@ msgstr "ドメイン" msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:132 assets/serializers/asset/common.py:306 +#: assets/models/asset/common.py:132 assets/serializers/asset/common.py:307 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "資産ハードウェア情報の収集" @@ -1337,7 +1346,7 @@ msgid "Submit selector" msgstr "ボタンセレクターを確認する" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:304 rbac/tree.py:35 +#: assets/serializers/asset/common.py:305 rbac/tree.py:35 msgid "Accounts" msgstr "アカウント" @@ -1355,7 +1364,7 @@ msgstr "アセットの自動化タスク" #: assets/models/automations/base.py:113 audits/models.py:177 #: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:184 -#: terminal/models/applet/applet.py:183 terminal/models/applet/host.py:108 +#: terminal/models/applet/applet.py:217 terminal/models/applet/host.py:136 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1382,7 +1391,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:723 +#: users/models/user.py:733 msgid "User group" msgstr "ユーザーグループ" @@ -1432,7 +1441,7 @@ msgstr "デフォルト" msgid "Default asset group" msgstr "デフォルトアセットグループ" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:928 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:938 msgid "System" msgstr "システム" @@ -1448,7 +1457,8 @@ msgstr "値" #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: assets/serializers/platform.py:93 #: authentication/serializers/connect_token_secret.py:113 -#: common/serializers/common.py:85 settings/serializers/sms.py:7 +#: common/serializers/common.py:85 perms/serializers/user_permission.py:28 +#: settings/serializers/sms.py:7 msgid "Label" msgstr "ラベル" @@ -1498,102 +1508,102 @@ msgstr "開ける" msgid "Setting" msgstr "設定" -#: assets/models/platform.py:39 audits/const.py:47 settings/models.py:37 +#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37 #: terminal/serializers/applet_host.py:29 msgid "Enabled" msgstr "有効化" -#: assets/models/platform.py:40 +#: assets/models/platform.py:32 msgid "Ansible config" msgstr "Ansible 構成" -#: assets/models/platform.py:42 assets/serializers/platform.py:63 +#: assets/models/platform.py:34 assets/serializers/platform.py:63 msgid "Ping enabled" msgstr "アセット ディスカバリを有効にする" -#: assets/models/platform.py:43 assets/serializers/platform.py:64 +#: assets/models/platform.py:35 assets/serializers/platform.py:64 msgid "Ping method" msgstr "資産検出方法" -#: assets/models/platform.py:44 +#: assets/models/platform.py:36 msgid "Ping params" msgstr "資産検出パラメータ" -#: assets/models/platform.py:46 assets/models/platform.py:70 +#: assets/models/platform.py:38 assets/models/platform.py:62 #: assets/serializers/platform.py:65 msgid "Gather facts enabled" msgstr "資産情報の収集を有効にする" -#: assets/models/platform.py:48 assets/models/platform.py:72 +#: assets/models/platform.py:40 assets/models/platform.py:64 #: assets/serializers/platform.py:66 msgid "Gather facts method" msgstr "情報収集の方法" -#: assets/models/platform.py:50 assets/models/platform.py:74 +#: assets/models/platform.py:42 assets/models/platform.py:66 msgid "Gather facts params" msgstr "情報収集パラメータ" -#: assets/models/platform.py:52 assets/serializers/platform.py:69 +#: assets/models/platform.py:44 assets/serializers/platform.py:69 msgid "Change secret enabled" msgstr "パスワードの変更が有効" -#: assets/models/platform.py:54 assets/serializers/platform.py:70 +#: assets/models/platform.py:46 assets/serializers/platform.py:70 msgid "Change secret method" msgstr "パスワード変更モード" -#: assets/models/platform.py:56 +#: assets/models/platform.py:48 msgid "Change secret params" msgstr "パスワード変更パラメータ" -#: assets/models/platform.py:58 assets/serializers/platform.py:71 +#: assets/models/platform.py:50 assets/serializers/platform.py:71 msgid "Push account enabled" msgstr "アカウントのプッシュを有効にする" -#: assets/models/platform.py:60 assets/serializers/platform.py:72 +#: assets/models/platform.py:52 assets/serializers/platform.py:72 msgid "Push account method" msgstr "アカウントプッシュ方式" -#: assets/models/platform.py:62 +#: assets/models/platform.py:54 msgid "Push account params" msgstr "アカウントプッシュパラメータ" -#: assets/models/platform.py:64 assets/serializers/platform.py:67 +#: assets/models/platform.py:56 assets/serializers/platform.py:67 msgid "Verify account enabled" msgstr "アカウントの確認をオンにする" -#: assets/models/platform.py:66 assets/serializers/platform.py:68 +#: assets/models/platform.py:58 assets/serializers/platform.py:68 msgid "Verify account method" msgstr "アカウント認証方法" -#: assets/models/platform.py:68 +#: assets/models/platform.py:60 msgid "Verify account params" msgstr "アカウント認証パラメータ" -#: assets/models/platform.py:92 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:84 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:93 +#: assets/models/platform.py:85 msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:97 assets/serializers/platform.py:108 +#: assets/models/platform.py:89 assets/serializers/platform.py:108 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:99 assets/serializers/platform.py:135 +#: assets/models/platform.py:91 assets/serializers/platform.py:136 msgid "Domain enabled" msgstr "ドメインを有効にする" -#: assets/models/platform.py:101 assets/serializers/platform.py:134 +#: assets/models/platform.py:93 assets/serializers/platform.py:135 msgid "Su enabled" msgstr "アカウントの切り替えを有効にする" -#: assets/models/platform.py:102 assets/serializers/platform.py:114 +#: assets/models/platform.py:94 assets/serializers/platform.py:114 msgid "Su method" msgstr "アカウントの切り替え方法" -#: assets/models/platform.py:103 assets/serializers/platform.py:117 +#: assets/models/platform.py:95 assets/serializers/platform.py:117 msgid "Custom fields" msgstr "カスタムフィールド" @@ -1623,23 +1633,29 @@ msgid "Node path" msgstr "ノードパスです" #: assets/serializers/asset/common.py:144 -#: assets/serializers/asset/common.py:307 +#: assets/serializers/asset/common.py:308 msgid "Auto info" msgstr "自動情報" -#: assets/serializers/asset/common.py:226 +#: assets/serializers/asset/common.py:227 msgid "Platform not exist" msgstr "プラットフォームが存在しません" -#: assets/serializers/asset/common.py:262 +#: assets/serializers/asset/common.py:263 msgid "port out of range (1-65535)" msgstr "ポート番号が範囲外です (1-65535)" -#: assets/serializers/asset/common.py:269 +#: assets/serializers/asset/common.py:270 msgid "Protocol is required: {}" msgstr "プロトコルが必要です: {}" -#: assets/serializers/asset/database.py:25 common/serializers/fields.py:103 +#: assets/serializers/asset/database.py:13 +#, fuzzy +#| msgid "Default storage" +msgid "Default database" +msgstr "デフォルトのストレージ" + +#: assets/serializers/asset/database.py:28 common/serializers/fields.py:103 #: tickets/serializers/ticket/common.py:58 #: xpack/plugins/cloud/serializers/account_attrs.py:56 #: xpack/plugins/cloud/serializers/account_attrs.py:79 @@ -1757,15 +1773,15 @@ msgstr "" msgid "Automation" msgstr "オートメーション" -#: assets/serializers/platform.py:136 +#: assets/serializers/platform.py:137 msgid "Default Domain" msgstr "デフォルト ドメイン" -#: assets/serializers/platform.py:145 +#: assets/serializers/platform.py:146 msgid "type is required" msgstr "タイプ このフィールドは必須です." -#: assets/serializers/platform.py:182 +#: assets/serializers/platform.py:183 msgid "Protocols is required" msgstr "同意が必要です" @@ -1921,7 +1937,7 @@ msgstr "セッションログ" msgid "Login log" msgstr "ログインログ" -#: audits/const.py:42 terminal/models/applet/host.py:112 +#: audits/const.py:42 terminal/models/applet/host.py:140 #: terminal/models/component/task.py:24 msgid "Task" msgstr "タスク" @@ -2017,7 +2033,7 @@ msgstr "ユーザーエージェント" #: audits/models.py:169 audits/serializers.py:47 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:740 +#: users/forms/profile.py:65 users/models/user.py:750 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" @@ -2071,22 +2087,24 @@ msgid "Auth Token" msgstr "認証トークン" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:74 authentication/views/wecom.py:177 +#: authentication/views/login.py:74 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:778 +#: users/models/user.py:680 users/models/user.py:788 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:144 +#: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:123 #: authentication/views/login.py:86 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 -#: settings/serializers/auth/feishu.py:13 users/models/user.py:780 +#: settings/serializers/auth/feishu.py:13 users/models/user.py:682 +#: users/models/user.py:790 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:179 +#: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:160 #: authentication/views/login.py:80 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:779 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:681 +#: users/models/user.py:789 msgid "DingTalk" msgstr "DingTalk" @@ -2103,19 +2121,19 @@ msgstr "監査セッション タスク ログのクリーンアップ" msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" -#: authentication/api/connection_token.py:296 +#: authentication/api/connection_token.py:303 msgid "Account not found" msgstr "アカウントが見つかりません" -#: authentication/api/connection_token.py:299 +#: authentication/api/connection_token.py:306 msgid "Permission expired" msgstr "承認の有効期限が切れています" -#: authentication/api/connection_token.py:311 +#: authentication/api/connection_token.py:318 msgid "ACL action is reject" msgstr "ACL アクションは拒否です" -#: authentication/api/connection_token.py:315 +#: authentication/api/connection_token.py:322 msgid "ACL action is review" msgstr "ACL アクションはレビューです" @@ -2123,7 +2141,7 @@ msgstr "ACL アクションはレビューです" msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" -#: authentication/api/password.py:31 terminal/api/session/session.py:247 +#: authentication/api/password.py:31 terminal/api/session/session.py:249 #: users/views/profile/reset.py:44 msgid "User does not exist: {}" msgstr "ユーザーが存在しない: {}" @@ -2347,21 +2365,21 @@ msgstr "電話が設定されていない" msgid "SSO auth closed" msgstr "SSO authは閉鎖されました" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:61 msgid "WeCom is already bound" msgstr "企業の微信はすでにバインドされています" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 -#: authentication/views/wecom.py:290 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:202 +#: authentication/views/wecom.py:244 msgid "WeCom is not bound" msgstr "企業の微信をバインドしていません" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 -#: authentication/views/dingtalk.py:296 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:210 +#: authentication/views/dingtalk.py:252 msgid "DingTalk is not bound" msgstr "DingTalkはバインドされていません" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:167 msgid "FeiShu is not bound" msgstr "本を飛ばすは拘束されていません" @@ -2369,15 +2387,15 @@ msgstr "本を飛ばすは拘束されていません" msgid "Your password is invalid" msgstr "パスワードが無効です" -#: authentication/errors/redirect.py:85 authentication/mixins.py:307 +#: authentication/errors/redirect.py:85 authentication/mixins.py:316 msgid "Your password is too simple, please change it for security" msgstr "パスワードがシンプルすぎるので、セキュリティのために変更してください" -#: authentication/errors/redirect.py:93 authentication/mixins.py:314 +#: authentication/errors/redirect.py:93 authentication/mixins.py:323 msgid "You should to change your password before login" msgstr "ログインする前にパスワードを変更する必要があります" -#: authentication/errors/redirect.py:101 authentication/mixins.py:321 +#: authentication/errors/redirect.py:101 authentication/mixins.py:330 msgid "Your password has expired, please reset before logging in" msgstr "" "パスワードの有効期限が切れました。ログインする前にリセットしてください。" @@ -2476,15 +2494,23 @@ msgstr "電話番号を設定して有効にする" msgid "Clear phone number to disable" msgstr "無効にする電話番号をクリアする" -#: authentication/middleware.py:88 settings/utils/ldap.py:652 +#: authentication/middleware.py:93 settings/utils/ldap.py:652 msgid "Authentication failed (before login check failed): {}" msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" -#: authentication/mixins.py:257 +#: authentication/mixins.py:91 +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:266 msgid "The MFA type ({}) is not enabled" msgstr "MFAタイプ ({}) が有効になっていない" -#: authentication/mixins.py:297 +#: authentication/mixins.py:306 msgid "Please change your password" msgstr "パスワードを変更してください" @@ -2498,7 +2524,7 @@ msgid "Input username" msgstr "カスタム ユーザー名" #: authentication/models/connection_token.py:38 -#: authentication/serializers/connection_token.py:17 +#: authentication/serializers/connection_token.py:20 msgid "Input secret" msgstr "カスタムパスワード" @@ -2516,34 +2542,40 @@ msgid "Asset display" msgstr "アセット名" #: authentication/models/connection_token.py:43 +#, fuzzy +#| msgid "Disable" +msgid "Reusable" +msgstr "無効化" + +#: authentication/models/connection_token.py:44 #: 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:761 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:771 msgid "Date expired" msgstr "期限切れの日付" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:48 #: perms/models/asset_permission.py:77 msgid "From ticket" msgstr "チケットから" -#: authentication/models/connection_token.py:53 +#: authentication/models/connection_token.py:54 msgid "Connection token" msgstr "接続トークン" -#: authentication/models/connection_token.py:55 +#: authentication/models/connection_token.py:56 msgid "Can view connection token secret" msgstr "接続トークンの秘密を表示できます" -#: authentication/models/connection_token.py:102 +#: authentication/models/connection_token.py:103 msgid "Connection token inactive" msgstr "接続トークンがアクティブ化されていません" -#: authentication/models/connection_token.py:105 +#: authentication/models/connection_token.py:106 msgid "Connection token expired at: {}" msgstr "接続トークンの有効期限: {}" -#: authentication/models/connection_token.py:108 +#: authentication/models/connection_token.py:109 msgid "No user or invalid user" msgstr "ユーザーなしまたは期限切れのユーザー" @@ -2595,15 +2627,15 @@ msgstr "コンポーネント" msgid "Expired now" msgstr "すぐに期限切れ" -#: authentication/serializers/connection_token.py:15 +#: authentication/serializers/connection_token.py:18 msgid "Expired time" msgstr "期限切れ時間" -#: authentication/serializers/connection_token.py:19 +#: authentication/serializers/connection_token.py:22 msgid "Ticket info" msgstr "作業指示情報" -#: authentication/serializers/connection_token.py:20 +#: authentication/serializers/connection_token.py:23 #: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 #: perms/serializers/permission.py:69 #: tickets/models/ticket/apply_application.py:28 @@ -2611,17 +2643,21 @@ msgstr "作業指示情報" msgid "Actions" msgstr "アクション" -#: authentication/serializers/connection_token.py:41 +#: authentication/serializers/connection_token.py:44 #: perms/serializers/permission.py:38 perms/serializers/permission.py:70 #: users/serializers/user.py:97 users/serializers/user.py:172 msgid "Is expired" msgstr "期限切れです" +#: authentication/serializers/connection_token.py:79 +msgid "Reusable connection token is not allowed, global setting not enabled" +msgstr "" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 #: settings/serializers/email.py:50 users/forms/profile.py:102 -#: users/forms/profile.py:106 users/models/user.py:719 +#: users/forms/profile.py:106 users/models/user.py:729 #: users/templates/users/forgot_password.html:116 #: users/views/profile/reset.py:73 msgid "Email" @@ -2711,7 +2747,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:417 +#: jumpserver/conf.py:419 #: 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 @@ -2863,76 +2899,77 @@ msgstr "コピー成功" msgid "LAN" msgstr "ローカルエリアネットワーク" -#: authentication/views/dingtalk.py:41 +#: authentication/views/base.py:64 +#: perms/templates/perms/_msg_permed_items_expire.html:21 +msgid "If you have any question, please contact the administrator" +msgstr "質問があったら、管理者に連絡して下さい" + +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "DingTalkエラー、システム管理者に連絡してください" -#: authentication/views/dingtalk.py:44 +#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:209 msgid "DingTalk Error" msgstr "DingTalkエラー" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 -#: authentication/views/wecom.py:55 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:57 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "システム設定が正しくありません。管理者に連絡してください" -#: authentication/views/dingtalk.py:80 +#: authentication/views/dingtalk.py:61 msgid "DingTalk is already bound" msgstr "DingTalkはすでにバインドされています" -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 +#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:129 msgid "Invalid user_id" msgstr "無効なuser_id" -#: authentication/views/dingtalk.py:164 +#: authentication/views/dingtalk.py:145 msgid "DingTalk query user failed" msgstr "DingTalkクエリユーザーが失敗しました" -#: authentication/views/dingtalk.py:173 +#: authentication/views/dingtalk.py:154 msgid "The DingTalk is already bound to another user" msgstr "DingTalkはすでに別のユーザーにバインドされています" -#: authentication/views/dingtalk.py:180 +#: authentication/views/dingtalk.py:161 msgid "Binding DingTalk successfully" msgstr "DingTalkのバインドに成功" -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 +#: authentication/views/dingtalk.py:211 authentication/views/dingtalk.py:246 msgid "Failed to get user from DingTalk" msgstr "DingTalkからユーザーを取得できませんでした" -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 +#: authentication/views/dingtalk.py:253 msgid "Please login with a password and then bind the DingTalk" msgstr "パスワードでログインし、DingTalkをバインドしてください" -#: authentication/views/feishu.py:39 +#: authentication/views/feishu.py:39 authentication/views/feishu.py:166 msgid "FeiShu Error" msgstr "FeiShuエラー" -#: authentication/views/feishu.py:87 +#: authentication/views/feishu.py:67 msgid "FeiShu is already bound" msgstr "FeiShuはすでにバインドされています" -#: authentication/views/feishu.py:129 +#: authentication/views/feishu.py:108 msgid "FeiShu query user failed" msgstr "FeiShuクエリユーザーが失敗しました" -#: authentication/views/feishu.py:138 +#: authentication/views/feishu.py:117 msgid "The FeiShu is already bound to another user" msgstr "FeiShuはすでに別のユーザーにバインドされています" -#: authentication/views/feishu.py:145 +#: authentication/views/feishu.py:124 msgid "Binding FeiShu successfully" msgstr "本を飛ばすのバインドに成功" -#: authentication/views/feishu.py:197 +#: authentication/views/feishu.py:168 msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/feishu.py:204 -msgid "Please login with a password and then bind the FeiShu" -msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" - #: authentication/views/login.py:182 msgid "Redirecting" msgstr "リダイレクト" @@ -2969,31 +3006,31 @@ msgstr "ログアウト成功" msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" -#: authentication/views/wecom.py:40 +#: authentication/views/wecom.py:42 msgid "WeCom Error, Please contact your system administrator" msgstr "企業微信エラー、システム管理者に連絡してください" -#: authentication/views/wecom.py:43 +#: authentication/views/wecom.py:45 authentication/views/wecom.py:201 msgid "WeCom Error" msgstr "企業微信エラー" -#: authentication/views/wecom.py:162 +#: authentication/views/wecom.py:144 msgid "WeCom query user failed" msgstr "企業微信ユーザーの問合せに失敗しました" -#: authentication/views/wecom.py:171 +#: authentication/views/wecom.py:153 msgid "The WeCom is already bound to another user" msgstr "この企業の微信はすでに他のユーザーをバインドしている。" -#: authentication/views/wecom.py:178 +#: authentication/views/wecom.py:160 msgid "Binding WeCom successfully" msgstr "企業の微信のバインドに成功" -#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 +#: authentication/views/wecom.py:203 authentication/views/wecom.py:238 msgid "Failed to get user from WeCom" msgstr "企業の微信からユーザーを取得できませんでした" -#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 +#: authentication/views/wecom.py:245 msgid "Please login with a password and then bind the WeCom" msgstr "パスワードでログインしてからWeComをバインドしてください" @@ -3013,7 +3050,7 @@ msgstr "タイミングトリガー" msgid "Ready" msgstr "の準備を" -#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: common/const/choices.py:16 terminal/const.py:59 tickets/const.py:29 #: tickets/const.py:39 msgid "Pending" msgstr "未定" @@ -3076,7 +3113,7 @@ msgstr "は破棄されます" msgid "discard time" msgstr "時間を捨てる" -#: common/db/models.py:34 users/models/user.py:764 +#: common/db/models.py:34 users/models/user.py:774 msgid "Updated by" msgstr "によって更新" @@ -3104,6 +3141,14 @@ msgstr "解析ファイルエラー: {}" msgid "Invalid excel file" msgstr "無効 excel 書類" +#: common/drf/renders/base.py:209 +msgid "" +"{} - The encryption password has not been set - please go to personal " +"information -> file encryption password to set the encryption password" +msgstr "" +"{} - 暗号化パスワードが設定されていません-個人情報->ファイル暗号化パスワード" +"に暗号化パスワードを設定してください" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -3145,7 +3190,7 @@ msgstr "サポートされていません Elasticsearch8" msgid "Network error, please contact system administrator" msgstr "ネットワークエラー、システム管理者に連絡してください" -#: common/sdk/im/wecom/__init__.py:15 +#: common/sdk/im/wecom/__init__.py:16 msgid "WeCom error, please contact system administrator" msgstr "企業微信エラー、システム管理者に連絡してください" @@ -3274,11 +3319,11 @@ msgstr "検索のエクスポート: %s" msgid "User %s view/export secret" msgstr "ユーザー %s がパスワードを閲覧/導き出しました" -#: jumpserver/conf.py:416 +#: jumpserver/conf.py:418 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:418 +#: jumpserver/conf.py:420 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -3346,15 +3391,15 @@ msgstr "システムメッセージ" msgid "Publish the station message" msgstr "投稿サイトニュース" -#: ops/ansible/inventory.py:77 +#: ops/ansible/inventory.py:82 msgid "No account available" msgstr "利用可能なアカウントがありません" -#: ops/ansible/inventory.py:236 +#: ops/ansible/inventory.py:247 msgid "Ansible disabled" msgstr "Ansible 無効" -#: ops/ansible/inventory.py:252 +#: ops/ansible/inventory.py:263 msgid "Skip hosts below:" msgstr "次のホストをスキップします: " @@ -3620,15 +3665,15 @@ msgstr "{max_threshold} を超えるCPUロード: => {value}" msgid "Run after save" msgstr "保存後に実行" -#: ops/serializers/job.py:70 +#: ops/serializers/job.py:54 msgid "Job type" msgstr "タスクの種類" -#: ops/serializers/job.py:73 terminal/serializers/session.py:49 +#: ops/serializers/job.py:57 terminal/serializers/session.py:49 msgid "Is finished" msgstr "終了しました" -#: ops/serializers/job.py:74 +#: ops/serializers/job.py:58 msgid "Time cost" msgstr "時を過ごす" @@ -3660,6 +3705,10 @@ msgstr "例外ジョブのクリーンアップ" msgid "Task log" msgstr "タスクログ" +#: ops/templates/ops/celery_task_log.html:71 +msgid "Task name" +msgstr "タスク名" + #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "JumpServerの現在のユーザーのユーザー名" @@ -3846,10 +3895,6 @@ msgstr "" " 次の %(item_type)s は %(count)s 日以内に期限切れになります\n" " " -#: perms/templates/perms/_msg_permed_items_expire.html:21 -msgid "If you have any question, please contact the administrator" -msgstr "質問があったら、管理者に連絡して下さい" - #: rbac/api/role.py:35 msgid "Internal role, can't be destroy" msgstr "内部の役割は、破壊することはできません" @@ -3928,7 +3973,7 @@ msgid "Scope" msgstr "スコープ" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:727 +#: users/models/user.py:737 msgid "Role" msgstr "ロール" @@ -3944,22 +3989,22 @@ msgstr "組織の役割" msgid "Role binding" msgstr "ロールバインディング" -#: rbac/models/rolebinding.py:145 +#: rbac/models/rolebinding.py:153 msgid "All organizations" msgstr "全ての組織" -#: rbac/models/rolebinding.py:174 +#: rbac/models/rolebinding.py:182 msgid "" "User last role in org, can not be delete, you can remove user from org " "instead" msgstr "" "ユーザーの最後のロールは削除できません。ユーザーを組織から削除できます。" -#: rbac/models/rolebinding.py:181 +#: rbac/models/rolebinding.py:189 msgid "Organization role binding" msgstr "組織の役割バインディング" -#: rbac/models/rolebinding.py:196 +#: rbac/models/rolebinding.py:204 msgid "System role binding" msgstr "システムロールバインディング" @@ -4035,8 +4080,8 @@ msgstr "タスクセンター" msgid "My assets" msgstr "私の資産" -#: rbac/tree.py:56 terminal/models/applet/applet.py:43 -#: terminal/models/applet/applet.py:180 terminal/models/applet/host.py:28 +#: rbac/tree.py:56 terminal/models/applet/applet.py:44 +#: terminal/models/applet/applet.py:214 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "リモートアプリケーション" @@ -4907,24 +4952,42 @@ msgid "Only single device login" msgstr "単一デバイスログインのみ" #: settings/serializers/security.py:97 -msgid "Next device login, pre login will be logout" -msgstr "次のデバイスログイン、事前ログインはログアウトになります" +msgid "" +"After the user logs in on the new device, other logged-in devices will " +"automatically log out" +msgstr "" +"ユーザーが新しいデバイスにログインすると、ログインしている他のデバイスは自動" +"的にログアウトします。" #: settings/serializers/security.py:100 msgid "Only exist user login" msgstr "ユーザーログインのみ存在" #: settings/serializers/security.py:101 -msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet" -msgstr "Enableの場合、ユーザーがまだ存在しない場合、CAS、OIDC authは失敗します" +msgid "" +"If enabled, non-existent users will not be allowed to log in; if disabled, " +"users of other authentication methods except local authentication methods " +"are allowed to log in and automatically create users (if the user does not " +"exist)" +msgstr "" +"有効にすると、存在しないユーザーはログインできなくなります。無効にすると、" +"ローカル認証方法を除く他の認証方法のユーザーはログインでき、ユーザーが自動的" +"に作成されます (ユーザーが存在しない場合)。" #: settings/serializers/security.py:104 msgid "Only from source login" msgstr "ソースログインからのみ" #: settings/serializers/security.py:105 -msgid "Only log in from the user source property" -msgstr "ユーザーソースのプロパティからのみログイン" +msgid "" +"If it is enabled, the user will only authenticate to the source when logging " +"in; if it is disabled, the user will authenticate all the enabled " +"authentication methods in a certain order when logging in, and as long as " +"one of the authentication methods is successful, they can log in directly" +msgstr "" +"これが有効な場合、ユーザーはログイン時にソースに対してのみ認証されます。無効" +"な場合、ユーザーはログイン時に、いずれかの認証方法が成功する限り、有効なすべ" +"ての認証方法を特定の順序で認証します。 、直接ログインできます" #: settings/serializers/security.py:109 msgid "MFA verify TTL" @@ -5476,15 +5539,15 @@ msgstr "テスト失敗: アカウントが無効" msgid "Have online sessions" msgstr "オンラインセッションを持つ" -#: terminal/api/session/session.py:239 +#: terminal/api/session/session.py:241 msgid "Session does not exist: {}" msgstr "セッションが存在しません: {}" -#: terminal/api/session/session.py:242 +#: terminal/api/session/session.py:244 msgid "Session is finished or the protocol not supported" msgstr "セッションが終了したか、プロトコルがサポートされていません" -#: terminal/api/session/session.py:255 +#: terminal/api/session/session.py:257 msgid "User does not have permission" msgstr "ユーザーに権限がありません" @@ -5537,7 +5600,7 @@ msgstr "クリティカル" msgid "High" msgstr "高い" -#: terminal/const.py:32 terminal/const.py:65 +#: terminal/const.py:32 terminal/const.py:66 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" @@ -5546,19 +5609,19 @@ msgstr "正常" msgid "Offline" msgstr "オフライン" -#: terminal/const.py:61 +#: terminal/const.py:62 msgid "Mismatch" msgstr "一致しない" -#: terminal/const.py:66 +#: terminal/const.py:67 msgid "Tunnel" msgstr "" -#: terminal/const.py:71 +#: terminal/const.py:72 msgid "Read Only" msgstr "読み取り専用" -#: terminal/const.py:72 +#: terminal/const.py:73 msgid "Writable" msgstr "書き込み可能" @@ -5575,31 +5638,37 @@ msgid "Author" msgstr "著者" #: terminal/models/applet/applet.py:35 +#, fuzzy +#| msgid "Can push account" +msgid "Can concurrent" +msgstr "アカウントをプッシュできます" + +#: terminal/models/applet/applet.py:36 msgid "Tags" msgstr "ラベル" -#: terminal/models/applet/applet.py:39 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:40 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "ホスト" -#: terminal/models/applet/applet.py:84 +#: terminal/models/applet/applet.py:85 msgid "Applet pkg not valid, Missing file {}" msgstr "無効なアプレット パッケージ、ファイル {} がありません" -#: terminal/models/applet/applet.py:103 +#: terminal/models/applet/applet.py:104 msgid "Load platform.yml failed: {}" msgstr "" -#: terminal/models/applet/applet.py:106 +#: terminal/models/applet/applet.py:107 msgid "Only support custom platform" msgstr "" -#: terminal/models/applet/applet.py:111 +#: terminal/models/applet/applet.py:112 msgid "Missing type in platform.yml" msgstr "" -#: terminal/models/applet/applet.py:182 terminal/models/applet/host.py:34 -#: terminal/models/applet/host.py:106 +#: terminal/models/applet/applet.py:216 terminal/models/applet/host.py:34 +#: terminal/models/applet/host.py:134 msgid "Hosting" msgstr "ホスト マシン" @@ -5619,7 +5688,7 @@ msgstr "" msgid "Date synced" msgstr "同期日" -#: terminal/models/applet/host.py:107 +#: terminal/models/applet/host.py:135 msgid "Initial" msgstr "初期化" @@ -6058,7 +6127,7 @@ msgstr "端末の状態を定期的にクリーンアップする" #: terminal/tasks.py:37 msgid "Clean orphan session" -msgstr "孤立したセッションをクリアする" +msgstr "オフライン セッションをクリアする" #: terminal/tasks.py:56 msgid "Upload session replay to external storage" @@ -6072,6 +6141,12 @@ msgstr "アプリケーション マシンの展開を実行する" msgid "Install applet" msgstr "アプリをインストールする" +#: terminal/tasks.py:104 +#, fuzzy +#| msgid "Gather assets accounts" +msgid "Generate applet host accounts" +msgstr "資産の口座番号を収集する" + #: terminal/templates/terminal/_msg_command_alert.html:10 msgid "view" msgstr "表示" @@ -6434,7 +6509,7 @@ msgstr "無効な承認アクション" msgid "This user is not authorized to approve this ticket" msgstr "このユーザーはこの作業指示を承認する権限がありません" -#: users/api/user.py:182 +#: users/api/user.py:185 msgid "Could not reset self otp, use profile reset instead" msgstr "自己otpをリセットできませんでした、代わりにプロファイルリセットを使用" @@ -6545,7 +6620,7 @@ msgstr "公開鍵は古いものと同じであってはなりません。" msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:170 users/models/user.py:750 +#: users/forms/profile.py:170 users/models/user.py:760 msgid "Public key" msgstr "公開キー" @@ -6553,68 +6628,68 @@ msgstr "公開キー" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:729 users/serializers/user.py:171 +#: users/models/user.py:739 users/serializers/user.py:171 msgid "Is service account" msgstr "サービスアカウントです" -#: users/models/user.py:731 +#: users/models/user.py:741 msgid "Avatar" msgstr "アバター" -#: users/models/user.py:734 +#: users/models/user.py:744 msgid "Wechat" msgstr "微信" -#: users/models/user.py:737 users/serializers/user.py:109 +#: users/models/user.py:747 users/serializers/user.py:109 msgid "Phone" msgstr "電話" -#: users/models/user.py:743 +#: users/models/user.py:753 msgid "OTP secret key" msgstr "OTP 秘密" -#: users/models/user.py:747 +#: users/models/user.py:757 msgid "Private key" msgstr "ssh秘密鍵" -#: users/models/user.py:753 +#: users/models/user.py:763 msgid "Secret key" msgstr "秘密キー" -#: users/models/user.py:758 users/serializers/profile.py:149 +#: users/models/user.py:768 users/serializers/profile.py:149 #: users/serializers/user.py:168 msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:772 +#: users/models/user.py:782 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:775 +#: users/models/user.py:785 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:913 +#: users/models/user.py:923 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:914 +#: users/models/user.py:924 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:915 +#: users/models/user.py:925 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:924 +#: users/models/user.py:934 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:927 +#: users/models/user.py:937 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:952 +#: users/models/user.py:962 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -6717,6 +6792,15 @@ msgstr "セキュリティのために、複数のユーザーのみをリスト msgid "name not unique" msgstr "名前が一意ではない" +#: users/signal_handlers.py:27 +msgid "" +"The administrator has enabled \"Only allow existing users to log in\", \n" +" and the current user is not in the user list. Please contact the " +"administrator." +msgstr "" +"管理者は「既存のユーザーのみログインを許可」をオンにしており、現在のユーザー" +"はユーザーリストにありません。管理者に連絡してください。" + #: users/tasks.py:21 msgid "Check password expired" msgstr "パスワードの有効期限が切れていることを確認する" @@ -7559,3 +7643,11 @@ msgstr "究極のエディション" #: xpack/plugins/license/models.py:86 msgid "Community edition" msgstr "コミュニティ版" + +#, fuzzy +#~| msgid "Trigger mode" +#~ msgid "Trigger type" +#~ msgstr "トリガーモード" + +#~ msgid "Please login with a password and then bind the FeiShu" +#~ msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 521993edd..ac7231fc1 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:0788b48bc50cffe3e7ff83803ef0edadfc120c9165bfe6bccd1f896d8bf39397 -size 114419 +oid sha256:bd60ca8b6c43b9b5940b14a8ca8073ae26062a5402f663ac39043cbc669199bd +size 116040 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 4683b46c6..91582121a 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-04-21 14:05+0800\n" +"POT-Creation-Date: 2023-05-16 18:32+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -69,7 +69,7 @@ msgstr "数据库" msgid "Collected" msgstr "收集" -#: accounts/const/account.py:21 accounts/serializers/account/account.py:25 +#: accounts/const/account.py:21 accounts/serializers/account/account.py:26 #: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "模板" @@ -86,7 +86,7 @@ msgstr "更新" #: accounts/const/account.py:27 #: accounts/serializers/automations/change_secret.py:156 audits/const.py:53 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +#: ops/const.py:58 terminal/const.py:61 xpack/plugins/cloud/const.py:41 msgid "Failed" msgstr "失败" @@ -178,14 +178,14 @@ msgstr "创建并推送" msgid "Only create" msgstr "仅创建" -#: accounts/models/account.py:47 +#: accounts/models/account.py:49 #: accounts/models/automations/gather_account.py:16 -#: accounts/serializers/account/account.py:199 -#: accounts/serializers/account/account.py:232 +#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:234 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/models/base.py:100 acls/serializers/base.py:56 +#: acls/models/base.py:100 acls/serializers/base.py:76 #: assets/models/asset/common.py:92 assets/models/asset/common.py:306 #: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19 #: assets/serializers/label.py:27 audits/models.py:48 @@ -197,29 +197,32 @@ msgstr "仅创建" msgid "Asset" msgstr "资产" -#: accounts/models/account.py:51 accounts/serializers/account/account.py:204 +#: accounts/models/account.py:53 accounts/models/account.py:113 +#: accounts/serializers/account/account.py:206 +#: accounts/serializers/account/account.py:244 +#: accounts/serializers/account/template.py:16 #: authentication/serializers/connect_token_secret.py:49 msgid "Su from" msgstr "切换自" -#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: accounts/models/account.py:55 settings/serializers/auth/cas.py:20 #: settings/serializers/auth/feishu.py:20 terminal/models/applet/applet.py:29 msgid "Version" msgstr "版本" -#: accounts/models/account.py:55 accounts/serializers/account/account.py:200 -#: users/models/user.py:768 +#: accounts/models/account.py:57 accounts/serializers/account/account.py:202 +#: users/models/user.py:778 msgid "Source" msgstr "来源" -#: accounts/models/account.py:56 +#: accounts/models/account.py:58 msgid "Source ID" msgstr "来源 ID" -#: accounts/models/account.py:59 +#: accounts/models/account.py:61 #: accounts/serializers/automations/change_secret.py:113 #: accounts/serializers/automations/change_secret.py:133 -#: acls/models/base.py:102 acls/serializers/base.py:57 +#: acls/models/base.py:102 acls/serializers/base.py:77 #: assets/serializers/asset/common.py:125 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 #: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 @@ -228,35 +231,35 @@ msgstr "来源 ID" msgid "Account" msgstr "账号" -#: accounts/models/account.py:65 +#: accounts/models/account.py:67 msgid "Can view asset account secret" msgstr "可以查看资产账号密码" -#: accounts/models/account.py:66 +#: accounts/models/account.py:68 msgid "Can view asset history account" msgstr "可以查看资产历史账号" -#: accounts/models/account.py:67 +#: accounts/models/account.py:69 msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" -#: accounts/models/account.py:68 +#: accounts/models/account.py:70 msgid "Can verify account" msgstr "可以验证账号" -#: accounts/models/account.py:69 +#: accounts/models/account.py:71 msgid "Can push account" msgstr "可以推送账号" -#: accounts/models/account.py:110 +#: accounts/models/account.py:117 msgid "Account template" msgstr "账号模版" -#: accounts/models/account.py:115 +#: accounts/models/account.py:122 msgid "Can view asset account template secret" msgstr "可以查看资产账号模版密码" -#: accounts/models/account.py:116 +#: accounts/models/account.py:123 msgid "Can change asset account template secret" msgstr "可以更改资产账号模版密码" @@ -275,7 +278,8 @@ msgstr "账号备份计划" #: accounts/models/automations/backup_account.py:83 #: assets/models/automations/base.py:115 audits/models.py:55 #: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:192 -#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:109 +#: ops/templates/ops/celery_task_log.html:75 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:137 #: terminal/models/session/session.py:45 #: tickets/models/ticket/apply_application.py:30 #: tickets/models/ticket/apply_asset.py:19 @@ -308,7 +312,7 @@ msgstr "原因" #: accounts/models/automations/backup_account.py:99 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:134 -#: ops/serializers/job.py:72 terminal/serializers/session.py:45 +#: ops/serializers/job.py:56 terminal/serializers/session.py:45 msgid "Is success" msgstr "是否成功" @@ -353,7 +357,7 @@ msgid "Can add push account execution" msgstr "创建推送账号执行" #: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 -#: accounts/serializers/account/account.py:394 +#: accounts/serializers/account/account.py:412 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 #: authentication/serializers/connect_token_secret.py:41 @@ -398,13 +402,14 @@ msgstr "开始日期" #: accounts/models/automations/change_secret.py:91 #: assets/models/automations/base.py:116 ops/models/base.py:56 #: ops/models/celery.py:64 ops/models/job.py:193 -#: terminal/models/applet/host.py:110 +#: terminal/models/applet/host.py:138 msgid "Date finished" msgstr "结束日期" #: accounts/models/automations/change_secret.py:93 -#: accounts/serializers/account/account.py:234 assets/const/automation.py:8 -#: common/const/choices.py:20 +#: accounts/serializers/account/account.py:236 assets/const/automation.py:8 +#: authentication/views/base.py:29 authentication/views/base.py:30 +#: authentication/views/base.py:31 common/const/choices.py:20 msgid "Error" msgstr "错误" @@ -422,13 +427,13 @@ msgstr "最后登录日期" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 -#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: acls/serializers/base.py:19 acls/serializers/base.py:50 #: assets/models/_user.py:23 audits/models.py:157 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:112 -#: users/models/user.py:715 users/templates/users/_msg_user_created.html:12 +#: users/models/user.py:725 users/templates/users/_msg_user_created.html:12 #: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" msgstr "用户名" @@ -455,8 +460,8 @@ msgid "Triggers" msgstr "触发方式" #: accounts/models/automations/push_account.py:16 acls/models/base.py:81 -#: acls/serializers/base.py:81 acls/serializers/login_acl.py:26 -#: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82 +#: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 +#: audits/models.py:65 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:108 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" @@ -471,14 +476,14 @@ msgid "Verify asset account" msgstr "账号验证" #: accounts/models/base.py:33 acls/models/base.py:75 -#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: acls/models/command_acl.py:21 acls/serializers/base.py:35 #: applications/models.py:9 assets/models/_user.py:22 #: assets/models/asset/common.py:90 assets/models/asset/common.py:123 #: assets/models/cmd_filter.py:21 assets/models/domain.py:18 #: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:13 assets/models/platform.py:89 +#: assets/models/platform.py:13 assets/models/platform.py:81 #: assets/serializers/asset/common.py:145 assets/serializers/platform.py:92 -#: assets/serializers/platform.py:193 +#: assets/serializers/platform.py:194 #: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:92 ops/models/playbook.py:23 ops/serializers/job.py:20 @@ -488,7 +493,7 @@ msgstr "账号验证" #: terminal/models/component/endpoint.py:90 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 #: terminal/models/component/terminal.py:84 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:717 +#: users/models/group.py:13 users/models/user.py:727 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名称" @@ -543,28 +548,28 @@ msgstr "" "{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" "密密码" -#: accounts/serializers/account/account.py:28 +#: accounts/serializers/account/account.py:29 msgid "Push now" msgstr "立即推送" -#: accounts/serializers/account/account.py:35 +#: accounts/serializers/account/account.py:36 msgid "Exist policy" msgstr "账号存在策略" -#: accounts/serializers/account/account.py:179 applications/models.py:11 -#: assets/models/label.py:21 assets/models/platform.py:90 +#: accounts/serializers/account/account.py:181 applications/models.py:11 +#: assets/models/label.py:21 assets/models/platform.py:82 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:110 assets/serializers/platform.py:194 +#: assets/serializers/platform.py:110 assets/serializers/platform.py:195 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "类别" -#: accounts/serializers/account/account.py:180 +#: accounts/serializers/account/account.py:182 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 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:91 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:83 #: assets/serializers/asset/common.py:122 assets/serializers/platform.py:94 #: assets/serializers/platform.py:109 audits/serializers.py:48 #: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:103 @@ -579,27 +584,27 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: accounts/serializers/account/account.py:195 +#: accounts/serializers/account/account.py:197 msgid "Asset not found" msgstr "资产不存在" -#: accounts/serializers/account/account.py:201 +#: accounts/serializers/account/account.py:203 #: accounts/serializers/account/base.py:64 msgid "Has secret" msgstr "已托管密码" -#: accounts/serializers/account/account.py:233 ops/models/celery.py:60 +#: accounts/serializers/account/account.py:235 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:235 +#: accounts/serializers/account/account.py:237 msgid "Changed" msgstr "已修改" -#: accounts/serializers/account/account.py:241 +#: accounts/serializers/account/account.py:246 #: accounts/serializers/automations/base.py:22 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 ops/models/base.py:17 @@ -608,29 +613,29 @@ msgstr "已修改" msgid "Assets" msgstr "资产" -#: accounts/serializers/account/account.py:293 +#: accounts/serializers/account/account.py:298 msgid "Account already exists" msgstr "账号已存在" -#: accounts/serializers/account/account.py:330 +#: accounts/serializers/account/account.py:348 #, python-format msgid "Asset does not support this secret type: %s" msgstr "资产不支持账号类型: %s" -#: accounts/serializers/account/account.py:361 +#: accounts/serializers/account/account.py:379 msgid "Account has exist" msgstr "账号已存在" -#: accounts/serializers/account/account.py:395 +#: accounts/serializers/account/account.py:413 #: authentication/serializers/connect_token_secret.py:146 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:33 msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:402 acls/models/base.py:98 -#: acls/models/login_acl.py:13 acls/serializers/base.py:55 -#: acls/serializers/login_acl.py:22 assets/models/cmd_filter.py:24 +#: accounts/serializers/account/account.py:420 acls/models/base.py:98 +#: acls/models/login_acl.py:13 acls/serializers/base.py:75 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24 #: assets/models/label.py:16 audits/models.py:44 audits/models.py:63 #: audits/models.py:141 authentication/models/connection_token.py:30 #: authentication/models/sso_token.py:16 @@ -641,12 +646,12 @@ msgstr "ID" #: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 #: terminal/notifications.py:96 terminal/notifications.py:144 #: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 +#: users/const.py:14 users/models/user.py:921 users/models/user.py:952 #: users/serializers/group.py:18 msgid "User" msgstr "用户" -#: accounts/serializers/account/account.py:403 +#: accounts/serializers/account/account.py:421 #: authentication/templates/authentication/_access_key_modal.html:33 #: terminal/notifications.py:98 terminal/notifications.py:146 msgid "Date" @@ -678,10 +683,14 @@ msgid "Key password" msgstr "密钥密码" #: accounts/serializers/account/base.py:80 -#: assets/serializers/asset/common.py:305 +#: assets/serializers/asset/common.py:306 msgid "Spec info" msgstr "特殊信息" +#: accounts/serializers/account/base.py:81 +msgid "Tip: If no username is required for authentication, fill in `null`" +msgstr "提示: 如果认证时不需要用户名,则填写为 null" + #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:129 assets/models/automations/base.py:18 #: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 @@ -718,8 +727,8 @@ msgstr "自动化任务执行历史" #: accounts/serializers/automations/change_secret.py:155 audits/const.py:52 #: audits/models.py:54 audits/signal_handlers/activity_log.py:33 -#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39 -#: terminal/const.py:59 terminal/models/session/sharing.py:107 +#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:40 +#: terminal/const.py:60 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" @@ -791,14 +800,14 @@ msgstr "优先级" msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" -#: acls/models/base.py:82 acls/serializers/base.py:75 -#: acls/serializers/login_acl.py:24 assets/models/cmd_filter.py:86 +#: acls/models/base.py:82 acls/serializers/base.py:95 +#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:80 msgid "Reviewers" msgstr "审批人" #: acls/models/base.py:83 authentication/models/access_key.py:17 -#: authentication/models/connection_token.py:49 +#: authentication/models/connection_token.py:50 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 @@ -806,7 +815,7 @@ msgid "Active" msgstr "激活中" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:71 terminal/const.py:67 +#: ops/serializers/job.py:55 terminal/const.py:68 #: terminal/models/session/session.py:43 terminal/serializers/command.py:18 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -848,7 +857,7 @@ msgstr "命令过滤" msgid "Command confirm" msgstr "命令复核" -#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:30 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:28 msgid "Rule" msgstr "规则" @@ -868,11 +877,11 @@ msgstr "登录资产访问控制" msgid "Login asset confirm" msgstr "登录资产复核" -#: acls/serializers/base.py:10 acls/serializers/login_acl.py:17 +#: acls/serializers/base.py:11 acls/serializers/login_acl.py:16 msgid "With * indicating a match all. " msgstr "* 表示匹配所有. " -#: acls/serializers/base.py:25 +#: acls/serializers/base.py:26 msgid "" "With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name " @@ -881,35 +890,35 @@ msgstr "" "* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:" "db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:19 +#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19 msgid "IP/Host" msgstr "IP/主机" -#: acls/serializers/base.py:60 +#: acls/serializers/base.py:80 msgid "User (username)" msgstr "用户(用户名)" -#: acls/serializers/base.py:64 +#: acls/serializers/base.py:84 msgid "Asset (name)" msgstr "资产(名称)" -#: acls/serializers/base.py:68 +#: acls/serializers/base.py:88 msgid "Asset (address)" msgstr "资产(地址)" -#: acls/serializers/base.py:72 +#: acls/serializers/base.py:92 msgid "Account (username)" msgstr "账号(用户名)" -#: acls/serializers/base.py:78 acls/serializers/login_acl.py:28 +#: acls/serializers/base.py:98 acls/serializers/login_acl.py:26 msgid "Reviewers amount" msgstr "审批人数量" -#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 +#: acls/serializers/base.py:126 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/base.py:115 +#: acls/serializers/base.py:132 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" @@ -1002,7 +1011,7 @@ msgid "Unable to connect to port {port} on {address}" msgstr "无法连接到 {port} 上的端口 {address}" #: assets/automations/ping_gateway/manager.py:58 -#: authentication/middleware.py:87 xpack/plugins/cloud/providers/fc.py:48 +#: authentication/middleware.py:92 xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "认证失败" @@ -1095,7 +1104,7 @@ msgstr "防火墙" msgid "Other" msgstr "其它" -#: assets/const/types.py:218 +#: assets/const/types.py:223 msgid "All types" msgstr "所有类型" @@ -1133,11 +1142,11 @@ msgstr "SSH公钥" #: assets/models/cmd_filter.py:88 assets/models/group.py:23 #: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:111 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:184 -#: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24 +#: terminal/models/applet/applet.py:37 terminal/models/applet/applet.py:218 +#: terminal/models/applet/host.py:139 terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:47 tickets/models/comment.py:32 -#: tickets/models/ticket/general.py:297 users/models/user.py:756 +#: tickets/models/ticket/general.py:297 users/models/user.py:766 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:111 msgid "Comment" msgstr "备注" @@ -1145,18 +1154,18 @@ msgstr "备注" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 #: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:191 -#: users/models/user.py:943 +#: users/models/user.py:953 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:36 users/models/user.py:777 +#: common/db/models.py:36 users/models/user.py:787 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:21 -#: common/db/models.py:33 users/models/user.py:763 +#: common/db/models.py:33 users/models/user.py:773 #: users/serializers/group.py:31 msgid "Created by" msgstr "创建者" @@ -1246,7 +1255,7 @@ msgstr "端口" msgid "Address" msgstr "地址" -#: assets/models/asset/common.py:125 assets/models/platform.py:120 +#: assets/models/asset/common.py:125 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:196 @@ -1255,7 +1264,7 @@ msgstr "系统平台" #: assets/models/asset/common.py:127 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 -#: perms/serializers/user_permission.py:28 +#: perms/serializers/user_permission.py:29 msgid "Domain" msgstr "网域" @@ -1263,7 +1272,7 @@ msgstr "网域" msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:132 assets/serializers/asset/common.py:306 +#: assets/models/asset/common.py:132 assets/serializers/asset/common.py:307 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "收集资产硬件信息" @@ -1330,7 +1339,7 @@ msgid "Submit selector" msgstr "确认按钮选择器" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:304 rbac/tree.py:35 +#: assets/serializers/asset/common.py:305 rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" @@ -1348,7 +1357,7 @@ msgstr "资产自动化任务" #: assets/models/automations/base.py:113 audits/models.py:177 #: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:184 -#: terminal/models/applet/applet.py:183 terminal/models/applet/host.py:108 +#: terminal/models/applet/applet.py:217 terminal/models/applet/host.py:136 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1375,7 +1384,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:723 +#: users/models/user.py:733 msgid "User group" msgstr "用户组" @@ -1425,7 +1434,7 @@ msgstr "默认" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:928 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:938 msgid "System" msgstr "系统" @@ -1441,7 +1450,8 @@ msgstr "值" #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: assets/serializers/platform.py:93 #: authentication/serializers/connect_token_secret.py:113 -#: common/serializers/common.py:85 settings/serializers/sms.py:7 +#: common/serializers/common.py:85 perms/serializers/user_permission.py:28 +#: settings/serializers/sms.py:7 msgid "Label" msgstr "标签" @@ -1491,102 +1501,102 @@ msgstr "开放的" msgid "Setting" msgstr "设置" -#: assets/models/platform.py:39 audits/const.py:47 settings/models.py:37 +#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37 #: terminal/serializers/applet_host.py:29 msgid "Enabled" msgstr "启用" -#: assets/models/platform.py:40 +#: assets/models/platform.py:32 msgid "Ansible config" msgstr "Ansible 配置" -#: assets/models/platform.py:42 assets/serializers/platform.py:63 +#: assets/models/platform.py:34 assets/serializers/platform.py:63 msgid "Ping enabled" msgstr "启用资产探活" -#: assets/models/platform.py:43 assets/serializers/platform.py:64 +#: assets/models/platform.py:35 assets/serializers/platform.py:64 msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:44 +#: assets/models/platform.py:36 msgid "Ping params" msgstr "资产探活参数" -#: assets/models/platform.py:46 assets/models/platform.py:70 +#: assets/models/platform.py:38 assets/models/platform.py:62 #: assets/serializers/platform.py:65 msgid "Gather facts enabled" msgstr "启用收集资产信息" -#: assets/models/platform.py:48 assets/models/platform.py:72 +#: assets/models/platform.py:40 assets/models/platform.py:64 #: assets/serializers/platform.py:66 msgid "Gather facts method" msgstr "收集信息方式" -#: assets/models/platform.py:50 assets/models/platform.py:74 +#: assets/models/platform.py:42 assets/models/platform.py:66 msgid "Gather facts params" msgstr "收集信息参数" -#: assets/models/platform.py:52 assets/serializers/platform.py:69 +#: assets/models/platform.py:44 assets/serializers/platform.py:69 msgid "Change secret enabled" msgstr "启用改密" -#: assets/models/platform.py:54 assets/serializers/platform.py:70 +#: assets/models/platform.py:46 assets/serializers/platform.py:70 msgid "Change secret method" msgstr "改密方式" -#: assets/models/platform.py:56 +#: assets/models/platform.py:48 msgid "Change secret params" msgstr "改密参数" -#: assets/models/platform.py:58 assets/serializers/platform.py:71 +#: assets/models/platform.py:50 assets/serializers/platform.py:71 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:60 assets/serializers/platform.py:72 +#: assets/models/platform.py:52 assets/serializers/platform.py:72 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:62 +#: assets/models/platform.py:54 msgid "Push account params" msgstr "账号推送参数" -#: assets/models/platform.py:64 assets/serializers/platform.py:67 +#: assets/models/platform.py:56 assets/serializers/platform.py:67 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:66 assets/serializers/platform.py:68 +#: assets/models/platform.py:58 assets/serializers/platform.py:68 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:68 +#: assets/models/platform.py:60 msgid "Verify account params" msgstr "账号验证参数" -#: assets/models/platform.py:92 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:84 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:93 +#: assets/models/platform.py:85 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:97 assets/serializers/platform.py:108 +#: assets/models/platform.py:89 assets/serializers/platform.py:108 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:99 assets/serializers/platform.py:135 +#: assets/models/platform.py:91 assets/serializers/platform.py:136 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:101 assets/serializers/platform.py:134 +#: assets/models/platform.py:93 assets/serializers/platform.py:135 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:102 assets/serializers/platform.py:114 +#: assets/models/platform.py:94 assets/serializers/platform.py:114 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:103 assets/serializers/platform.py:117 +#: assets/models/platform.py:95 assets/serializers/platform.py:117 msgid "Custom fields" msgstr "自定义属性" @@ -1614,23 +1624,29 @@ msgid "Node path" msgstr "节点路径" #: assets/serializers/asset/common.py:144 -#: assets/serializers/asset/common.py:307 +#: assets/serializers/asset/common.py:308 msgid "Auto info" msgstr "自动化信息" -#: assets/serializers/asset/common.py:226 +#: assets/serializers/asset/common.py:227 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:262 +#: assets/serializers/asset/common.py:263 msgid "port out of range (1-65535)" msgstr "端口超出范围 (1-65535)" -#: assets/serializers/asset/common.py:269 +#: assets/serializers/asset/common.py:270 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" -#: assets/serializers/asset/database.py:25 common/serializers/fields.py:103 +#: assets/serializers/asset/database.py:13 +#, fuzzy +#| msgid "Default storage" +msgid "Default database" +msgstr "默认存储" + +#: assets/serializers/asset/database.py:28 common/serializers/fields.py:103 #: tickets/serializers/ticket/common.py:58 #: xpack/plugins/cloud/serializers/account_attrs.py:56 #: xpack/plugins/cloud/serializers/account_attrs.py:79 @@ -1748,15 +1764,15 @@ msgstr "" msgid "Automation" msgstr "自动化" -#: assets/serializers/platform.py:136 +#: assets/serializers/platform.py:137 msgid "Default Domain" msgstr "默认网域" -#: assets/serializers/platform.py:145 +#: assets/serializers/platform.py:146 msgid "type is required" msgstr "类型 该字段是必填项。" -#: assets/serializers/platform.py:182 +#: assets/serializers/platform.py:183 msgid "Protocols is required" msgstr "协议是必填的" @@ -1910,7 +1926,7 @@ msgstr "会话日志" msgid "Login log" msgstr "登录日志" -#: audits/const.py:42 terminal/models/applet/host.py:112 +#: audits/const.py:42 terminal/models/applet/host.py:140 #: terminal/models/component/task.py:24 msgid "Task" msgstr "任务" @@ -2006,7 +2022,7 @@ msgstr "用户代理" #: audits/models.py:169 audits/serializers.py:47 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:740 +#: users/forms/profile.py:65 users/models/user.py:750 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" @@ -2060,22 +2076,24 @@ msgid "Auth Token" msgstr "认证令牌" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:74 authentication/views/wecom.py:177 +#: authentication/views/login.py:74 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:778 +#: users/models/user.py:680 users/models/user.py:788 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:144 +#: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:123 #: authentication/views/login.py:86 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 -#: settings/serializers/auth/feishu.py:13 users/models/user.py:780 +#: settings/serializers/auth/feishu.py:13 users/models/user.py:682 +#: users/models/user.py:790 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:179 +#: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:160 #: authentication/views/login.py:80 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:779 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:681 +#: users/models/user.py:789 msgid "DingTalk" msgstr "钉钉" @@ -2092,19 +2110,19 @@ msgstr "清理审计会话任务日志" msgid "This action require verify your MFA" msgstr "该操作需要验证您的 MFA, 请先开启并配置" -#: authentication/api/connection_token.py:296 +#: authentication/api/connection_token.py:303 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:299 +#: authentication/api/connection_token.py:306 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:311 +#: authentication/api/connection_token.py:318 msgid "ACL action is reject" msgstr "ACL 动作是拒绝" -#: authentication/api/connection_token.py:315 +#: authentication/api/connection_token.py:322 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2112,7 +2130,7 @@ msgstr "ACL 动作是复核" msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" -#: authentication/api/password.py:31 terminal/api/session/session.py:247 +#: authentication/api/password.py:31 terminal/api/session/session.py:249 #: users/views/profile/reset.py:44 msgid "User does not exist: {}" msgstr "用户不存在: {}" @@ -2326,21 +2344,21 @@ msgstr "手机号没有设置" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:61 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 -#: authentication/views/wecom.py:290 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:202 +#: authentication/views/wecom.py:244 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 -#: authentication/views/dingtalk.py:296 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:210 +#: authentication/views/dingtalk.py:252 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:167 msgid "FeiShu is not bound" msgstr "没有绑定飞书" @@ -2348,15 +2366,15 @@ msgstr "没有绑定飞书" msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/errors/redirect.py:85 authentication/mixins.py:307 +#: authentication/errors/redirect.py:85 authentication/mixins.py:316 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors/redirect.py:93 authentication/mixins.py:314 +#: authentication/errors/redirect.py:93 authentication/mixins.py:323 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors/redirect.py:101 authentication/mixins.py:321 +#: authentication/errors/redirect.py:101 authentication/mixins.py:330 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -2453,15 +2471,21 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:88 settings/utils/ldap.py:652 +#: authentication/middleware.py:93 settings/utils/ldap.py:652 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败(登录前检查失败): {}" -#: authentication/mixins.py:257 +#: authentication/mixins.py:91 +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:266 msgid "The MFA type ({}) is not enabled" msgstr "该 MFA ({}) 方式没有启用" -#: authentication/mixins.py:297 +#: authentication/mixins.py:306 msgid "Please change your password" msgstr "请修改密码" @@ -2475,7 +2499,7 @@ msgid "Input username" msgstr "自定义用户名" #: authentication/models/connection_token.py:38 -#: authentication/serializers/connection_token.py:17 +#: authentication/serializers/connection_token.py:20 msgid "Input secret" msgstr "自定义密码" @@ -2493,34 +2517,40 @@ msgid "Asset display" msgstr "资产名称" #: authentication/models/connection_token.py:43 +#, fuzzy +#| msgid "Disable" +msgid "Reusable" +msgstr "禁用" + +#: authentication/models/connection_token.py:44 #: 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:761 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:771 msgid "Date expired" msgstr "失效日期" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:48 #: perms/models/asset_permission.py:77 msgid "From ticket" msgstr "来自工单" -#: authentication/models/connection_token.py:53 +#: authentication/models/connection_token.py:54 msgid "Connection token" msgstr "连接令牌" -#: authentication/models/connection_token.py:55 +#: authentication/models/connection_token.py:56 msgid "Can view connection token secret" msgstr "可以查看连接令牌密文" -#: authentication/models/connection_token.py:102 +#: authentication/models/connection_token.py:103 msgid "Connection token inactive" msgstr "连接令牌未激活" -#: authentication/models/connection_token.py:105 +#: authentication/models/connection_token.py:106 msgid "Connection token expired at: {}" msgstr "连接令牌过期: {}" -#: authentication/models/connection_token.py:108 +#: authentication/models/connection_token.py:109 msgid "No user or invalid user" msgstr "没有用户或用户失效" @@ -2572,15 +2602,15 @@ msgstr "组件" msgid "Expired now" msgstr "立刻过期" -#: authentication/serializers/connection_token.py:15 +#: authentication/serializers/connection_token.py:18 msgid "Expired time" msgstr "过期时间" -#: authentication/serializers/connection_token.py:19 +#: authentication/serializers/connection_token.py:22 msgid "Ticket info" msgstr "工单信息" -#: authentication/serializers/connection_token.py:20 +#: authentication/serializers/connection_token.py:23 #: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 #: perms/serializers/permission.py:69 #: tickets/models/ticket/apply_application.py:28 @@ -2588,17 +2618,21 @@ msgstr "工单信息" msgid "Actions" msgstr "动作" -#: authentication/serializers/connection_token.py:41 +#: authentication/serializers/connection_token.py:44 #: perms/serializers/permission.py:38 perms/serializers/permission.py:70 #: users/serializers/user.py:97 users/serializers/user.py:172 msgid "Is expired" msgstr "已过期" +#: authentication/serializers/connection_token.py:79 +msgid "Reusable connection token is not allowed, global setting not enabled" +msgstr "" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 #: settings/serializers/email.py:50 users/forms/profile.py:102 -#: users/forms/profile.py:106 users/models/user.py:719 +#: users/forms/profile.py:106 users/models/user.py:729 #: users/templates/users/forgot_password.html:116 #: users/views/profile/reset.py:73 msgid "Email" @@ -2688,7 +2722,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:417 +#: jumpserver/conf.py:419 #: 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 @@ -2832,76 +2866,77 @@ msgstr "复制成功" msgid "LAN" msgstr "局域网" -#: authentication/views/dingtalk.py:41 +#: authentication/views/base.py:64 +#: perms/templates/perms/_msg_permed_items_expire.html:21 +msgid "If you have any question, please contact the administrator" +msgstr "如果有疑问或需求,请联系系统管理员" + +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:44 +#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:209 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 -#: authentication/views/wecom.py:55 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:57 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:80 +#: authentication/views/dingtalk.py:61 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 +#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:129 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:164 +#: authentication/views/dingtalk.py:145 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:173 +#: authentication/views/dingtalk.py:154 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:180 +#: authentication/views/dingtalk.py:161 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 +#: authentication/views/dingtalk.py:211 authentication/views/dingtalk.py:246 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 +#: authentication/views/dingtalk.py:253 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:39 +#: authentication/views/feishu.py:39 authentication/views/feishu.py:166 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:87 +#: authentication/views/feishu.py:67 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:129 +#: authentication/views/feishu.py:108 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:138 +#: authentication/views/feishu.py:117 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:145 +#: authentication/views/feishu.py:124 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:197 +#: authentication/views/feishu.py:168 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/feishu.py:204 -msgid "Please login with a password and then bind the FeiShu" -msgstr "请使用密码登录,然后绑定飞书" - #: authentication/views/login.py:182 msgid "Redirecting" msgstr "跳转中" @@ -2938,31 +2973,31 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:40 +#: authentication/views/wecom.py:42 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:43 +#: authentication/views/wecom.py:45 authentication/views/wecom.py:201 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:162 +#: authentication/views/wecom.py:144 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:171 +#: authentication/views/wecom.py:153 msgid "The WeCom is already bound to another user" msgstr "该企业微信已经绑定其他用户" -#: authentication/views/wecom.py:178 +#: authentication/views/wecom.py:160 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 +#: authentication/views/wecom.py:203 authentication/views/wecom.py:238 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 +#: authentication/views/wecom.py:245 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" @@ -2982,7 +3017,7 @@ msgstr "定时触发" msgid "Ready" msgstr "准备" -#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: common/const/choices.py:16 terminal/const.py:59 tickets/const.py:29 #: tickets/const.py:39 msgid "Pending" msgstr "待定的" @@ -3045,7 +3080,7 @@ msgstr "忽略的" msgid "discard time" msgstr "忽略时间" -#: common/db/models.py:34 users/models/user.py:764 +#: common/db/models.py:34 users/models/user.py:774 msgid "Updated by" msgstr "最后更新者" @@ -3073,6 +3108,12 @@ msgstr "解析文件错误: {}" msgid "Invalid excel file" msgstr "无效的 excel 文件" +#: common/drf/renders/base.py:209 +msgid "" +"{} - The encryption password has not been set - please go to personal " +"information -> file encryption password to set the encryption password" +msgstr "{} - 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加密密码" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -3114,7 +3155,7 @@ msgstr "不支持 Elasticsearch8" msgid "Network error, please contact system administrator" msgstr "网络错误,请联系系统管理员" -#: common/sdk/im/wecom/__init__.py:15 +#: common/sdk/im/wecom/__init__.py:16 msgid "WeCom error, please contact system administrator" msgstr "企业微信错误,请联系系统管理员" @@ -3243,11 +3284,11 @@ msgstr "导出搜素: %s" msgid "User %s view/export secret" msgstr "用户 %s 查看/导出 了密码" -#: jumpserver/conf.py:416 +#: jumpserver/conf.py:418 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:418 +#: jumpserver/conf.py:420 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -3310,15 +3351,15 @@ msgstr "系统信息" msgid "Publish the station message" msgstr "发布站内消息" -#: ops/ansible/inventory.py:77 +#: ops/ansible/inventory.py:82 msgid "No account available" msgstr "无可用账号" -#: ops/ansible/inventory.py:236 +#: ops/ansible/inventory.py:247 msgid "Ansible disabled" msgstr "Ansible 已禁用" -#: ops/ansible/inventory.py:252 +#: ops/ansible/inventory.py:263 msgid "Skip hosts below:" msgstr "跳过以下主机: " @@ -3584,15 +3625,15 @@ msgstr "CPU 使用率超过 {max_threshold}: => {value}" msgid "Run after save" msgstr "保存后执行" -#: ops/serializers/job.py:70 +#: ops/serializers/job.py:54 msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:73 terminal/serializers/session.py:49 +#: ops/serializers/job.py:57 terminal/serializers/session.py:49 msgid "Is finished" msgstr "是否完成" -#: ops/serializers/job.py:74 +#: ops/serializers/job.py:58 msgid "Time cost" msgstr "花费时间" @@ -3624,6 +3665,10 @@ msgstr "清理异常作业" msgid "Task log" msgstr "任务列表" +#: ops/templates/ops/celery_task_log.html:71 +msgid "Task name" +msgstr "任务名称" + #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "JumpServer 当前用户的用户名" @@ -3809,10 +3854,6 @@ msgstr "" " 以下 %(item_type)s 即将在 %(count)s 天后过期\n" " " -#: perms/templates/perms/_msg_permed_items_expire.html:21 -msgid "If you have any question, please contact the administrator" -msgstr "如果有疑问或需求,请联系系统管理员" - #: rbac/api/role.py:35 msgid "Internal role, can't be destroy" msgstr "内部角色,不能删除" @@ -3891,7 +3932,7 @@ msgid "Scope" msgstr "范围" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:727 +#: users/models/user.py:737 msgid "Role" msgstr "角色" @@ -3907,21 +3948,21 @@ msgstr "组织角色" msgid "Role binding" msgstr "角色绑定" -#: rbac/models/rolebinding.py:145 +#: rbac/models/rolebinding.py:153 msgid "All organizations" msgstr "所有组织" -#: rbac/models/rolebinding.py:174 +#: rbac/models/rolebinding.py:182 msgid "" "User last role in org, can not be delete, you can remove user from org " "instead" msgstr "用户最后一个角色,不能删除,你可以将用户从组织移除" -#: rbac/models/rolebinding.py:181 +#: rbac/models/rolebinding.py:189 msgid "Organization role binding" msgstr "组织角色绑定" -#: rbac/models/rolebinding.py:196 +#: rbac/models/rolebinding.py:204 msgid "System role binding" msgstr "系统角色绑定" @@ -3997,8 +4038,8 @@ msgstr "任务中心" msgid "My assets" msgstr "我的资产" -#: rbac/tree.py:56 terminal/models/applet/applet.py:43 -#: terminal/models/applet/applet.py:180 terminal/models/applet/host.py:28 +#: rbac/tree.py:56 terminal/models/applet/applet.py:44 +#: terminal/models/applet/applet.py:214 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "远程应用" @@ -4854,24 +4895,38 @@ msgid "Only single device login" msgstr "仅一台设备登录" #: settings/serializers/security.py:97 -msgid "Next device login, pre login will be logout" -msgstr "下个设备登录,上次登录会被顶掉" +msgid "" +"After the user logs in on the new device, other logged-in devices will " +"automatically log out" +msgstr "用户在新设备登录后,其他已登录的设备会自动退出" #: settings/serializers/security.py:100 msgid "Only exist user login" msgstr "仅已存在用户登录" #: settings/serializers/security.py:101 -msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet" -msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败" +msgid "" +"If enabled, non-existent users will not be allowed to log in; if disabled, " +"users of other authentication methods except local authentication methods " +"are allowed to log in and automatically create users (if the user does not " +"exist)" +msgstr "" +"如果开启,不存在的用户将不被允许登录;如果关闭,除本地认证方式外,其他认证方" +"式的用户都允许登录并自动创建用户(如果用户不存在)" #: settings/serializers/security.py:104 msgid "Only from source login" msgstr "仅从用户来源登录" #: settings/serializers/security.py:105 -msgid "Only log in from the user source property" -msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败" +msgid "" +"If it is enabled, the user will only authenticate to the source when logging " +"in; if it is disabled, the user will authenticate all the enabled " +"authentication methods in a certain order when logging in, and as long as " +"one of the authentication methods is successful, they can log in directly" +msgstr "" +"如果开启,用户登录时仅会向来源端进行认证;如果关闭,用户登录时会按照一定的顺" +"序对所有已开启的认证方式进行顺序认证,只要有一个认证成功就可以直接登录" #: settings/serializers/security.py:109 msgid "MFA verify TTL" @@ -5403,15 +5458,15 @@ msgstr "测试失败: 账号无效" msgid "Have online sessions" msgstr "有在线会话" -#: terminal/api/session/session.py:239 +#: terminal/api/session/session.py:241 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session/session.py:242 +#: terminal/api/session/session.py:244 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session/session.py:255 +#: terminal/api/session/session.py:257 msgid "User does not have permission" msgstr "用户没有权限" @@ -5464,7 +5519,7 @@ msgstr "严重" msgid "High" msgstr "较高" -#: terminal/const.py:32 terminal/const.py:65 +#: terminal/const.py:32 terminal/const.py:66 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" @@ -5473,19 +5528,19 @@ msgstr "正常" msgid "Offline" msgstr "离线" -#: terminal/const.py:61 +#: terminal/const.py:62 msgid "Mismatch" msgstr "未匹配" -#: terminal/const.py:66 +#: terminal/const.py:67 msgid "Tunnel" msgstr "隧道" -#: terminal/const.py:71 +#: terminal/const.py:72 msgid "Read Only" msgstr "只读" -#: terminal/const.py:72 +#: terminal/const.py:73 msgid "Writable" msgstr "读写" @@ -5502,31 +5557,37 @@ msgid "Author" msgstr "作者" #: terminal/models/applet/applet.py:35 +#, fuzzy +#| msgid "Can push account" +msgid "Can concurrent" +msgstr "可以推送账号" + +#: terminal/models/applet/applet.py:36 msgid "Tags" msgstr "标签" -#: terminal/models/applet/applet.py:39 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:40 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" -#: terminal/models/applet/applet.py:84 +#: terminal/models/applet/applet.py:85 msgid "Applet pkg not valid, Missing file {}" msgstr "Applet pkg 无效,缺少文件 {}" -#: terminal/models/applet/applet.py:103 +#: terminal/models/applet/applet.py:104 msgid "Load platform.yml failed: {}" msgstr "" -#: terminal/models/applet/applet.py:106 +#: terminal/models/applet/applet.py:107 msgid "Only support custom platform" msgstr "" -#: terminal/models/applet/applet.py:111 +#: terminal/models/applet/applet.py:112 msgid "Missing type in platform.yml" msgstr "" -#: terminal/models/applet/applet.py:182 terminal/models/applet/host.py:34 -#: terminal/models/applet/host.py:106 +#: terminal/models/applet/applet.py:216 terminal/models/applet/host.py:34 +#: terminal/models/applet/host.py:134 msgid "Hosting" msgstr "宿主机" @@ -5546,7 +5607,7 @@ msgstr "初始化日期" msgid "Date synced" msgstr "同步日期" -#: terminal/models/applet/host.py:107 +#: terminal/models/applet/host.py:135 msgid "Initial" msgstr "初始化" @@ -5979,7 +6040,7 @@ msgstr "周期清理终端状态" #: terminal/tasks.py:37 msgid "Clean orphan session" -msgstr "清除孤儿会话" +msgstr "清除离线会话" #: terminal/tasks.py:56 msgid "Upload session replay to external storage" @@ -5993,6 +6054,12 @@ msgstr "运行应用机部署" msgid "Install applet" msgstr "安装应用" +#: terminal/tasks.py:104 +#, fuzzy +#| msgid "Gather assets accounts" +msgid "Generate applet host accounts" +msgstr "收集资产上的账号" + #: terminal/templates/terminal/_msg_command_alert.html:10 msgid "view" msgstr "查看" @@ -6349,7 +6416,7 @@ msgstr "无效的审批动作" msgid "This user is not authorized to approve this ticket" msgstr "此用户无权审批此工单" -#: users/api/user.py:182 +#: users/api/user.py:185 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置 MFA 多因子认证, 请去个人信息页面重置" @@ -6460,7 +6527,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:170 users/models/user.py:750 +#: users/forms/profile.py:170 users/models/user.py:760 msgid "Public key" msgstr "SSH公钥" @@ -6468,68 +6535,68 @@ msgstr "SSH公钥" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:729 users/serializers/user.py:171 +#: users/models/user.py:739 users/serializers/user.py:171 msgid "Is service account" msgstr "服务账号" -#: users/models/user.py:731 +#: users/models/user.py:741 msgid "Avatar" msgstr "头像" -#: users/models/user.py:734 +#: users/models/user.py:744 msgid "Wechat" msgstr "微信" -#: users/models/user.py:737 users/serializers/user.py:109 +#: users/models/user.py:747 users/serializers/user.py:109 msgid "Phone" msgstr "手机" -#: users/models/user.py:743 +#: users/models/user.py:753 msgid "OTP secret key" msgstr "OTP 密钥" -#: users/models/user.py:747 +#: users/models/user.py:757 msgid "Private key" msgstr "ssh私钥" -#: users/models/user.py:753 +#: users/models/user.py:763 msgid "Secret key" msgstr "Secret key" -#: users/models/user.py:758 users/serializers/profile.py:149 +#: users/models/user.py:768 users/serializers/profile.py:149 #: users/serializers/user.py:168 msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:772 +#: users/models/user.py:782 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:775 +#: users/models/user.py:785 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:913 +#: users/models/user.py:923 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:914 +#: users/models/user.py:924 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:915 +#: users/models/user.py:925 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:924 +#: users/models/user.py:934 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:927 +#: users/models/user.py:937 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:952 +#: users/models/user.py:962 msgid "User password history" msgstr "用户密码历史" @@ -6632,6 +6699,14 @@ msgstr "为了安全,仅列出几个用户" msgid "name not unique" msgstr "名称重复" +#: users/signal_handlers.py:27 +msgid "" +"The administrator has enabled \"Only allow existing users to log in\", \n" +" and the current user is not in the user list. Please contact the " +"administrator." +msgstr "" +"管理员已开启'仅允许已存在用户登录',当前用户不在用户列表中,请联系管理员。" + #: users/tasks.py:21 msgid "Check password expired" msgstr "校验密码已过期" @@ -7459,3 +7534,11 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:86 msgid "Community edition" msgstr "社区版" + +#, fuzzy +#~| msgid "Trigger mode" +#~ msgid "Trigger type" +#~ msgstr "触发模式" + +#~ msgid "Please login with a password and then bind the FeiShu" +#~ msgstr "请使用密码登录,然后绑定飞书" diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 4bcb9be60..2045b6f29 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -1,4 +1,5 @@ from collections import defaultdict +from functools import reduce class DefaultCallback: @@ -18,6 +19,7 @@ class DefaultCallback: failures=defaultdict(dict), dark=defaultdict(dict), skipped=defaultdict(dict), + ignored=defaultdict(dict), ) self.summary = dict( ok=[], @@ -59,6 +61,14 @@ class DefaultCallback: } self.result['ok'][host][task] = detail + def runner_on_skipped(self, event_data, host=None, task=None, **kwargs): + detail = { + 'action': event_data.get('task_action', ''), + 'res': {}, + 'rc': 0, + } + self.result['skipped'][host][task] = detail + def runner_on_failed(self, event_data, host=None, task=None, res=None, **kwargs): detail = { 'action': event_data.get('task_action', ''), @@ -67,15 +77,9 @@ class DefaultCallback: 'stdout': res.get('stdout', ''), 'stderr': ';'.join([res.get('stderr', ''), res.get('msg', '')]).strip(';') } - self.result['failures'][host][task] = detail - - def runner_on_skipped(self, event_data, host=None, task=None, **kwargs): - detail = { - 'action': event_data.get('task_action', ''), - 'res': {}, - 'rc': 0, - } - self.result['skipped'][host][task] = detail + ignore_errors = event_data.get('ignore_errors', False) + error_key = 'ignored' if ignore_errors else 'failures' + self.result[error_key][host][task] = detail def runner_on_unreachable(self, event_data, host=None, task=None, res=None, **kwargs): detail = { @@ -106,13 +110,18 @@ class DefaultCallback: def playbook_on_stats(self, event_data, **kwargs): failed = [] - for i in ['dark', 'failures']: - for host, tasks in self.result[i].items(): + error_func = lambda err, task_detail: err + f"{task_detail[0]}: {task_detail[1]['stderr']};" + for tp in ['dark', 'failures']: + for host, tasks in self.result[tp].items(): failed.append(host) - error = '' - for task, detail in tasks.items(): - error += f'{task}: {detail["stderr"]};' - self.summary[i][host] = error.strip(';') + error = reduce(error_func, tasks.items(), '').strip(';') + self.summary[tp][host] = error + + for host, tasks in self.result.get('ignored', {}).items(): + ignore_errors = reduce(error_func, tasks.items(), '').strip(';') + if host in failed: + self.summary['failures'][host] += {ignore_errors} + self.summary['ok'] = list(set(self.result['ok'].keys()) - set(failed)) self.summary['skipped'] = list(set(self.result['skipped'].keys()) - set(failed)) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index fc124b210..80922859b 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -9,8 +9,11 @@ __all__ = ['JMSInventory'] class JMSInventory: - def __init__(self, assets, account_policy='privileged_first', - account_prefer='root,Administrator', host_callback=None, exclude_localhost=False): + def __init__( + self, assets, account_policy='privileged_first', + account_prefer='root,Administrator', host_callback=None, + exclude_localhost=False, task_type=None + ): """ :param assets: :param account_prefer: account username name if not set use account_policy @@ -22,6 +25,7 @@ class JMSInventory: self.host_callback = host_callback self.exclude_hosts = {} self.exclude_localhost = exclude_localhost + self.task_type = task_type @staticmethod def clean_assets(assets): @@ -73,6 +77,7 @@ class JMSInventory: return var def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway): + from accounts.const import AutomationTypes if not account: host['error'] = _("No account available") return host @@ -92,6 +97,12 @@ class JMSInventory: host['ansible_become_password'] = su_from.secret else: host['ansible_become_password'] = account.secret + 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)) + host['ansible_become'] = True + host['ansible_become_user'] = 'root' + host['ansible_become_password'] = account.secret else: host.update(self.make_account_ansible_vars(account)) diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 72d970dee..d4540269c 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -4,19 +4,19 @@ import os import re from celery.result import AsyncResult -from rest_framework import generics, viewsets, mixins, status from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from django_celery_beat.models import PeriodicTask +from rest_framework import generics, viewsets, mixins, status from rest_framework.response import Response +from common.api import LogTailApi, CommonApiMixin from common.exceptions import JMSException from common.permissions import IsValidUser -from common.api import LogTailApi, CommonApiMixin from ops.celery import app -from ..models import CeleryTaskExecution, CeleryTask -from ..celery.utils import get_celery_task_log_path from ..ansible.utils import get_ansible_task_log_path +from ..celery.utils import get_celery_task_log_path +from ..models import CeleryTaskExecution, CeleryTask from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..serializers.celery import CeleryTaskSerializer, CeleryTaskExecutionSerializer diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index 280ab5b98..6fac1bc95 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -1,23 +1,28 @@ from django.conf import settings from django.db.models import Count from django.db.transaction import atomic -from rest_framework.views import APIView from django.shortcuts import get_object_or_404 from rest_framework.response import Response +from rest_framework.views import APIView +from assets.models import Asset +from common.permissions import IsValidUser from ops.const import Types from ops.models import Job, JobExecution from ops.serializers.job import JobSerializer, JobExecutionSerializer -__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', - 'JobAssetDetail', 'JobExecutionTaskDetail', 'FrequentUsernames'] +__all__ = [ + 'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', + 'JobAssetDetail', 'JobExecutionTaskDetail', 'UsernameHintsAPI' +] from ops.tasks import run_ops_job_execution from ops.variables import JMS_JOB_VARIABLE_HELP from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_org, get_current_org from accounts.models import Account -from rbac.permissions import RBACPermission +from perms.models import PermNode +from perms.utils import UserPermAssetUtil def set_task_to_serializer_data(serializer, task): @@ -26,9 +31,22 @@ def set_task_to_serializer_data(serializer, task): setattr(serializer, "_data", data) +def merge_nodes_and_assets(nodes, assets, user): + if nodes: + perm_util = UserPermAssetUtil(user=user) + for node_id in nodes: + if node_id == PermNode.FAVORITE_NODE_KEY: + node_assets = perm_util.get_favorite_assets() + elif node_id == PermNode.UNGROUPED_NODE_KEY: + node_assets = perm_util.get_ungroup_assets() + else: + node, node_assets = perm_util.get_node_all_assets(node_id) + assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) + return assets + + class JobViewSet(OrgBulkModelViewSet): serializer_class = JobSerializer - permission_classes = (RBACPermission,) search_fields = ('name', 'comment') model = Job @@ -49,6 +67,10 @@ class JobViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): run_after_save = serializer.validated_data.pop('run_after_save', False) + node_ids = serializer.validated_data.pop('nodes', []) + assets = serializer.validated_data.__getitem__('assets') + assets = merge_nodes_and_assets(node_ids, assets, self.request.user) + serializer.validated_data.__setitem__('assets', assets) instance = serializer.save() if instance.instant or run_after_save: self.run_job(instance, serializer) @@ -70,9 +92,9 @@ class JobViewSet(OrgBulkModelViewSet): class JobExecutionViewSet(OrgBulkModelViewSet): serializer_class = JobExecutionSerializer http_method_names = ('get', 'post', 'head', 'options',) - permission_classes = (RBACPermission,) model = JobExecution search_fields = ('material',) + filterset_fields = ['status', 'job_id'] @atomic def perform_create(self, serializer): @@ -88,56 +110,66 @@ class JobExecutionViewSet(OrgBulkModelViewSet): def get_queryset(self): queryset = super().get_queryset() queryset = queryset.filter(creator=self.request.user) - job_id = self.request.query_params.get('job_id') - if job_id: - queryset = queryset.filter(job_id=job_id) return queryset +class JobAssetDetail(APIView): + rbac_perms = { + 'get': ['ops.view_jobexecution'], + } + + def get(self, request, **kwargs): + execution_id = request.query_params.get('execution_id', '') + execution = get_object_or_404(JobExecution, id=execution_id) + return Response(data=execution.assent_result_detail) + + +class JobExecutionTaskDetail(APIView): + rbac_perms = { + 'GET': ['ops.view_jobexecution'], + } + + def get(self, request, **kwargs): + org = get_current_org() + task_id = str(kwargs.get('task_id')) + + with tmp_to_org(org): + execution = get_object_or_404(JobExecution, task_id=task_id) + + return Response(data={ + 'status': execution.status, + 'is_finished': execution.is_finished, + 'is_success': execution.is_success, + 'time_cost': execution.time_cost, + 'job_id': execution.job.id, + }) + + class JobRunVariableHelpAPIView(APIView): - rbac_perms = () - permission_classes = () + permission_classes = [IsValidUser] def get(self, request, **kwargs): return Response(data=JMS_JOB_VARIABLE_HELP) -class JobAssetDetail(APIView): - rbac_perms = () - permission_classes = () +class UsernameHintsAPI(APIView): + permission_classes = [IsValidUser] - def get(self, request, **kwargs): - execution_id = request.query_params.get('execution_id') - if execution_id: - execution = get_object_or_404(JobExecution, id=execution_id) - return Response(data=execution.assent_result_detail) + def post(self, request, **kwargs): + node_ids = request.data.get('nodes', None) + asset_ids = request.data.get('assets', []) + query = request.data.get('query', None) + assets = list(Asset.objects.filter(id__in=asset_ids).all()) -class JobExecutionTaskDetail(APIView): - rbac_perms = () - permission_classes = () + assets = merge_nodes_and_assets(node_ids, assets, request.user) - def get(self, request, **kwargs): - org = get_current_org() - task_id = str(kwargs.get('task_id')) - if task_id: - with tmp_to_org(org): - execution = get_object_or_404(JobExecution, task_id=task_id) - return Response(data={ - 'status': execution.status, - 'is_finished': execution.is_finished, - 'is_success': execution.is_success, - 'time_cost': execution.time_cost, - 'job_id': execution.job.id, - }) - - -class FrequentUsernames(APIView): - rbac_perms = () - permission_classes = () - - def get(self, request, **kwargs): - top_accounts = Account.objects.exclude(username='root').exclude(username__startswith='jms_').values( - 'username').annotate( - total=Count('username')).order_by('total')[:5] + top_accounts = Account.objects \ + .exclude(username__startswith='jms_') \ + .exclude(username__startswith='js_') \ + .filter(username__icontains=query) \ + .filter(asset__in=assets) \ + .values('username') \ + .annotate(total=Count('username')) \ + .order_by('total', '-username')[:10] return Response(data=top_accounts) diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 657600e50..5166aa755 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -5,13 +5,14 @@ from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import PeriodicTask from rest_framework import serializers +from ops.celery import app +from ops.models import CeleryTask, CeleryTaskExecution + __all__ = [ 'CeleryResultSerializer', 'CeleryTaskExecutionSerializer', 'CeleryPeriodTaskSerializer', 'CeleryTaskSerializer' ] -from ops.models import CeleryTask, CeleryTaskExecution - class CeleryResultSerializer(serializers.Serializer): id = serializers.UUIDField() @@ -37,11 +38,24 @@ class CeleryTaskSerializer(serializers.ModelSerializer): class CeleryTaskExecutionSerializer(serializers.ModelSerializer): is_success = serializers.BooleanField(required=False, read_only=True, label=_('Success')) + task_name = serializers.SerializerMethodField() class Meta: model = CeleryTaskExecution fields = [ - "id", "name", "args", "kwargs", "time_cost", "timedelta", + "id", "name", "task_name", "args", "kwargs", "time_cost", "timedelta", "is_success", "is_finished", "date_published", "date_start", "date_finished" ] + + @staticmethod + def get_task_name(obj): + from assets.const import AutomationTypes as AssetTypes + from accounts.const import AutomationTypes as AccountTypes + tp_dict = dict(AssetTypes.choices) | dict(AccountTypes.choices) + tp = obj.kwargs.get('tp') + task = app.tasks.get(obj.name) + task_name = getattr(task, 'verbose_name', obj.name) + if tp: + task_name = f'{task_name}({tp_dict.get(tp, tp)})' + return task_name diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 8b157b66f..011bea4e2 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -33,22 +33,6 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): user = request.user if request else None return user - def create(self, validated_data): - assets = validated_data.__getitem__('assets') - node_ids = validated_data.pop('nodes', None) - if node_ids: - user = self.get_request_user() - perm_util = UserPermAssetUtil(user=user) - for node_id in node_ids: - if node_id == PermNode.FAVORITE_NODE_KEY: - node_assets = perm_util.get_favorite_assets() - elif node_id == PermNode.UNGROUPED_NODE_KEY: - node_assets = perm_util.get_ungroup_assets() - else: - node, node_assets = perm_util.get_node_all_assets(node_id) - assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) - return super().create(validated_data) - class Meta: model = Job read_only_fields = [ diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index 2305218a9..3dfc23886 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -7,9 +7,12 @@ + + +
    +
  • + ID: + +
  • +
  • + {% trans 'Task name' %}: + +
  • +
  • + {% trans 'Date start' %}: + +
  • +
@@ -52,6 +89,7 @@ var failOverWsURL = scheme + "://" + document.location.hostname + ':' + failOverPort + url; var term; var ws; + var extraQuery = Object.fromEntries(new URLSearchParams(window.location.search)); $(document).ready(function () { term = new Terminal({ @@ -85,7 +123,23 @@ term.write("Connect websocket server error") } } + getAutomationExecutionInfo(); }).on('resize', window, function () { window.fit.fit(term); }); + function getAutomationExecutionInfo() { + let url = "{% url 'api-ops:task-executions-detail' pk=task_id %}"; + + requestApi({ + url: url, + method: "GET", + flash_message: false, + success(data) { + const dateStart = new Date(data.date_start).toLocaleString(); + $('.task-id').html(data.id); + $('.task-type').html(data.task_name); + $('.date-start').html(dateStart); + } + }) + } diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 051ca7894..905f0ed0a 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -27,7 +27,7 @@ urlpatterns = [ path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), path('job-execution/task-detail//', api.JobExecutionTaskDetail.as_view(), name='task-detail'), - path('frequent-username/', api.FrequentUsernames.as_view(), name='frequent-usernames'), + path('username-hints/', api.UsernameHintsAPI.as_view(), name='username-hints'), path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), path('celery/task//task-execution//log/', api.CeleryTaskExecutionLogApi.as_view(), diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index 01ce911c4..998ee216c 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -59,8 +59,6 @@ def expire_user_orgs(*args): @receiver(post_save, sender=Organization) def on_org_create(sender, instance, created=False, **kwargs): - if not created: - return expire_user_orgs() @@ -80,7 +78,7 @@ def on_org_create_or_update(sender, instance, **kwargs): @receiver(pre_delete, sender=Organization) -def on_org_delete(sender, instance, **kwargs): +def delete_org_root_node_on_org_delete(sender, instance, **kwargs): expire_orgs_mapping_for_memory(instance.id) # 删除该组织下所有 节点 @@ -91,7 +89,7 @@ def on_org_delete(sender, instance, **kwargs): @receiver(post_delete, sender=Organization) -def on_org_delete(sender, instance, **kwargs): +def expire_user_orgs_on_org_delete(sender, instance, **kwargs): expire_user_orgs() diff --git a/apps/perms/api/user_permission/tree/node_with_asset.py b/apps/perms/api/user_permission/tree/node_with_asset.py index 021933d40..5469c6ed3 100644 --- a/apps/perms/api/user_permission/tree/node_with_asset.py +++ b/apps/perms/api/user_permission/tree/node_with_asset.py @@ -1,4 +1,6 @@ import abc +import re +from collections import defaultdict from urllib.parse import parse_qsl from django.conf import settings @@ -11,6 +13,7 @@ from rest_framework.response import Response from accounts.const import AliasAccount from assets.api import SerializeToTreeNodeMixin +from assets.const import AllTypes from assets.models import Asset from assets.utils import KubernetesTree from authentication.models import ConnectionToken @@ -26,7 +29,8 @@ from ..mixin import SelfOrPKUserMixin __all__ = [ 'UserGrantedK8sAsTreeApi', 'UserPermedNodesWithAssetsAsTreeApi', - 'UserPermedNodeChildrenWithAssetsAsTreeApi' + 'UserPermedNodeChildrenWithAssetsAsTreeApi', + 'UserPermedNodeChildrenWithAssetsAsCategoryTreeApi', ] @@ -137,6 +141,74 @@ class UserPermedNodeChildrenWithAssetsAsTreeApi(BaseUserNodeWithAssetAsTreeApi): return self.query_node_key or self.default_unfolded_node_key +class UserPermedNodeChildrenWithAssetsAsCategoryTreeApi( + SelfOrPKUserMixin, SerializeToTreeNodeMixin, ListAPIView +): + @property + def is_sync(self): + sync = self.request.query_params.get('sync', 0) + return int(sync) == 1 + + @property + def tp(self): + return self.request.query_params.get('type') + + def get_assets(self): + query_asset_util = UserPermAssetUtil(self.user) + node = PermNode.objects.filter( + granted_node_rels__user=self.user, parent_key='').first() + if node: + __, assets = query_asset_util.get_node_all_assets(node.id) + else: + assets = Asset.objects.none() + return assets + + def to_tree_nodes(self, assets): + if not assets: + return [] + assets = assets.annotate(tp=F('platform__type')) + asset_type_map = defaultdict(list) + for asset in assets: + asset_type_map[asset.tp].append(asset) + tp = self.tp + if tp: + assets = asset_type_map.get(tp, []) + if not assets: + return [] + pid = f'ROOT_{str(assets[0].category).upper()}_{tp}' + return self.serialize_assets(assets, pid=pid) + resource_platforms = assets.order_by('id').values_list('platform_id', flat=True) + node_all = AllTypes.get_tree_nodes(resource_platforms) + pattern = re.compile(r'\(0\)?') + nodes = [] + for node in node_all: + meta = node.get('meta', {}) + if pattern.search(node['name']) or meta.get('type') == 'platform': + continue + _type = meta.get('_type') + if _type: + node['type'] = _type + nodes.append(node) + + if not self.is_sync: + return nodes + + asset_nodes = [] + for node in nodes: + node['open'] = True + tp = node.get('meta', {}).get('_type') + if not tp: + continue + assets = asset_type_map.get(tp, []) + asset_nodes += self.serialize_assets(assets, pid=node['id']) + return nodes + asset_nodes + + def list(self, request, *args, **kwargs): + assets = self.get_assets() + nodes = self.to_tree_nodes(assets) + return Response(data=nodes) + + class UserGrantedK8sAsTreeApi(SelfOrPKUserMixin, ListAPIView): """ 用户授权的K8s树 """ diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 43876d864..4d037e4d3 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -97,6 +97,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): if condition in username_secret_type_dict: continue account_data = {key: getattr(template, key) for key in account_attribute} + account_data['su_from'] = template.get_su_from_account(asset) account_data['name'] = f"{account_data['name']}-{_('Account template')}" need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) return Account.objects.bulk_create(need_create_accounts) diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 4c175cba5..7d054c0d7 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -8,7 +8,7 @@ from rest_framework import serializers from accounts.models import Account from assets.const import Category, AllTypes from assets.models import Node, Asset, Platform -from assets.serializers.asset.common import AssetProtocolsPermsSerializer +from assets.serializers.asset.common import AssetProtocolsPermsSerializer, AssetLabelSerializer from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField @@ -25,13 +25,15 @@ class AssetPermedSerializer(OrgResourceModelSerializerMixin): protocols = AssetProtocolsPermsSerializer(many=True, required=False, label=_('Protocols')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) + labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) domain = ObjectRelatedField(required=False, queryset=Node.objects, label=_('Domain')) class Meta: model = Asset only_fields = [ - "id", "name", "address", 'domain', 'platform', - "comment", "org_id", "is_active", + 'id', 'name', 'address', 'domain', 'platform', + 'comment', 'org_id', 'is_active', 'date_verified', + 'created_by', 'date_created', 'connectivity', 'nodes', 'labels' ] fields = only_fields + ['protocols', 'category', 'type'] + ['org_name'] read_only_fields = fields diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py index ba172747c..4f3d4c71d 100644 --- a/apps/perms/urls/user_permission.py +++ b/apps/perms/urls/user_permission.py @@ -37,6 +37,9 @@ user_permission_urlpatterns = [ path('/nodes/children-with-assets/tree/', api.UserPermedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-node-children-with-assets-as-tree'), + path('/nodes/children-with-assets/category/tree/', + api.UserPermedNodeChildrenWithAssetsAsCategoryTreeApi.as_view(), + name='user-node-children-with-assets-as-category-tree'), # 同步树 path('/nodes/all-with-assets/tree/', api.UserPermedNodesWithAssetsAsTreeApi.as_view(), diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py index 12f979a4f..d054e67ce 100644 --- a/apps/perms/utils/user_perm.py +++ b/apps/perms/utils/user_perm.py @@ -1,15 +1,11 @@ -from assets.models import FavoriteAsset, Asset - from django.conf import settings from django.db.models import Q +from assets.models import FavoriteAsset, Asset from common.utils.common import timeit - from perms.models import AssetPermission, PermNode, UserAssetGrantedTreeNodeRelation - from .permission import AssetPermissionUtil - __all__ = ['AssetPermissionPermAssetUtil', 'UserPermAssetUtil', 'UserPermNodeUtil'] diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py index f8f21d2bf..72b36e5dd 100644 --- a/apps/rbac/models/rolebinding.py +++ b/apps/rbac/models/rolebinding.py @@ -1,9 +1,9 @@ -from django.utils.translation import gettext_lazy as _ -from django.db import models -from django.db.models import Q from django.conf import settings from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import Q from django.db.models.signals import post_save +from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import ValidationError from common.db.models import JMSBaseModel, CASCADE_SIGNAL_SKIP @@ -109,6 +109,13 @@ class RoleBinding(JMSBaseModel): def is_scope_org(self): return self.scope == Scope.org + @staticmethod + def orgs_order_by_name(orgs): + from orgs.models import Organization + default_system_org_ids = [Organization.DEFAULT_ID, Organization.SYSTEM_ID] + default_system_orgs = orgs.filter(id__in=default_system_org_ids) + return default_system_orgs | orgs.exclude(id__in=default_system_org_ids).order_by('name') + @classmethod def get_user_has_the_perm_orgs(cls, perm, user): from orgs.models import Organization @@ -134,6 +141,7 @@ class RoleBinding(JMSBaseModel): org_ids = [b.org.id for b in bindings if b.org] orgs = all_orgs.filter(id__in=org_ids) + orgs = cls.orgs_order_by_name(orgs) workbench_perm = 'rbac.view_workbench' # 全局组织 if orgs and perm != workbench_perm and user.has_perm('orgs.view_rootorg'): @@ -183,7 +191,7 @@ class OrgRoleBinding(RoleBinding): class SystemRoleBindingManager(RoleBindingManager): def get_queryset(self): - queryset = super(RoleBindingManager, self).get_queryset()\ + queryset = super(RoleBindingManager, self).get_queryset() \ .filter(scope=Scope.system) return queryset diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index e9861eb72..c195b5e4e 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -48,3 +48,4 @@ class PrivateSettingSerializer(PublicSettingSerializer): ANNOUNCEMENT = serializers.DictField() TICKETS_ENABLED = serializers.BooleanField() + CONNECTION_TOKEN_REUSABLE = serializers.BooleanField() diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 1dc41b037..891149add 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -94,15 +94,15 @@ class SecurityAuthSerializer(serializers.Serializer): ) USER_LOGIN_SINGLE_MACHINE_ENABLED = serializers.BooleanField( required=False, default=False, label=_("Only single device login"), - help_text=_("Next device login, pre login will be logout") + help_text=_("After the user logs in on the new device, other logged-in devices will automatically log out") ) ONLY_ALLOW_EXIST_USER_AUTH = serializers.BooleanField( required=False, default=False, label=_("Only exist user login"), - help_text=_("If enable, CAS、OIDC auth will be failed, if user not exist yet") + help_text=_("If enabled, non-existent users will not be allowed to log in; if disabled, users of other authentication methods except local authentication methods are allowed to log in and automatically create users (if the user does not exist)") ) ONLY_ALLOW_AUTH_FROM_SOURCE = serializers.BooleanField( required=False, default=False, label=_("Only from source login"), - help_text=_("Only log in from the user source property") + help_text=_("If it is enabled, the user will only authenticate to the source when logging in; if it is disabled, the user will authenticate all the enabled authentication methods in a certain order when logging in, and as long as one of the authentication methods is successful, they can log in directly") ) SECURITY_MFA_VERIFY_TTL = serializers.IntegerField( min_value=5, max_value=60 * 60 * 10, diff --git a/apps/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py index 89bc1f96c..cc40936e5 100644 --- a/apps/terminal/api/applet/host.py +++ b/apps/terminal/api/applet/host.py @@ -19,6 +19,9 @@ class AppletHostViewSet(JMSBulkModelViewSet): serializer_class = AppletHostSerializer queryset = AppletHost.objects.all() search_fields = ['asset_ptr__name', 'asset_ptr__address', ] + rbac_perms = { + 'generate_accounts': 'terminal.change_applethost', + } def dispatch(self, request, *args, **kwargs): with tmp_to_builtin_org(system=1): @@ -37,6 +40,12 @@ class AppletHostViewSet(JMSBulkModelViewSet): instance.check_terminal_binding(request) return Response({'msg': 'ok'}) + @action(methods=['put'], detail=True, url_path='generate-accounts') + def generate_accounts(self, request, *args, **kwargs): + instance = self.get_object() + instance.generate_accounts() + return Response({'msg': 'ok'}) + class AppletHostDeploymentViewSet(viewsets.ModelViewSet): serializer_class = AppletHostDeploymentSerializer diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 13cd62b7a..0661536f7 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -186,6 +186,8 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): tp = 'guacamole' if url.endswith('.cast.gz'): tp = 'asciicast' + if url.endswith('.replay.mp4'): + tp = 'mp4' download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id}) data = { diff --git a/apps/terminal/api/session/sharing.py b/apps/terminal/api/session/sharing.py index a3d324205..9a60734fa 100644 --- a/apps/terminal/api/session/sharing.py +++ b/apps/terminal/api/session/sharing.py @@ -1,8 +1,8 @@ -from rest_framework.exceptions import MethodNotAllowed, ValidationError -from rest_framework.decorators import action -from rest_framework.response import Response from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from rest_framework.decorators import action +from rest_framework.exceptions import MethodNotAllowed, ValidationError +from rest_framework.response import Response from common.const.http import PATCH from orgs.mixins.api import OrgModelViewSet diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index b68f6e06c..730f8e54f 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -25,6 +25,16 @@ register: rds_install - name: Stop Tinker before install (jumpserver) + ansible.windows.win_powershell: + script: | + if (Get-Process -Name 'tinker' -ErrorAction SilentlyContinue) { + TASKKILL /F /IM tinker.exe /T + } + else { + $Ansible.Changed = $false + } + + - name: Stop Tinkerd before install (jumpserver) ansible.windows.win_powershell: script: | if (Get-Service -Name 'JumpServer Tinker' -ErrorAction SilentlyContinue) { diff --git a/apps/terminal/const.py b/apps/terminal/const.py index fd0421427..cfec290c7 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -48,6 +48,7 @@ class TerminalType(TextChoices): magnus = 'magnus', 'Magnus' razor = 'razor', 'Razor' tinker = 'tinker', 'Tinker' + video_worker = 'video_worker', 'Video Worker' @classmethod def types(cls): diff --git a/apps/terminal/migrations/0050_auto_20220606_1745.py b/apps/terminal/migrations/0050_auto_20220606_1745.py index e88d37971..d0eb6ea5d 100644 --- a/apps/terminal/migrations/0050_auto_20220606_1745.py +++ b/apps/terminal/migrations/0050_auto_20220606_1745.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('terminal', '0049_endpoint_redis_port'), ] @@ -13,10 +12,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='terminal', name='type', - field=models.CharField(choices=[ - ('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), - ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), - ('magnus', 'Magnus'), ('razor', 'Razor'), ('tinker', 'Tinker'), - ], default='koko', max_length=64, verbose_name='type'), + field=models.CharField( + choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), + ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), + ('razor', 'Razor'), ('tinker', 'Tinker'), ('video_worker', 'Video Worker')], default='koko', + max_length=64, verbose_name='type'), ), ] diff --git a/apps/terminal/migrations/0061_applet_can_concurrent.py b/apps/terminal/migrations/0061_applet_can_concurrent.py new file mode 100644 index 000000000..4ca762e65 --- /dev/null +++ b/apps/terminal/migrations/0061_applet_can_concurrent.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-05-09 11:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0060_sessionsharing_action_permission'), + ] + + operations = [ + migrations.AddField( + model_name='applet', + name='can_concurrent', + field=models.BooleanField(default=True, verbose_name='Can concurrent'), + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index c7296eda6..2114740ef 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -32,6 +32,7 @@ class Applet(JMSBaseModel): is_active = models.BooleanField(default=True, verbose_name=_('Is active')) builtin = models.BooleanField(default=False, verbose_name=_('Builtin')) protocols = models.JSONField(default=list, verbose_name=_('Protocol')) + can_concurrent = models.BooleanField(default=True, verbose_name=_('Can concurrent')) tags = models.JSONField(default=list, verbose_name=_('Tags')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) hosts = models.ManyToManyField( @@ -134,37 +135,70 @@ class Applet(JMSBaseModel): shutil.copytree(path, pkg_path) return instance, serializer - def select_host_account(self): - # 选择激活的发布机 + def select_host(self, user): hosts = [ host for host in self.hosts.filter(is_active=True) if host.load != 'offline' ] - if not hosts: return None - key_tmpl = 'applet_host_accounts_{}_{}' - host = random.choice(hosts) - using_keys = cache.keys(key_tmpl.format(host.id, '*')) or [] - accounts_username_used = list(cache.get_many(using_keys).values()) - logger.debug('Applet host account using: {}: {}'.format(host.name, accounts_username_used)) - accounts = host.accounts.all() \ - .filter(is_active=True, privileged=False) \ - .exclude(username__in=accounts_username_used) + prefer_key = 'applet_host_prefer_{}'.format(user.id) + prefer_host_id = cache.get(prefer_key, None) + pref_host = [host for host in hosts if host.id == prefer_host_id] + if pref_host: + host = pref_host[0] + else: + host = random.choice(hosts) + cache.set(prefer_key, host.id, timeout=None) + return host - msg = 'Applet host remain accounts: {}: {}'.format(host.name, len(accounts)) + @staticmethod + def random_select_prefer_account(user, host, accounts): + msg = 'Applet host remain public accounts: {}: {}'.format(host.name, len(accounts)) if len(accounts) == 0: logger.error(msg) - else: - logger.debug(msg) - - if not accounts: return None + prefer_host_account_key = 'applet_host_prefer_account_{}_{}'.format(user.id, host.id) + prefer_account_id = cache.get(prefer_host_account_key, None) + prefer_account = None + if prefer_account_id: + prefer_account = accounts.filter(id=prefer_account_id).first() + if prefer_account: + account = prefer_account + else: + account = random.choice(accounts) + cache.set(prefer_host_account_key, account.id, timeout=None) + return account - account = random.choice(accounts) + def select_host_account(self, user): + # 选择激活的发布机 + host = self.select_host(user) + if not host: + return None + can_concurrent = self.can_concurrent and self.type == 'general' + + accounts = host.accounts.all().filter(is_active=True, privileged=False) + private_account = accounts.filter(username='js_{}'.format(user.username)).first() + accounts_using_key_tmpl = 'applet_host_accounts_{}_{}' + + if private_account and can_concurrent: + account = private_account + else: + using_keys = cache.keys(accounts_using_key_tmpl.format(host.id, '*')) or [] + accounts_username_used = list(cache.get_many(using_keys).values()) + logger.debug('Applet host account using: {}: {}'.format(host.name, accounts_username_used)) + + # 优先使用 private account + if private_account and private_account.username not in accounts_username_used: + account = private_account + else: + accounts = accounts.exclude(username__in=accounts_username_used).filter(username__startswith='jms_') + account = self.random_select_prefer_account(user, host, accounts) + if not account: + return ttl = 60 * 60 * 24 - lock_key = key_tmpl.format(host.id, account.username) + lock_key = accounts_using_key_tmpl.format(host.id, account.username) cache.set(lock_key, account.username, ttl) return { diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 47de718df..b0e7c478c 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -84,9 +84,13 @@ class AppletHost(Host): return random_string(16, special_char=True) def generate_accounts(self): - amount = int(os.getenv('TERMINAL_ACCOUNTS_AMOUNT', 100)) - now_count = self.accounts.filter(privileged=False).count() - need = amount - now_count + self.generate_public_accounts() + self.generate_private_accounts() + + def generate_public_accounts(self): + public_amount = int(os.getenv('TERMINAL_ACCOUNTS_AMOUNT', 100)) + now_count = self.accounts.filter(privileged=False, username__startswith='jms').count() + need = public_amount - now_count accounts = [] account_model = self.accounts.model @@ -99,7 +103,31 @@ class AppletHost(Host): org_id=self.LOCKING_ORG, is_active=False, ) accounts.append(account) - bulk_create_with_history(accounts, account_model, batch_size=20) + bulk_create_with_history(accounts, account_model, batch_size=20, ignore_conflicts=True) + + def generate_private_accounts_by_usernames(self, usernames): + accounts = [] + account_model = self.accounts.model + for username in usernames: + password = self.random_password() + username = 'js_' + username + account = account_model( + username=username, secret=password, name=username, + asset_id=self.id, secret_type='password', version=1, + org_id=self.LOCKING_ORG, is_active=False, + ) + accounts.append(account) + bulk_create_with_history(accounts, account_model, batch_size=20, ignore_conflicts=True) + + def generate_private_accounts(self): + from users.models import User + usernames = User.objects \ + .filter(is_active=True, is_service_account=False) \ + .values_list('username', flat=True) + account_usernames = self.accounts.all().values_list('username', flat=True) + account_usernames = [username[3:] for username in account_usernames if username.startswith('js_')] + not_exist_users = set(usernames) - set(account_usernames) + self.generate_private_accounts_by_usernames(not_exist_users) class AppletHostDeployment(JMSBaseModel): diff --git a/apps/terminal/models/session/session.py b/apps/terminal/models/session/session.py index 2e0f3a5c9..5ddd6e647 100644 --- a/apps/terminal/models/session/session.py +++ b/apps/terminal/models/session/session.py @@ -48,8 +48,8 @@ class Session(OrgModelMixin): upload_to = 'replay' ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' - SUFFIX_MAP = {1: '.gz', 2: '.replay.gz', 3: '.cast.gz'} - DEFAULT_SUFFIXES = ['.replay.gz', '.cast.gz', '.gz'] + SUFFIX_MAP = {1: '.gz', 2: '.replay.gz', 3: '.cast.gz', 4: '.replay.mp4'} + DEFAULT_SUFFIXES = ['.replay.gz', '.cast.gz', '.gz', '.replay.mp4'] # Todo: 将来干掉 local_path, 使用 default storage 实现 def get_all_possible_local_path(self): diff --git a/apps/terminal/models/session/sharing.py b/apps/terminal/models/session/sharing.py index a62e23a85..c0e83dfda 100644 --- a/apps/terminal/models/session/sharing.py +++ b/apps/terminal/models/session/sharing.py @@ -133,6 +133,13 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin): # self if self.verify_code != self.sharing.verify_code: return False, _('Invalid verification code') + + # Link can only be joined once by the same user. + queryset = SessionJoinRecord.objects.filter( + verify_code=self.verify_code, sharing=self.sharing, + joiner=self.joiner, date_joined__lt=self.date_joined) + if queryset.exists(): + return False, _('You have already joined this session') return True, '' def join_failed(self, reason): diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 572c1ecc3..67eea06be 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -61,7 +61,7 @@ class SessionDisplaySerializer(SessionSerializer): class ReplaySerializer(serializers.Serializer): file = serializers.FileField(allow_empty_file=True) - version = serializers.IntegerField(write_only=True, required=False, min_value=2, max_value=3) + version = serializers.IntegerField(write_only=True, required=False, min_value=2, max_value=4) class SessionJoinValidateSerializer(serializers.Serializer): diff --git a/apps/terminal/signal_handlers/applet.py b/apps/terminal/signal_handlers/applet.py index 18595fc7c..4fe390b8e 100644 --- a/apps/terminal/signal_handlers/applet.py +++ b/apps/terminal/signal_handlers/applet.py @@ -2,11 +2,14 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.utils.functional import LazyObject +from accounts.models import Account from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub from orgs.utils import tmp_to_builtin_org +from users.models import User from ..models import Applet, AppletHost +from ..tasks import applet_host_generate_accounts from ..utils import DBPortManager db_port_manager: DBPortManager @@ -19,12 +22,30 @@ def on_applet_host_create(sender, instance, created=False, **kwargs): return applets = Applet.objects.all() instance.applets.set(applets) - with tmp_to_builtin_org(system=1): - instance.generate_accounts() + applet_host_generate_accounts.delay(instance.id) applet_host_change_pub_sub.publish(True) +@receiver(post_save, sender=User) +def on_user_create_create_account(sender, instance, created=False, **kwargs): + if not created: + return + + with tmp_to_builtin_org(system=1): + applet_hosts = AppletHost.objects.all() + for host in applet_hosts: + host.generate_private_accounts_by_usernames([instance.username]) + + +@receiver(post_delete, sender=User) +def on_user_delete_remove_account(sender, instance, **kwargs): + with tmp_to_builtin_org(system=1): + applet_hosts = AppletHost.objects.all().values_list('id', flat=True) + accounts = Account.objects.filter(asset_id__in=applet_hosts, username=instance.username) + accounts.delete() + + @receiver(post_delete, sender=AppletHost) def on_applet_host_delete(sender, instance, **kwargs): applet_host_change_pub_sub.publish(True) diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 28356972f..66e6871d7 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -16,7 +16,7 @@ from ops.celery.decorator import ( from orgs.utils import tmp_to_builtin_org from .backends import server_replay_storage from .models import ( - Status, Session, Task, AppletHostDeployment + Status, Session, Task, AppletHostDeployment, AppletHost ) from .utils import find_session_replay_local @@ -82,7 +82,7 @@ def upload_session_replay_to_external_storage(session_id): @shared_task( verbose_name=_('Run applet host deployment'), - activity_callback=lambda self, did, *args, **kwargs: ([did], ) + activity_callback=lambda self, did, *args, **kwargs: ([did],) ) def run_applet_host_deployment(did): with tmp_to_builtin_org(system=1): @@ -98,3 +98,16 @@ def run_applet_host_deployment_install_applet(did, applet_id): with tmp_to_builtin_org(system=1): deployment = AppletHostDeployment.objects.get(id=did) deployment.install_applet(applet_id) + + +@shared_task( + verbose_name=_('Generate applet host accounts'), + activity_callback=lambda self, host_id, *args, **kwargs: ([host_id],) +) +def applet_host_generate_accounts(host_id): + applet_host = AppletHost.objects.filter(id=host_id).first() + if not applet_host: + return + + with tmp_to_builtin_org(system=1): + applet_host.generate_accounts() diff --git a/apps/terminal/utils/components.py b/apps/terminal/utils/components.py index a3de26322..100391dbd 100644 --- a/apps/terminal/utils/components.py +++ b/apps/terminal/utils/components.py @@ -140,7 +140,7 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil): for component in self.components: if not component.is_alive: continue - component_stat = component.latest_stat + component_stat = component.last_stat if not component_stat: continue metric_text = state_metric_text % ( diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index ac59ad13b..98587f266 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -40,6 +40,8 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer): ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { 'apply_accounts': {'required': False}, + 'apply_date_start': {'allow_null': False}, + 'apply_date_expired': {'allow_null': False}, } extra_kwargs.update(ticket_extra_kwargs) diff --git a/apps/users/api/user.py b/apps/users/api/user.py index c7882f6b2..7d964977c 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -12,13 +12,18 @@ from common.drf.filters import AttrRulesFilterBackend from common.utils import get_logger from orgs.utils import current_org, tmp_to_root_org from rbac.models import Role, RoleBinding +from rbac.permissions import RBACPermission from users.utils import LoginBlockUtil, MFABlockUtils from .mixins import UserQuerysetMixin from .. import serializers from ..filters import UserFilter from ..models import User from ..notifications import ResetMFAMsg -from ..serializers import UserSerializer, MiniUserSerializer, InviteSerializer +from ..permissions import UserObjectPermission +from ..serializers import ( + UserSerializer, + MiniUserSerializer, InviteSerializer +) from ..signals import post_user_create logger = get_logger(__name__) @@ -32,6 +37,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelV filterset_class = UserFilter extra_filter_backends = [AttrRulesFilterBackend] search_fields = ('username', 'email', 'name') + permission_classes = [RBACPermission, UserObjectPermission] serializer_classes = { 'default': UserSerializer, 'suggestion': MiniUserSerializer, diff --git a/apps/users/migrations/0040_alter_user_source.py b/apps/users/migrations/0040_alter_user_source.py index 56ad9befc..cb90c4971 100644 --- a/apps/users/migrations/0040_alter_user_source.py +++ b/apps/users/migrations/0040_alter_user_source.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='source', - field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS'), ('saml2', 'SAML2'), ('oauth2', 'OAuth2'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'), + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS'), ('saml2', 'SAML2'), ('oauth2', 'OAuth2'), ('wecom', 'WeCom'), ('dingtalk', 'DingTalk'), ('feishu', 'FeiShu'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'), ), ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 5d1edb036..a156478bd 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -703,14 +703,15 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract cas = 'cas', 'CAS' saml2 = 'saml2', 'SAML2' oauth2 = 'oauth2', 'OAuth2' + wecom = 'wecom', _('WeCom') + dingtalk = 'dingtalk', _('DingTalk') + feishu = 'feishu', _('FeiShu') custom = 'custom', 'Custom' SOURCE_BACKEND_MAPPING = { Source.local: [ settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY, - settings.AUTH_BACKEND_WECOM, - settings.AUTH_BACKEND_DINGTALK, ], Source.ldap: [ settings.AUTH_BACKEND_LDAP @@ -731,6 +732,15 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract Source.oauth2: [ settings.AUTH_BACKEND_OAUTH2 ], + Source.wecom: [ + settings.AUTH_BACKEND_WECOM + ], + Source.feishu: [ + settings.AUTH_BACKEND_FEISHU + ], + Source.dingtalk: [ + settings.AUTH_BACKEND_DINGTALK + ], Source.custom: [ settings.AUTH_BACKEND_CUSTOM ] diff --git a/apps/users/permissions.py b/apps/users/permissions.py index ee821f8d7..33081c5b3 100644 --- a/apps/users/permissions.py +++ b/apps/users/permissions.py @@ -1,5 +1,6 @@ from rest_framework import permissions +from rbac.builtin import BuiltinRole from .utils import is_auth_password_time_valid @@ -7,4 +8,16 @@ class IsAuthPasswdTimeValid(permissions.IsAuthenticated): def has_permission(self, request, view): return super().has_permission(request, view) \ - and is_auth_password_time_valid(request.session) + and is_auth_password_time_valid(request.session) + + +class UserObjectPermission(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + if view.action not in ['update', 'partial_update', 'destroy']: + return True + + if not request.user.is_superuser and obj.is_superuser: + return False + + return True diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index aa997d27f..eb8d35cd9 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -132,6 +132,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer "last_login", "date_updated" # 日期字段 ] fields_bool = [ + "is_superuser", "is_service_account", "is_valid", "is_expired", "is_active", # 布尔字段 "is_otp_secret_key_bound", "can_public_key_auth", diff --git a/apps/users/signal_handlers.py b/apps/users/signal_handlers.py index 487b3c917..2b2054ea3 100644 --- a/apps/users/signal_handlers.py +++ b/apps/users/signal_handlers.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # from django.conf import settings -from django.core.exceptions import PermissionDenied from django.db.models.signals import post_save from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ from django_auth_ldap.backend import populate_user from django_cas_ng.signals import cas_user_authenticated @@ -12,20 +12,33 @@ from authentication.backends.oidc.signals import openid_create_or_update_user from authentication.backends.saml2.signals import saml2_create_or_update_user from common.decorators import on_transaction_commit from common.utils import get_logger +from jumpserver.utils import get_current_request from .models import User, UserPasswordHistory from .signals import post_user_create logger = get_logger(__file__) -def user_authenticated_handle(user, created, source, attrs=None, **kwargs): +def check_only_allow_exist_user_auth(created): if created and settings.ONLY_ALLOW_EXIST_USER_AUTH: - user.delete() - raise PermissionDenied(f'Not allow non-exist user auth: {user.username}') + request = get_current_request() + request.user_need_delete = True + request.error_message = _( + '''The administrator has enabled "Only allow existing users to log in", + and the current user is not in the user list. Please contact the administrator.''' + ) + return False + return True + + +def user_authenticated_handle(user, created, source, attrs=None, **kwargs): if created: user.source = source user.save() + if not check_only_allow_exist_user_auth(created): + return + if not attrs: return @@ -122,10 +135,6 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): @receiver(openid_create_or_update_user) def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs): - if created and settings.ONLY_ALLOW_EXIST_USER_AUTH: - user.delete() - raise PermissionDenied(f'Not allow non-exist user auth: {username}') - if created: logger.debug( "Receive OpenID user created signal: {}, " @@ -133,7 +142,11 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna ) user.source = User.Source.openid.value user.save() - elif not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: + + if not check_only_allow_exist_user_auth(created): + return + + if not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: logger.debug( "Receive OpenID user updated signal: {}, " "Update user info: {}" diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e01baa999..a8ed07b01 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ aiofiles==22.1.0 amqp==5.0.9 -git+https://gitee.com/jumpserver/ansible@master#egg=ansible-core +git+https://github.com/jumpserver/ansible@master#egg=ansible-core ansible==7.1.0 ansible-runner==2.2.1 asn1crypto==0.24.0 @@ -68,7 +68,7 @@ geoip2==4.5.0 ipip-ipdb==1.6.1 pywinrm==0.4.3 # Django environment -Django==3.2.17 +Django==3.2.19 django-bootstrap3==14.2.0 django-filter==2.4.0 django-formtools==2.2 diff --git a/requirements/requirements_xpack.txt b/requirements/requirements_xpack.txt index 4f0f0ea7d..c7d48e644 100644 --- a/requirements/requirements_xpack.txt +++ b/requirements/requirements_xpack.txt @@ -5,6 +5,7 @@ azure-identity==1.5.0 azure-mgmt-compute==4.6.2 azure-mgmt-network==2.7.0 google-cloud-compute==0.5.0 +grpcio==1.54.2 alibabacloud_dysmsapi20170525==2.0.2 python-novaclient==11.0.1 python-keystoneclient==4.3.0 diff --git a/utils/disable_user_mfa.sh b/utils/disable_user_mfa.sh index ba98a9db8..0663c3643 100644 --- a/utils/disable_user_mfa.sh +++ b/utils/disable_user_mfa.sh @@ -16,7 +16,7 @@ user = User.objects.filter(username="${username}") if not user: print("No user found") sys.exit(1) -user.update(otp_level=0) +user.update(mfa_level=0) print("Disable user ${username} success") EOF }