diff --git a/.github/ISSUE_TEMPLATE/bug---.md b/.github/ISSUE_TEMPLATE/bug---.md index d668e0065..3c590459c 100644 --- a/.github/ISSUE_TEMPLATE/bug---.md +++ b/.github/ISSUE_TEMPLATE/bug---.md @@ -7,7 +7,7 @@ assignees: wojiushixiaobai --- -**JumpServer 版本(v1.5.9以下不再支持)** +**JumpServer 版本( v2.28 之前的版本不再支持 )** **浏览器版本** @@ -17,6 +17,6 @@ assignees: wojiushixiaobai **Bug 重现步骤(有截图更好)** -1. -2. -3. +1. +2. +3. diff --git a/.github/workflows/jms-build-test.yml b/.github/workflows/jms-build-test.yml index 044e3c016..636321324 100644 --- a/.github/workflows/jms-build-test.yml +++ b/.github/workflows/jms-build-test.yml @@ -24,6 +24,7 @@ jobs: build-args: | APT_MIRROR=http://deb.debian.org PIP_MIRROR=https://pypi.org/simple + PIP_JMS_MIRROR=https://pypi.org/simple cache-from: type=gha cache-to: type=gha,mode=max diff --git a/README.md b/README.md index cc7d85f8d..c5a1d2d2a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,17 @@ Stars

+ +

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

+ +| :warning: 注意 :warning: | +|:-------------------------------------------------------------------------------------------------------------------------:| +| 3.0 架构上和 2.0 变化较大,建议全新安装一套环境来体验。如需升级,请务必升级前进行备份,并[查阅文档](https://kb.fit2cloud.com/?p=06638d69-f109-4333-b5bf-65b17b297ed9) | + -------------------------- JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。 @@ -27,7 +38,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 ## UI 展示 -![UI展示](https://www.jumpserver.org/images/screenshot/1.png) +![UI展示](https://docs.jumpserver.org/zh/v3/img/dashboard.png) ## 在线体验 @@ -41,8 +52,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 ## 快速开始 -- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) -- [手动安装](https://github.com/jumpserver/installer) +- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/) - [产品文档](https://docs.jumpserver.org) - [知识库](https://kb.fit2cloud.com/categories/jumpserver) diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index 64dc745f8..d12f11549 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from accounts import serializers from accounts.filters import AccountFilterSet from accounts.models import Account -from assets.models import Asset +from assets.models import Asset, Node from common.permissions import UserConfirmation, ConfirmType from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet @@ -28,6 +28,7 @@ class AccountViewSet(OrgBulkModelViewSet): rbac_perms = { 'partial_update': ['accounts.change_account'], 'su_from_accounts': 'accounts.view_account', + 'username_suggestions': 'accounts.view_account', } @action(methods=['get'], detail=False, url_path='su-from-accounts') @@ -42,11 +43,34 @@ class AccountViewSet(OrgBulkModelViewSet): asset = get_object_or_404(Asset, pk=asset_id) accounts = asset.accounts.all() else: - accounts = [] + accounts = Account.objects.none() accounts = self.filter_queryset(accounts) serializer = serializers.AccountSerializer(accounts, many=True) return Response(data=serializer.data) + @action(methods=['get'], detail=False, url_path='username-suggestions') + def username_suggestions(self, request, *args, **kwargs): + asset_ids = request.query_params.get('assets') + node_keys = request.query_params.get('keys') + username = request.query_params.get('username') + + assets = Asset.objects.all() + if asset_ids: + assets = assets.filter(id__in=asset_ids.split(',')) + if node_keys: + patten = Node.get_node_all_children_key_pattern(node_keys.split(',')) + assets = assets.filter(nodes__key__regex=patten) + + accounts = Account.objects.filter(asset__in=assets) + if username: + accounts = accounts.filter(username__icontains=username) + usernames = list(accounts.values_list('username', flat=True).distinct()[:10]) + usernames.sort() + common = [i for i in usernames if i in usernames if i.lower() in ['root', 'admin', 'administrator']] + others = [i for i in usernames if i not in common] + usernames = common + others + return Response(data=usernames) + class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): """ diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index de4c4991e..3bbe902ae 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,15 +1,39 @@ -from rbac.permissions import RBACPermission -from common.permissions import UserConfirmation, ConfirmType +from django_filters import rest_framework as drf_filters -from common.views.mixins import RecordViewLogMixin -from orgs.mixins.api import OrgBulkModelViewSet +from assets.const import Protocol from accounts import serializers from accounts.models import AccountTemplate +from orgs.mixins.api import OrgBulkModelViewSet +from rbac.permissions import RBACPermission +from common.permissions import UserConfirmation, ConfirmType +from common.views.mixins import RecordViewLogMixin +from common.drf.filters import BaseFilterSet + + +class AccountTemplateFilterSet(BaseFilterSet): + protocols = drf_filters.CharFilter(method='filter_protocols') + + class Meta: + model = AccountTemplate + fields = ('username', 'name') + + @staticmethod + def filter_protocols(queryset, name, value): + secret_types = set() + protocols = value.split(',') + protocol_secret_type_map = Protocol.settings() + for p in protocols: + if p not in protocol_secret_type_map: + continue + _st = protocol_secret_type_map[p].get('secret_types', []) + secret_types.update(_st) + queryset = queryset.filter(secret_type__in=secret_types) + return queryset class AccountTemplateViewSet(OrgBulkModelViewSet): model = AccountTemplate - filterset_fields = ("username", 'name') + filterset_class = AccountTemplateFilterSet search_fields = ('username', 'name') serializer_classes = { 'default': serializers.AccountTemplateSerializer diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml index cca9d681b..3e3daae7f 100644 --- a/apps/accounts/automations/change_secret/host/aix/main.yml +++ b/apps/accounts/automations/change_secret/host/aix/main.yml @@ -9,12 +9,12 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('des') }}" update_password: always - when: secret_type == "password" + when: account.secret_type == "password" - name: create user If it already exists, no operation will be performed ansible.builtin.user: name: "{{ account.username }}" - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" - name: remove jumpserver ssh key ansible.builtin.lineinfile: @@ -22,7 +22,7 @@ regexp: "{{ kwargs.regexp }}" state: absent when: - - secret_type == "ssh_key" + - account.secret_type == "ssh_key" - kwargs.strategy == "set_jms" - name: Change SSH key @@ -30,7 +30,7 @@ user: "{{ account.username }}" key: "{{ account.secret }}" exclusive: "{{ kwargs.exclusive }}" - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -42,7 +42,7 @@ ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" ansible_become: no - when: secret_type == "password" + when: account.secret_type == "password" - name: Verify SSH key ansible.builtin.ping: @@ -51,4 +51,4 @@ ansible_user: "{{ account.username }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}" ansible_become: no - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml index b4e6aede6..932f3cade 100644 --- a/apps/accounts/automations/change_secret/host/posix/main.yml +++ b/apps/accounts/automations/change_secret/host/posix/main.yml @@ -9,12 +9,12 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always - when: secret_type == "password" + when: account.secret_type == "password" - name: create user If it already exists, no operation will be performed ansible.builtin.user: name: "{{ account.username }}" - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" - name: remove jumpserver ssh key ansible.builtin.lineinfile: @@ -22,7 +22,7 @@ regexp: "{{ kwargs.regexp }}" state: absent when: - - secret_type == "ssh_key" + - account.secret_type == "ssh_key" - kwargs.strategy == "set_jms" - name: Change SSH key @@ -30,7 +30,7 @@ user: "{{ account.username }}" key: "{{ account.secret }}" exclusive: "{{ kwargs.exclusive }}" - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -42,7 +42,7 @@ ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" ansible_become: no - when: secret_type == "password" + when: account.secret_type == "password" - name: Verify SSH key ansible.builtin.ping: @@ -51,4 +51,4 @@ ansible_user: "{{ account.username }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}" ansible_become: no - when: secret_type == "ssh_key" + when: account.secret_type == "ssh_key" diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index 3dd8dade6..41bad5bda 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -12,7 +12,7 @@ from accounts.models import ChangeSecretRecord from accounts.notifications import ChangeSecretExecutionTaskMsg from accounts.serializers import ChangeSecretRecordBackUpSerializer from assets.const import HostTypes -from common.utils import get_logger, lazyproperty +from common.utils import get_logger from common.utils.file import encrypt_and_compress_zip_file from common.utils.timezone import local_now_display from users.models import User @@ -28,23 +28,23 @@ class ChangeSecretManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.method_hosts_mapper = defaultdict(list) - self.secret_type = self.execution.snapshot['secret_type'] + self.secret_type = self.execution.snapshot.get('secret_type') self.secret_strategy = self.execution.snapshot.get( 'secret_strategy', SecretStrategy.custom ) self.ssh_key_change_strategy = self.execution.snapshot.get( 'ssh_key_change_strategy', SSHKeyStrategy.add ) - self.snapshot_account_usernames = self.execution.snapshot['accounts'] + self.account_ids = self.execution.snapshot['accounts'] self.name_recorder_mapper = {} # 做个映射,方便后面处理 @classmethod def method_type(cls): return AutomationTypes.change_secret - def get_kwargs(self, account, secret): + def get_kwargs(self, account, secret, secret_type): kwargs = {} - if self.secret_type != SecretType.SSH_KEY: + if secret_type != SecretType.SSH_KEY: return kwargs kwargs['strategy'] = self.ssh_key_change_strategy kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' @@ -54,18 +54,29 @@ class ChangeSecretManager(AccountBasePlaybookManager): kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) return kwargs - @lazyproperty - def secret_generator(self): + def secret_generator(self, secret_type): return SecretGenerator( - self.secret_strategy, self.secret_type, + self.secret_strategy, secret_type, self.execution.snapshot.get('password_rules') ) - def get_secret(self): + def get_secret(self, secret_type): if self.secret_strategy == SecretStrategy.custom: return self.execution.snapshot['secret'] else: - return self.secret_generator.get_secret() + return self.secret_generator(secret_type).get_secret() + + def get_accounts(self, privilege_account): + if not privilege_account: + print(f'not privilege account') + return [] + + asset = privilege_account.asset + accounts = asset.accounts.exclude(username=privilege_account.username) + accounts = accounts.filter(id__in=self.account_ids) + if self.secret_type: + accounts = accounts.filter(secret_type=self.secret_type) + return accounts def host_callback( self, host, asset=None, account=None, @@ -78,17 +89,10 @@ class ChangeSecretManager(AccountBasePlaybookManager): if host.get('error'): return host - accounts = asset.accounts.all() - if account: - accounts = accounts.exclude(username=account.username) - - if '*' not in self.snapshot_account_usernames: - accounts = accounts.filter(username__in=self.snapshot_account_usernames) - - accounts = accounts.filter(secret_type=self.secret_type) + accounts = self.get_accounts(account) if not accounts: - print('没有发现待改密账号: %s 用户名: %s 类型: %s' % ( - asset.name, self.snapshot_account_usernames, self.secret_type + print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % ( + asset.name, self.account_ids, self.secret_type )) return [] @@ -97,16 +101,16 @@ class ChangeSecretManager(AccountBasePlaybookManager): method_hosts = [h for h in method_hosts if h != host['name']] inventory_hosts = [] records = [] - host['secret_type'] = self.secret_type if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: - print(f'Windows {asset} does not support ssh key push \n') + print(f'Windows {asset} does not support ssh key push') return inventory_hosts for account in accounts: h = deepcopy(host) + secret_type = account.secret_type h['name'] += '(' + account.username + ')' - new_secret = self.get_secret() + new_secret = self.get_secret(secret_type) recorder = ChangeSecretRecord( asset=asset, account=account, execution=self.execution, @@ -116,15 +120,15 @@ class ChangeSecretManager(AccountBasePlaybookManager): self.name_recorder_mapper[h['name']] = recorder private_key_path = None - if self.secret_type == SecretType.SSH_KEY: + if secret_type == SecretType.SSH_KEY: private_key_path = self.generate_private_key_path(new_secret, path_dir) new_secret = self.generate_public_key(new_secret) - h['kwargs'] = self.get_kwargs(account, new_secret) + h['kwargs'] = self.get_kwargs(account, new_secret, secret_type) h['account'] = { 'name': account.name, 'username': account.username, - 'secret_type': account.secret_type, + 'secret_type': secret_type, 'secret': new_secret, 'private_key_path': private_key_path } @@ -206,7 +210,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): serializer = serializer_cls(recorders, many=True) header = [str(v.label) for v in serializer.child.fields.values()] - rows = [list(row.values()) for row in serializer.data] + rows = [[str(i) for i in row.values()] for row in serializer.data] if not rows: return False diff --git a/apps/accounts/automations/gather_accounts/filter.py b/apps/accounts/automations/gather_accounts/filter.py index f3c9e583d..af7cdefa4 100644 --- a/apps/accounts/automations/gather_accounts/filter.py +++ b/apps/accounts/automations/gather_accounts/filter.py @@ -60,4 +60,6 @@ class GatherAccountsFilter: if not run_method_name: return info - return getattr(self, f'{run_method_name}_filter')(info) + if hasattr(self, f'{run_method_name}_filter'): + return getattr(self, f'{run_method_name}_filter')(info) + return info diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py index 5be1f423b..2ecd3d2e1 100644 --- a/apps/accounts/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -22,8 +22,8 @@ class GatherAccountsManager(AccountBasePlaybookManager): self.host_asset_mapper[host['name']] = asset return host - def filter_success_result(self, host, result): - result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result) + def filter_success_result(self, tp, result): + result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result) return result @staticmethod diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py index b66ba436e..92fde0b37 100644 --- a/apps/accounts/automations/push_account/manager.py +++ b/apps/accounts/automations/push_account/manager.py @@ -1,9 +1,6 @@ from copy import deepcopy -from django.db.models import QuerySet - from accounts.const import AutomationTypes, SecretType -from accounts.models import Account from assets.const import HostTypes from common.utils import get_logger from ..base.manager import AccountBasePlaybookManager @@ -19,36 +16,6 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): def method_type(cls): return AutomationTypes.push_account - def create_nonlocal_accounts(self, accounts, snapshot_account_usernames, asset): - secret_type = self.secret_type - usernames = accounts.filter(secret_type=secret_type).values_list( - 'username', flat=True - ) - create_usernames = set(snapshot_account_usernames) - set(usernames) - create_account_objs = [ - Account( - name=f'{username}-{secret_type}', username=username, - secret_type=secret_type, asset=asset, - ) - for username in create_usernames - ] - Account.objects.bulk_create(create_account_objs) - - def get_accounts(self, privilege_account, accounts: QuerySet): - if not privilege_account: - print(f'not privilege account') - return [] - snapshot_account_usernames = self.execution.snapshot['accounts'] - if '*' in snapshot_account_usernames: - return accounts.exclude(username=privilege_account.username) - - asset = privilege_account.asset - self.create_nonlocal_accounts(accounts, snapshot_account_usernames, asset) - accounts = asset.accounts.exclude(username=privilege_account.username).filter( - username__in=snapshot_account_usernames, secret_type=self.secret_type - ) - return accounts - def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): host = super(ChangeSecretManager, self).host_callback( host, asset=asset, account=account, automation=automation, @@ -57,34 +24,36 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): if host.get('error'): return host - accounts = asset.accounts.all() - accounts = self.get_accounts(account, accounts) + accounts = self.get_accounts(account) inventory_hosts = [] - host['secret_type'] = self.secret_type if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: - msg = f'Windows {asset} does not support ssh key push \n' + msg = f'Windows {asset} does not support ssh key push' print(msg) return inventory_hosts for account in accounts: h = deepcopy(host) + secret_type = account.secret_type h['name'] += '(' + account.username + ')' - new_secret = self.get_secret() + if self.secret_type is None: + new_secret = account.secret + else: + new_secret = self.get_secret(secret_type) self.name_recorder_mapper[h['name']] = { 'account': account, 'new_secret': new_secret, } private_key_path = None - if self.secret_type == SecretType.SSH_KEY: + if secret_type == SecretType.SSH_KEY: private_key_path = self.generate_private_key_path(new_secret, path_dir) new_secret = self.generate_public_key(new_secret) - h['kwargs'] = self.get_kwargs(account, new_secret) + h['kwargs'] = self.get_kwargs(account, new_secret, secret_type) h['account'] = { 'name': account.name, 'username': account.username, - 'secret_type': account.secret_type, + 'secret_type': secret_type, 'secret': new_secret, 'private_key_path': private_key_path } @@ -112,9 +81,9 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): logger.error("Pust account error: ", e) def run(self, *args, **kwargs): - if not self.check_secret(): + if self.secret_type and not self.check_secret(): return - super().run(*args, **kwargs) + super(ChangeSecretManager, self).run(*args, **kwargs) # @classmethod # def trigger_by_asset_create(cls, asset): diff --git a/apps/accounts/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py index bf43eff46..b0e4a10ab 100644 --- a/apps/accounts/automations/verify_account/manager.py +++ b/apps/accounts/automations/verify_account/manager.py @@ -25,6 +25,15 @@ class VerifyAccountManager(AccountBasePlaybookManager): f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n') return path + @classmethod + def method_type(cls): + return AutomationTypes.verify_account + + def get_accounts(self, privilege_account, accounts: QuerySet): + account_ids = self.execution.snapshot['accounts'] + accounts = accounts.filter(id__in=account_ids) + return accounts + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): host = super().host_callback( host, asset=asset, account=account, @@ -62,16 +71,6 @@ class VerifyAccountManager(AccountBasePlaybookManager): inventory_hosts.append(h) return inventory_hosts - @classmethod - def method_type(cls): - return AutomationTypes.verify_account - - def get_accounts(self, privilege_account, accounts: QuerySet): - snapshot_account_usernames = self.execution.snapshot['accounts'] - if '*' not in snapshot_account_usernames: - accounts = accounts.filter(username__in=snapshot_account_usernames) - return accounts - def on_host_success(self, host, result): account = self.host_account_mapper.get(host) account.set_connectivity(Connectivity.OK) diff --git a/apps/accounts/automations/verify_gateway_account/manager.py b/apps/accounts/automations/verify_gateway_account/manager.py index 94da021b5..f6e4e38ab 100644 --- a/apps/accounts/automations/verify_gateway_account/manager.py +++ b/apps/accounts/automations/verify_gateway_account/manager.py @@ -1,6 +1,6 @@ -from common.utils import get_logger from accounts.const import AutomationTypes from assets.automations.ping_gateway.manager import PingGatewayManager +from common.utils import get_logger logger = get_logger(__name__) @@ -16,6 +16,6 @@ class VerifyGatewayAccountManager(PingGatewayManager): logger.info(">>> 开始执行测试网关账号可连接性任务") def get_accounts(self, gateway): - usernames = self.execution.snapshot['accounts'] - accounts = gateway.accounts.filter(username__in=usernames) + account_ids = self.execution.snapshot['accounts'] + accounts = gateway.accounts.filter(id__in=account_ids) return accounts diff --git a/apps/accounts/migrations/0009_account_usernames_to_ids.py b/apps/accounts/migrations/0009_account_usernames_to_ids.py new file mode 100644 index 000000000..895176b4c --- /dev/null +++ b/apps/accounts/migrations/0009_account_usernames_to_ids.py @@ -0,0 +1,69 @@ +# Generated by Django 3.2.16 on 2023-03-07 07:36 + +from django.db import migrations +from django.db.models import Q + + +def get_nodes_all_assets(apps, *nodes): + node_model = apps.get_model('assets', 'Node') + asset_model = apps.get_model('assets', 'Asset') + node_ids = set() + descendant_node_query = Q() + for n in nodes: + node_ids.add(n.id) + descendant_node_query |= Q(key__istartswith=f'{n.key}:') + if descendant_node_query: + _ids = node_model.objects.order_by().filter(descendant_node_query).values_list('id', flat=True) + node_ids.update(_ids) + return asset_model.objects.order_by().filter(nodes__id__in=node_ids).distinct() + + +def get_all_assets(apps, snapshot): + node_model = apps.get_model('assets', 'Node') + asset_model = apps.get_model('assets', 'Asset') + asset_ids = snapshot.get('assets', []) + node_ids = snapshot.get('nodes', []) + + nodes = node_model.objects.filter(id__in=node_ids) + node_asset_ids = get_nodes_all_assets(apps, *nodes).values_list('id', flat=True) + asset_ids = set(list(asset_ids) + list(node_asset_ids)) + return asset_model.objects.filter(id__in=asset_ids) + + +def migrate_account_usernames_to_ids(apps, schema_editor): + db_alias = schema_editor.connection.alias + execution_model = apps.get_model('accounts', 'AutomationExecution') + account_model = apps.get_model('accounts', 'Account') + executions = execution_model.objects.using(db_alias).all() + executions_update = [] + for execution in executions: + snapshot = execution.snapshot + accounts = account_model.objects.none() + account_usernames = snapshot.get('accounts', []) + for asset in get_all_assets(apps, snapshot): + accounts = accounts | asset.accounts.all() + secret_type = snapshot.get('secret_type') + if secret_type: + ids = accounts.filter( + username__in=account_usernames, + secret_type=secret_type + ).values_list('id', flat=True) + else: + ids = accounts.filter( + username__in=account_usernames + ).values_list('id', flat=True) + snapshot['accounts'] = [str(_id) for _id in ids] + execution.snapshot = snapshot + executions_update.append(execution) + + execution_model.objects.bulk_update(executions_update, ['snapshot']) + + +class Migration(migrations.Migration): + dependencies = [ + ('accounts', '0008_alter_gatheredaccount_options'), + ] + + operations = [ + migrations.RunPython(migrate_account_usernames_to_ids), + ] diff --git a/apps/accounts/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py index 76ae9b4f2..d4ad77608 100644 --- a/apps/accounts/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -1,11 +1,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.db import fields -from common.db.models import JMSBaseModel from accounts.const import ( AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy ) +from accounts.models import Account +from common.db import fields +from common.db.models import JMSBaseModel from .base import AccountBaseAutomation __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin'] @@ -27,18 +28,35 @@ class ChangeSecretMixin(models.Model): default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') ) + accounts: list[str] # account usernames + get_all_assets: callable # get all assets + class Meta: abstract = True + def create_nonlocal_accounts(self, usernames, asset): + pass + + def get_account_ids(self): + usernames = self.accounts + accounts = Account.objects.none() + for asset in self.get_all_assets(): + self.create_nonlocal_accounts(usernames, asset) + accounts = accounts | asset.accounts.all() + account_ids = accounts.filter( + username__in=usernames, secret_type=self.secret_type + ).values_list('id', flat=True) + return [str(_id) for _id in account_ids] + def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ 'secret': self.secret, 'secret_type': self.secret_type, - 'secret_strategy': self.secret_strategy, + 'accounts': self.get_account_ids(), 'password_rules': self.password_rules, + 'secret_strategy': self.secret_strategy, 'ssh_key_change_strategy': self.ssh_key_change_strategy, - }) return attr_json diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py index 411e8cb12..f189a5fbd 100644 --- a/apps/accounts/models/automations/push_account.py +++ b/apps/accounts/models/automations/push_account.py @@ -2,6 +2,8 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from accounts.const import AutomationTypes +from accounts.models import Account +from jumpserver.utils import has_valid_xpack_license from .base import AccountBaseAutomation from .change_secret import ChangeSecretMixin @@ -13,6 +15,21 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): username = models.CharField(max_length=128, verbose_name=_('Username')) action = models.CharField(max_length=16, verbose_name=_('Action')) + def create_nonlocal_accounts(self, usernames, asset): + secret_type = self.secret_type + account_usernames = asset.accounts.filter(secret_type=self.secret_type).values_list( + 'username', flat=True + ) + create_usernames = set(usernames) - set(account_usernames) + create_account_objs = [ + Account( + name=f'{username}-{secret_type}', username=username, + secret_type=secret_type, asset=asset, + ) + for username in create_usernames + ] + Account.objects.bulk_create(create_account_objs) + def set_period_schedule(self): pass @@ -27,6 +44,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.push_account + if not has_valid_xpack_license(): + self.is_periodic = False super().save(*args, **kwargs) def to_attr_json(self): diff --git a/apps/accounts/models/base.py b/apps/accounts/models/base.py index 7233a12a2..e4bd780ac 100644 --- a/apps/accounts/models/base.py +++ b/apps/accounts/models/base.py @@ -12,7 +12,7 @@ from accounts.const import SecretType from common.db import fields from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, - random_string, lazyproperty, parse_ssh_public_key_str + random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key ) from orgs.mixins.models import JMSOrgBaseModel, OrgManager @@ -118,7 +118,13 @@ class BaseAccount(JMSOrgBaseModel): key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() key_path = os.path.join(tmp_dir, key_name) if not os.path.exists(key_path): - self.private_key_obj.write_private_key_file(key_path) + # https://github.com/ansible/ansible-runner/issues/544 + # ssh requires OpenSSH format keys to have a full ending newline. + # It does not require this for old-style PEM keys. + with open(key_path, 'w') as f: + f.write(self.secret) + if is_openssh_format_key(self.secret.encode('utf-8')): + f.write("\n") os.chmod(key_path, 0o400) return key_path diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py index 489bd794f..f31f19196 100644 --- a/apps/accounts/serializers/account/base.py +++ b/apps/accounts/serializers/account/base.py @@ -33,7 +33,8 @@ class AuthValidateMixin(serializers.Serializer): return secret elif secret_type == SecretType.SSH_KEY: passphrase = passphrase if passphrase else None - return validate_ssh_key(secret, passphrase) + secret = validate_ssh_key(secret, passphrase) + return secret else: return secret @@ -41,8 +42,9 @@ class AuthValidateMixin(serializers.Serializer): secret_type = validated_data.get('secret_type') passphrase = validated_data.get('passphrase') secret = validated_data.pop('secret', None) - self.handle_secret(secret, secret_type, passphrase) - validated_data['secret'] = secret + validated_data['secret'] = self.handle_secret( + secret, secret_type, passphrase + ) for field in ('secret',): value = validated_data.get(field) if not value: diff --git a/apps/accounts/tasks/automation.py b/apps/accounts/tasks/automation.py index 2b9f99235..359a15913 100644 --- a/apps/accounts/tasks/automation.py +++ b/apps/accounts/tasks/automation.py @@ -8,7 +8,7 @@ from orgs.utils import tmp_to_org, tmp_to_root_org logger = get_logger(__file__) -def task_activity_callback(self, pid, trigger, tp): +def task_activity_callback(self, pid, trigger, tp, *args, **kwargs): model = AutomationTypes.get_type_model(tp) with tmp_to_root_org(): instance = get_object_or_none(model, pk=pid) diff --git a/apps/accounts/tasks/backup_account.py b/apps/accounts/tasks/backup_account.py index 1f4e46d83..16eafaf5e 100644 --- a/apps/accounts/tasks/backup_account.py +++ b/apps/accounts/tasks/backup_account.py @@ -9,7 +9,7 @@ from orgs.utils import tmp_to_org, tmp_to_root_org logger = get_logger(__file__) -def task_activity_callback(self, pid, trigger): +def task_activity_callback(self, pid, trigger, *args, **kwargs): from accounts.models import AccountBackupAutomation with tmp_to_root_org(): plan = get_object_or_none(AccountBackupAutomation, pk=pid) diff --git a/apps/accounts/tasks/gather_accounts.py b/apps/accounts/tasks/gather_accounts.py index ceead3f9d..42f8641bb 100644 --- a/apps/accounts/tasks/gather_accounts.py +++ b/apps/accounts/tasks/gather_accounts.py @@ -27,7 +27,7 @@ def gather_asset_accounts_util(nodes, task_name): @shared_task( queue="ansible", verbose_name=_('Gather asset accounts'), - activity_callback=lambda self, node_ids, task_name=None: (node_ids, None) + activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None) ) def gather_asset_accounts_task(node_ids, task_name=None): if task_name is None: diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py index 10dd4ce64..2a753cc1a 100644 --- a/apps/accounts/tasks/push_account.py +++ b/apps/accounts/tasks/push_account.py @@ -13,7 +13,7 @@ __all__ = [ @shared_task( queue="ansible", verbose_name=_('Push accounts to assets'), - activity_callback=lambda self, account_ids, asset_ids: (account_ids, None) + activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None) ) def push_accounts_to_assets_task(account_ids): from accounts.models import PushAccountAutomation @@ -23,12 +23,10 @@ def push_accounts_to_assets_task(account_ids): task_name = gettext_noop("Push accounts to assets") task_name = PushAccountAutomation.generate_unique_name(task_name) - for account in accounts: - task_snapshot = { - 'secret': account.secret, - 'secret_type': account.secret_type, - 'accounts': [account.username], - 'assets': [str(account.asset_id)], - } - tp = AutomationTypes.push_account - quickstart_automation_by_snapshot(task_name, tp, task_snapshot) + task_snapshot = { + 'accounts': [str(account.id) for account in accounts], + 'assets': [str(account.asset_id) for account in accounts], + } + + tp = AutomationTypes.push_account + quickstart_automation_by_snapshot(task_name, tp, task_snapshot) diff --git a/apps/accounts/tasks/verify_account.py b/apps/accounts/tasks/verify_account.py index 5f221fcd6..523e7f3d2 100644 --- a/apps/accounts/tasks/verify_account.py +++ b/apps/accounts/tasks/verify_account.py @@ -17,9 +17,9 @@ __all__ = [ def verify_connectivity_util(assets, tp, accounts, task_name): if not assets or not accounts: return - account_usernames = list(accounts.values_list('username', flat=True)) + account_ids = [str(account.id) for account in accounts] task_snapshot = { - 'accounts': account_usernames, + 'accounts': account_ids, 'assets': [str(asset.id) for asset in assets], } quickstart_automation_by_snapshot(task_name, tp, task_snapshot) diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index d8b46baab..c917e09d9 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -99,13 +99,14 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ("platform", serializers.PlatformSerializer), ("suggestion", serializers.MiniAssetSerializer), ("gateways", serializers.GatewaySerializer), - ("spec_info", serializers.SpecSerializer) + ("spec_info", serializers.SpecSerializer), ) rbac_perms = ( ("match", "assets.match_asset"), ("platform", "assets.view_platform"), ("gateways", "assets.view_gateway"), ("spec_info", "assets.view_asset"), + ("info", "assets.view_asset"), ) extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py index d923b2b84..b92448bfb 100644 --- a/apps/assets/api/asset/host.py +++ b/apps/assets/api/asset/host.py @@ -21,4 +21,10 @@ class HostViewSet(AssetViewSet): @action(methods=["GET"], detail=True, url_path="info") def info(self, *args, **kwargs): asset = super().get_object() - return Response(asset.info) + serializer = self.get_serializer(asset.info) + data = serializer.data + data['asset'] = { + 'id': asset.id, 'name': asset.name, + 'address': asset.address + } + return Response(data) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 084ac0c82..7184d91f7 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -12,8 +12,7 @@ from django.utils.translation import gettext as _ from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError from assets.automations.methods import platform_automation_methods -from common.utils import get_logger, lazyproperty -from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj +from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) @@ -127,7 +126,13 @@ class BasePlaybookManager: key_path = os.path.join(path_dir, key_name) if not os.path.exists(key_path): - ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) + # https://github.com/ansible/ansible-runner/issues/544 + # ssh requires OpenSSH format keys to have a full ending newline. + # It does not require this for old-style PEM keys. + with open(key_path, 'w') as f: + f.write(secret) + if is_openssh_format_key(secret.encode('utf-8')): + f.write("\n") os.chmod(key_path, 0o400) return key_path diff --git a/apps/assets/automations/gather_facts/format_asset_info.py b/apps/assets/automations/gather_facts/format_asset_info.py new file mode 100644 index 000000000..d3184bf59 --- /dev/null +++ b/apps/assets/automations/gather_facts/format_asset_info.py @@ -0,0 +1,35 @@ +__all__ = ['FormatAssetInfo'] + + +class FormatAssetInfo: + + def __init__(self, tp): + self.tp = tp + + @staticmethod + def posix_format(info): + for cpu_model in info.get('cpu_model', []): + if cpu_model.endswith('GHz') or cpu_model.startswith("Intel"): + break + else: + cpu_model = '' + info['cpu_model'] = cpu_model[:48] + info['cpu_count'] = info.get('cpu_count', 0) + return info + + def run(self, method_id_meta_mapper, info): + for k, v in info.items(): + info[k] = v.strip() if isinstance(v, str) else v + + run_method_name = None + for k, v in method_id_meta_mapper.items(): + if self.tp not in v['type']: + continue + run_method_name = k.replace(f'{v["method"]}_', '') + + if not run_method_name: + return info + + if hasattr(self, f'{run_method_name}_format'): + return getattr(self, f'{run_method_name}_format')(info) + return info diff --git a/apps/assets/automations/gather_facts/host/posix/main.yml b/apps/assets/automations/gather_facts/host/posix/main.yml index 760ca601e..ba86d9a91 100644 --- a/apps/assets/automations/gather_facts/host/posix/main.yml +++ b/apps/assets/automations/gather_facts/host/posix/main.yml @@ -11,7 +11,7 @@ cpu_count: "{{ ansible_processor_count }}" cpu_cores: "{{ ansible_processor_cores }}" cpu_vcpus: "{{ ansible_processor_vcpus }}" - memory: "{{ ansible_memtotal_mb }}" + memory: "{{ ansible_memtotal_mb / 1024 | round(2) }}" disk_total: "{{ (ansible_mounts | map(attribute='size_total') | sum / 1024 / 1024 / 1024) | round(2) }}" distribution: "{{ ansible_distribution }}" distribution_version: "{{ ansible_distribution_version }}" diff --git a/apps/assets/automations/gather_facts/manager.py b/apps/assets/automations/gather_facts/manager.py index 5ec25cb17..afd5ce523 100644 --- a/apps/assets/automations/gather_facts/manager.py +++ b/apps/assets/automations/gather_facts/manager.py @@ -1,5 +1,6 @@ -from common.utils import get_logger from assets.const import AutomationTypes +from common.utils import get_logger +from .format_asset_info import FormatAssetInfo from ..base.manager import BasePlaybookManager logger = get_logger(__name__) @@ -19,13 +20,16 @@ class GatherFactsManager(BasePlaybookManager): self.host_asset_mapper[host['name']] = asset return host + def format_asset_info(self, tp, info): + info = FormatAssetInfo(tp).run(self.method_id_meta_mapper, info) + return info + def on_host_success(self, host, result): info = result.get('debug', {}).get('res', {}).get('info', {}) asset = self.host_asset_mapper.get(host) if asset and info: - for k, v in info.items(): - info[k] = v.strip() if isinstance(v, str) else v + info = self.format_asset_info(asset.type, info) asset.info = info - asset.save() + asset.save(update_fields=['info']) else: logger.error("Not found info: {}".format(host)) diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index be2637ddf..12c4f09dd 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -1,10 +1,12 @@ +from django.utils.translation import gettext_lazy as _ + from .base import BaseType class CloudTypes(BaseType): - PUBLIC = 'public', 'Public cloud' - PRIVATE = 'private', 'Private cloud' - K8S = 'k8s', 'Kubernetes' + PUBLIC = 'public', _('Public cloud') + PRIVATE = 'private', _('Private cloud') + K8S = 'k8s', _('Kubernetes') @classmethod def _get_base_constrains(cls) -> dict: diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index e7abba3fc..eb7930b09 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext_lazy as _ + from .base import BaseType GATEWAY_NAME = 'Gateway' @@ -7,7 +9,7 @@ class HostTypes(BaseType): LINUX = 'linux', 'Linux' WINDOWS = 'windows', 'Windows' UNIX = 'unix', 'Unix' - OTHER_HOST = 'other', "Other" + OTHER_HOST = 'other', _("Other") @classmethod def _get_base_constrains(cls) -> dict: diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index b8efc654f..884a53785 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -39,7 +39,7 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 3389, 'secret_types': ['password'], 'setting': { - 'console': True, + 'console': False, 'security': 'any', } }, diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index cde7c8aba..00a496eee 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -214,10 +214,13 @@ class AllTypes(ChoicesMixin): tp_node = cls.choice_to_node(tp, category_node['id'], opened=False, meta=meta) tp_count = category_type_mapper.get(category + '_' + tp, 0) tp_node['name'] += f'({tp_count})' + platforms = tp_platforms.get(category + '_' + tp, []) + if not platforms: + tp_node['isParent'] = False nodes.append(tp_node) # Platform 格式化 - for p in tp_platforms.get(category + '_' + tp, []): + for p in platforms: platform_node = cls.platform_to_node(p, tp_node['id'], include_asset) platform_node['name'] += f'({platform_count.get(p.id, 0)})' nodes.append(platform_node) @@ -306,10 +309,11 @@ class AllTypes(ChoicesMixin): protocols_data = deepcopy(default_protocols) if _protocols: protocols_data = [p for p in protocols_data if p['name'] in _protocols] + for p in protocols_data: setting = _protocols_setting.get(p['name'], {}) - p['required'] = p.pop('required', False) - p['default'] = p.pop('default', False) + p['required'] = setting.pop('required', False) + p['default'] = setting.pop('default', False) p['setting'] = {**p.get('setting', {}), **setting} platform_data = { diff --git a/apps/assets/migrations/0093_auto_20220403_1627.py b/apps/assets/migrations/0093_auto_20220403_1627.py index d28d8cfc0..c34c70ed7 100644 --- a/apps/assets/migrations/0093_auto_20220403_1627.py +++ b/apps/assets/migrations/0093_auto_20220403_1627.py @@ -93,7 +93,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='asset', name='address', - field=models.CharField(db_index=True, max_length=1024, verbose_name='Address'), + field=models.CharField(db_index=True, max_length=767, verbose_name='Address'), ), migrations.AddField( model_name='asset', diff --git a/apps/assets/migrations/0098_auto_20220430_2126.py b/apps/assets/migrations/0098_auto_20220430_2126.py index 89a168316..5fd333262 100644 --- a/apps/assets/migrations/0098_auto_20220430_2126.py +++ b/apps/assets/migrations/0098_auto_20220430_2126.py @@ -34,8 +34,9 @@ def migrate_database_to_asset(apps, *args): _attrs = app.attrs or {} attrs.update(_attrs) + name = 'DB-{}'.format(app.name) db = db_model( - id=app.id, name=app.name, address=attrs['host'], + id=app.id, name=name, address=attrs['host'], protocols='{}/{}'.format(app.type, attrs['port']), db_name=attrs['database'] or '', platform=platforms_map[app.type], @@ -61,8 +62,9 @@ def migrate_cloud_to_asset(apps, *args): for app in applications: attrs = app.attrs print("\t- Create cloud: {}".format(app.name)) + name = 'Cloud-{}'.format(app.name) cloud = cloud_model( - id=app.id, name=app.name, + id=app.id, name=name, address=attrs.get('cluster', ''), protocols='k8s/443', platform=platform, org_id=app.org_id, diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index bba1dad43..cd5732630 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -1,12 +1,15 @@ # Generated by Django 3.2.12 on 2022-07-11 06:13 import time +from django.utils import timezone +from itertools import groupby from django.db import migrations def migrate_asset_accounts(apps, schema_editor): auth_book_model = apps.get_model('assets', 'AuthBook') account_model = apps.get_model('accounts', 'Account') + account_history_model = apps.get_model('accounts', 'HistoricalAccount') count = 0 bulk_size = 1000 @@ -20,34 +23,35 @@ def migrate_asset_accounts(apps, schema_editor): break count += len(auth_books) - accounts = [] # auth book 和 account 相同的属性 same_attrs = [ 'id', 'username', 'comment', 'date_created', 'date_updated', 'created_by', 'asset_id', 'org_id', ] - # 认证的属性,可能是 authbook 的,可能是 systemuser 的 + # 认证的属性,可能是 auth_book 的,可能是 system_user 的 auth_attrs = ['password', 'private_key', 'token'] all_attrs = same_attrs + auth_attrs + accounts = [] for auth_book in auth_books: - values = {'version': 1} + account_values = {'version': 1} system_user = auth_book.systemuser if system_user: # 更新一次系统用户的认证属性 - values.update({attr: getattr(system_user, attr, '') for attr in all_attrs}) - values['created_by'] = str(system_user.id) - values['privileged'] = system_user.type == 'admin' + account_values.update({attr: getattr(system_user, attr, '') for attr in all_attrs}) + account_values['created_by'] = str(system_user.id) + account_values['privileged'] = system_user.type == 'admin' \ + or system_user.username in ['root', 'Administrator'] auth_book_auth = {attr: getattr(auth_book, attr, '') for attr in all_attrs if getattr(auth_book, attr, '')} - # 最终使用 authbook 的认证属性 - values.update(auth_book_auth) + # 最终优先使用 auth_book 的认证属性 + account_values.update(auth_book_auth) auth_infos = [] - username = values['username'] + username = account_values['username'] for attr in auth_attrs: - secret = values.pop(attr, None) + secret = account_values.pop(attr, None) if not secret: continue @@ -66,13 +70,48 @@ def migrate_asset_accounts(apps, schema_editor): auth_infos.append((username, 'password', '')) for name, secret_type, secret in auth_infos: - account = account_model(**values, name=name, secret=secret, secret_type=secret_type) + if not name: + continue + account = account_model(**account_values, name=name, secret=secret, secret_type=secret_type) accounts.append(account) - account_model.objects.bulk_create(accounts, ignore_conflicts=True) + accounts.sort(key=lambda x: (x.name, x.asset_id, x.date_updated)) + grouped_accounts = groupby(accounts, lambda x: (x.name, x.asset_id)) + + accounts_to_add = [] + accounts_to_history = [] + for key, _accounts in grouped_accounts: + _accounts = list(_accounts) + if not _accounts: + continue + _account = _accounts[-1] + accounts_to_add.append(_account) + _account_history = [] + + for ac in _accounts: + if not ac.secret: + continue + if ac.id != _account.id and ac.secret == _account.secret: + continue + history_data = { + 'id': _account.id, + 'secret': ac.secret, + 'secret_type': ac.secret_type, + 'history_date': ac.date_updated, + 'history_type': '~', + 'history_change_reason': 'from account {}'.format(_account.name), + } + _account_history.append(account_history_model(**history_data)) + _account.version = len(_account_history) + accounts_to_history.extend(_account_history) + + account_model.objects.bulk_create(accounts_to_add, ignore_conflicts=True) + account_history_model.objects.bulk_create(accounts_to_history, ignore_conflicts=True) print("\t - Create asset accounts: {}-{} using: {:.2f}s".format( count - len(auth_books), count, time.time() - start )) + print("\t - accounts: {}".format(len(accounts_to_add))) + print("\t - histories: {}".format(len(accounts_to_history))) def migrate_db_accounts(apps, schema_editor): @@ -130,6 +169,9 @@ def migrate_db_accounts(apps, schema_editor): values['secret_type'] = secret_type values['secret'] = secret + if not name: + continue + for app in apps: values['asset_id'] = str(app.id) account = account_model(**values) diff --git a/apps/assets/migrations/0110_auto_20230315_1741.py b/apps/assets/migrations/0110_auto_20230315_1741.py new file mode 100644 index 000000000..2c0468d87 --- /dev/null +++ b/apps/assets/migrations/0110_auto_20230315_1741.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.17 on 2023-03-15 09:41 + +from django.db import migrations + + +def set_windows_platform_non_console(apps, schema_editor): + Platform = apps.get_model('assets', 'Platform') + names = ['Windows', 'Windows-RDP', 'Windows-TLS', 'RemoteAppHost'] + windows = Platform.objects.filter(name__in=names) + if not windows: + return + + for p in windows: + rdp = p.protocols.filter(name='rdp').first() + if not rdp: + continue + rdp.setting['console'] = False + rdp.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0109_alter_asset_options'), + ] + + operations = [ + migrations.RunPython(set_windows_platform_non_console) + ] diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 54fa473bd..3a38699e8 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -100,7 +100,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): Type = const.AllTypes name = models.CharField(max_length=128, verbose_name=_('Name')) - address = models.CharField(max_length=1024, verbose_name=_('Address'), db_index=True) + address = models.CharField(max_length=767, verbose_name=_('Address'), db_index=True) platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) @@ -108,7 +108,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) - info = models.JSONField(verbose_name='Info', default=dict, blank=True) # 资产的一些信息,如 硬件信息 + info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息 objects = AssetManager.from_queryset(AssetQuerySet)() diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 8f6ee45f3..32bfcaa09 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -489,7 +489,7 @@ class SomeNodesMixin: return cls.default_node() if ori_org and ori_org.is_root(): - return None + return cls.default_node() org_roots = cls.org_root_nodes() org_roots_length = len(org_roots) diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index 929eff10e..488525ccd 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -11,7 +11,7 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] class PlatformProtocol(models.Model): SETTING_ATTRS = { - 'console': True, + 'console': False, 'security': 'any,tls,rdp', 'sftp_enabled': True, 'sftp_home': '/tmp' diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index 1dc6aa28d..d7dc7b175 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -26,6 +26,13 @@ __all__ = [ class AssetProtocolsSerializer(serializers.ModelSerializer): port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1) + def to_file_representation(self, data): + return '{name}/{port}'.format(**data) + + def to_file_internal_value(self, data): + name, port = data.split('/') + return {'name': name, 'port': port} + class Meta: model = Protocol fields = ['name', 'port'] @@ -73,7 +80,7 @@ class AssetAccountSerializer( 'is_active', 'version', 'secret_type', ] fields_write_only = [ - 'secret', 'push_now', 'template' + 'secret', 'passphrase', 'push_now', 'template' ] fields = fields_mini + fields_write_only extra_kwargs = { @@ -121,7 +128,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) - accounts = AssetAccountSerializer(many=True, required=False, write_only=True, label=_('Account')) + accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account')) + nodes_display = serializers.ListField(read_only=True, label=_("Node path")) class Meta: model = Asset @@ -133,11 +141,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali 'nodes_display', 'accounts' ] read_only_fields = [ - 'category', 'type', 'connectivity', + 'category', 'type', 'connectivity', 'auto_info', 'date_verified', 'created_by', 'date_created', - 'auto_info', ] fields = fields_small + fields_fk + fields_m2m + read_only_fields + fields_unexport = ['auto_info'] extra_kwargs = { 'auto_info': {'label': _('Auto info')}, 'name': {'label': _("Name")}, @@ -150,7 +158,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali self._init_field_choices() def _get_protocols_required_default(self): - platform = self._initial_data_platform + platform = self._asset_platform platform_protocols = platform.protocols.all() protocols_default = [p for p in platform_protocols if p.default] protocols_required = [p for p in platform_protocols if p.required or p.primary] @@ -206,20 +214,22 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali instance.nodes.set(nodes_to_set) @property - def _initial_data_platform(self): - if self.instance: - return self.instance.platform - + def _asset_platform(self): platform_id = self.initial_data.get('platform') if isinstance(platform_id, dict): platform_id = platform_id.get('id') or platform_id.get('pk') - platform = Platform.objects.filter(id=platform_id).first() + + if not platform_id and self.instance: + platform = self.instance.platform + else: + platform = Platform.objects.filter(id=platform_id).first() + if not platform: raise serializers.ValidationError({'platform': _("Platform not exist")}) return platform def validate_domain(self, value): - platform = self._initial_data_platform + platform = self._asset_platform if platform.domain_enabled: return value else: @@ -263,6 +273,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali @staticmethod def accounts_create(accounts_data, asset): + if not accounts_data: + return for data in accounts_data: data['asset'] = asset AssetAccountSerializer().create(data) diff --git a/apps/assets/serializers/asset/host.py b/apps/assets/serializers/asset/host.py index 6a733c1e3..0f6bd8f5d 100644 --- a/apps/assets/serializers/asset/host.py +++ b/apps/assets/serializers/asset/host.py @@ -1,26 +1,25 @@ -from rest_framework import serializers from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers from assets.models import Host from .common import AssetSerializer - __all__ = ['HostInfoSerializer', 'HostSerializer'] class HostInfoSerializer(serializers.Serializer): vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor')) - model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model')) - sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number')) - cpu_model = serializers.ListField(child=serializers.CharField(max_length=64, allow_blank=True), required=False, label=_('CPU model')) - cpu_count = serializers.IntegerField(required=False, label=_('CPU count')) - cpu_cores = serializers.IntegerField(required=False, label=_('CPU cores')) - cpu_vcpus = serializers.IntegerField(required=False, label=_('CPU vcpus')) + model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model')) + sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number')) + cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model')) + cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count')) + cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores')) + cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus')) memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory')) disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total')) distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS')) - distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version')) + distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version')) arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch')) @@ -36,5 +35,3 @@ class HostSerializer(AssetSerializer): 'label': _("IP/Host") }, } - - diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 6992f4e8d..48bc0885f 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -29,7 +29,8 @@ class LabelSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): - queryset = queryset.annotate(asset_count=Count('assets')) + queryset = queryset.prefetch_related('assets') \ + .annotate(asset_count=Count('assets')) return queryset diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 1e4172dc8..15b4e7cca 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -1,6 +1,5 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from django.core import validators from assets.const.web import FillType from common.serializers import WritableNestedModelSerializer @@ -19,7 +18,7 @@ class ProtocolSettingSerializer(serializers.Serializer): ("nla", "NLA"), ] # RDP - console = serializers.BooleanField(required=False) + console = serializers.BooleanField(required=False, default=False) security = serializers.ChoiceField(choices=SECURITY_CHOICES, default="any") # SFTP diff --git a/apps/assets/tasks/automation.py b/apps/assets/tasks/automation.py index 02e946ede..20426451c 100644 --- a/apps/assets/tasks/automation.py +++ b/apps/assets/tasks/automation.py @@ -8,7 +8,7 @@ from orgs.utils import tmp_to_root_org, tmp_to_org logger = get_logger(__file__) -def task_activity_callback(self, pid, trigger, tp): +def task_activity_callback(self, pid, trigger, tp, *args, **kwargs): model = AutomationTypes.get_type_model(tp) with tmp_to_root_org(): instance = get_object_or_none(model, pk=pid) diff --git a/apps/assets/utils/k8s.py b/apps/assets/utils/k8s.py index 8cda3d62e..0ed17835f 100644 --- a/apps/assets/utils/k8s.py +++ b/apps/assets/utils/k8s.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- from urllib.parse import urlencode -from urllib3.exceptions import MaxRetryError, LocationParseError from kubernetes import client from kubernetes.client import api_client from kubernetes.client.api import core_v1_api -from kubernetes.client.exceptions import ApiException from common.utils import get_logger -from common.exceptions import JMSException from ..const import CloudTypes, Category logger = get_logger(__file__) @@ -20,7 +17,8 @@ class KubernetesClient: self.token = token self.proxy = proxy - def get_api(self): + @property + def api(self): configuration = client.Configuration() configuration.host = self.url configuration.proxy = self.proxy @@ -30,64 +28,29 @@ class KubernetesClient: api = core_v1_api.CoreV1Api(c) return api - def get_namespace_list(self): - api = self.get_api() - namespace_list = [] - for ns in api.list_namespace().items: - namespace_list.append(ns.metadata.name) - return namespace_list + def get_namespaces(self): + namespaces = [] + resp = self.api.list_namespace() + for ns in resp.items: + namespaces.append(ns.metadata.name) + return namespaces - def get_services(self): - api = self.get_api() - ret = api.list_service_for_all_namespaces(watch=False) - for i in ret.items: - print("%s \t%s \t%s \t%s \t%s \n" % ( - i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports)) + def get_pods(self, namespace): + pods = [] + resp = self.api.list_namespaced_pod(namespace) + for pd in resp.items: + pods.append(pd.metadata.name) + return pods - def get_pod_info(self, namespace, pod): - api = self.get_api() - resp = api.read_namespaced_pod(namespace=namespace, name=pod) - return resp + def get_containers(self, namespace, pod_name): + containers = [] + resp = self.api.read_namespaced_pod(pod_name, namespace) + for container in resp.spec.containers: + containers.append(container.name) + return containers - def get_pod_logs(self, namespace, pod): - api = self.get_api() - log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200) - return log_content - - def get_pods(self): - api = self.get_api() - try: - ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3)) - except LocationParseError as e: - logger.warning("Kubernetes API request url error: {}".format(e)) - raise JMSException(code='k8s_tree_error', detail=e) - except MaxRetryError: - msg = "Kubernetes API request timeout" - logger.warning(msg) - raise JMSException(code='k8s_tree_error', detail=msg) - except ApiException as e: - if e.status == 401: - msg = "Kubernetes API request unauthorized" - logger.warning(msg) - else: - msg = e - logger.warning(msg) - raise JMSException(code='k8s_tree_error', detail=msg) - data = {} - for i in ret.items: - namespace = i.metadata.namespace - pod_info = { - 'pod_name': i.metadata.name, - 'containers': [j.name for j in i.spec.containers] - } - if namespace in data: - data[namespace].append(pod_info) - else: - data[namespace] = [pod_info, ] - return data - - @classmethod - def get_proxy_url(cls, asset): + @staticmethod + def get_proxy_url(asset): if not asset.domain: return None @@ -97,11 +60,14 @@ class KubernetesClient: return f'{gateway.address}:{gateway.port}' @classmethod - def get_kubernetes_data(cls, asset, secret): + def run(cls, asset, secret, tp, *args): k8s_url = f'{asset.address}' proxy_url = cls.get_proxy_url(asset) k8s = cls(k8s_url, secret, proxy=proxy_url) - return k8s.get_pods() + func_name = f'get_{tp}s' + if hasattr(k8s, func_name): + return getattr(k8s, func_name)(*args) + return [] class KubernetesTree: @@ -117,17 +83,15 @@ class KubernetesTree: ) return node - def as_namespace_node(self, name, tp, counts=0): + def as_namespace_node(self, name, tp): i = urlencode({'namespace': name}) pid = str(self.asset.id) - name = f'{name}({counts})' node = self.create_tree_node(i, pid, name, tp, icon='cloud') return node - def as_pod_tree_node(self, namespace, name, tp, counts=0): + def as_pod_tree_node(self, namespace, name, tp): pid = urlencode({'namespace': namespace}) i = urlencode({'namespace': namespace, 'pod': name}) - name = f'{name}({counts})' node = self.create_tree_node(i, pid, name, tp, icon='cloud') return node @@ -162,30 +126,26 @@ class KubernetesTree: def async_tree_node(self, namespace, pod): tree = [] - data = KubernetesClient.get_kubernetes_data(self.asset, self.secret) - if not data: - return tree - if pod: - for container in next( - filter( - lambda x: x['pod_name'] == pod, data[namespace] - ) - )['containers']: + tp = 'container' + containers = KubernetesClient.run( + self.asset, self.secret, tp, namespace, pod + ) + for container in containers: container_node = self.as_container_tree_node( - namespace, pod, container, 'container' + namespace, pod, container, tp ) tree.append(container_node) elif namespace: - for pod in data[namespace]: - pod_nodes = self.as_pod_tree_node( - namespace, pod['pod_name'], 'pod', len(pod['containers']) - ) - tree.append(pod_nodes) + tp = 'pod' + pods = KubernetesClient.run(self.asset, self.secret, tp, namespace) + for pod in pods: + pod_node = self.as_pod_tree_node(namespace, pod, tp) + tree.append(pod_node) else: - for namespace, pods in data.items(): - namespace_node = self.as_namespace_node( - namespace, 'namespace', len(pods) - ) + tp = 'namespace' + namespaces = KubernetesClient.run(self.asset, self.secret, tp) + for namespace in namespaces: + namespace_node = self.as_namespace_node(namespace, tp) tree.append(namespace_node) return tree diff --git a/apps/audits/api.py b/apps/audits/api.py index 513796ed9..b672d8bad 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -10,6 +10,7 @@ from rest_framework.permissions import IsAuthenticated from common.drf.filters import DatetimeRangeFilter from common.plugins.es import QuerySet as ESQuerySet from common.utils import is_uuid +from common.utils import lazyproperty from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet from orgs.utils import current_org, tmp_to_root_org from orgs.models import Organization @@ -143,13 +144,19 @@ class OperateLogViewSet(OrgReadonlyModelViewSet): search_fields = ['resource', 'user'] ordering = ['-datetime'] + @lazyproperty + def is_action_detail(self): + return self.detail and self.request.query_params.get('type') == 'action_detail' + def get_serializer_class(self): - if self.request.query_params.get('type') == 'action_detail': + if self.is_action_detail: return OperateLogActionDetailSerializer return super().get_serializer_class() def get_queryset(self): - org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id) + org_q = Q(org_id=current_org.id) + if self.is_action_detail: + org_q |= Q(org_id=Organization.SYSTEM_ID) with tmp_to_root_org(): qs = OperateLog.objects.filter(org_q) es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG diff --git a/apps/audits/handler.py b/apps/audits/handler.py index d5f39dc7a..27b116488 100644 --- a/apps/audits/handler.py +++ b/apps/audits/handler.py @@ -4,7 +4,6 @@ from django.db import transaction from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ -from users.models import User from common.utils import get_request_ip, get_logger from common.utils.timezone import as_current_tz from common.utils.encode import Singleton diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index c6029e517..204d901e2 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -2,7 +2,7 @@ # from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers - +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from audits.backends.db import OperateLogStore from common.serializers.fields import LabeledChoiceField from common.utils import reverse, i18n_trans @@ -78,7 +78,7 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer): return data -class OperateLogSerializer(serializers.ModelSerializer): +class OperateLogSerializer(BulkOrgResourceModelSerializer): action = LabeledChoiceField(choices=ActionChoices.choices, label=_("Action")) resource = serializers.SerializerMethodField(label=_("Resource")) resource_type = serializers.SerializerMethodField(label=_('Resource Type')) diff --git a/apps/audits/utils.py b/apps/audits/utils.py index d069a6d26..27cdd6e28 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -1,13 +1,15 @@ import codecs import copy import csv + from itertools import chain +from datetime import datetime from django.db import models from django.http import HttpResponse +from common.utils.timezone import as_current_tz from common.utils import validate_ip, get_ip_city, get_logger -from settings.serializers import SettingsSerializer from .const import DEFAULT_CITY logger = get_logger(__name__) @@ -70,6 +72,8 @@ def _get_instance_field_value( f.verbose_name = 'id' elif isinstance(value, (list, dict)): value = 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): nested_data = _get_instance_field_value( value, include_model_fields, model_need_continue_fields, ('id',) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index c14898cee..dd225073e 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -24,7 +24,7 @@ from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil from terminal.models import EndpointRule -from ..models import ConnectionToken +from ..models import ConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer @@ -172,6 +172,7 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): get_object: callable get_serializer: callable perform_create: callable + validate_exchange_token: callable @action(methods=['POST', 'GET'], detail=True, url_path='rdp-file') def get_rdp_file(self, *args, **kwargs): @@ -204,6 +205,18 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): instance.expire() return Response(status=status.HTTP_204_NO_CONTENT) + @action(methods=['POST'], detail=False) + def exchange(self, request, *args, **kwargs): + pk = request.data.get('id', None) or request.data.get('pk', None) + # 只能兑换自己使用的 Token + instance = get_object_or_404(ConnectionToken, pk=pk, user=request.user) + instance.id = None + self.validate_exchange_token(instance) + instance.date_expired = date_expired_default() + instance.save() + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_201_CREATED) + class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet): filterset_fields = ( @@ -217,6 +230,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', 'create': 'authentication.add_connectiontoken', + 'exchange': 'authentication.add_connectiontoken', 'expire': 'authentication.change_connectiontoken', 'get_rdp_file': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken', @@ -240,10 +254,24 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView user = self.get_user(serializer) asset = data.get('asset') account_name = data.get('account') + _data = self._validate(user, asset, account_name) + data.update(_data) + return serializer + + def validate_exchange_token(self, token): + user = token.user + asset = token.asset + account_name = token.account + _data = self._validate(user, asset, account_name) + for k, v in _data.items(): + setattr(token, k, v) + return token + + def _validate(self, user, asset, account_name): + data = dict() data['org_id'] = asset.org_id data['user'] = user data['value'] = random_string(16) - account = self._validate_perm(user, asset, account_name) if account.has_secret: data['input_secret'] = '' @@ -257,8 +285,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView if ticket: data['from_ticket'] = ticket data['is_active'] = False - - return account + return data @staticmethod def _validate_perm(user, asset, account_name): diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 5355d79bd..14e5fd35f 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -225,6 +225,7 @@ class MFAMixin: 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() def clean_mfa_mark(self): keys = ['auth_mfa', 'auth_mfa_time', 'auth_mfa_required', 'auth_mfa_type'] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index fa9fd64c6..6a69a7e37 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -222,7 +222,8 @@ class ConnectionToken(JMSOrgBaseModel): 'secret_type': account.secret_type, 'secret': account.secret or self.input_secret, 'su_from': account.su_from, - 'org_id': account.org_id + 'org_id': account.org_id, + 'privileged': account.privileged } return Account(**data) diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 16e111bad..e9734b170 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -60,7 +60,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View): 'state': state, 'redirect_uri': redirect_uri, } - url = URL.AUTHEN + '?' + urlencode(params) + url = URL().authen + '?' + urlencode(params) return url @staticmethod diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 1a2e9e244..dfba7155c 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -6,6 +6,7 @@ import os import datetime from typing import Callable +from django.db import IntegrityError from django.templatetags.static import static from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse, HttpRequest @@ -229,6 +230,23 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView): ) as e: form.add_error('code', e.msg) return super().form_invalid(form) + except (IntegrityError,) as e: + # (1062, "Duplicate entry 'youtester001@example.com' for key 'users_user.email'") + error = str(e) + if len(e.args) < 2: + form.add_error(None, error) + return super().form_invalid(form) + + msg_list = e.args[1].split("'") + if len(msg_list) < 4: + form.add_error(None, error) + return super().form_invalid(form) + + email, field = msg_list[1], msg_list[3] + if field == 'users_user.email': + error = _('User email already exists ({})').format(email) + form.add_error(None, error) + return super().form_invalid(form) self.clear_rsa_key() return self.redirect_to_guard_view() diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py index fd8b80e32..c297a3261 100644 --- a/apps/authentication/views/mfa.py +++ b/apps/authentication/views/mfa.py @@ -32,11 +32,14 @@ class UserLoginMFAView(mixins.AuthMixin, FormView): return super().get(*args, **kwargs) def form_valid(self, form): + from users.utils import MFABlockUtils code = form.cleaned_data.get('code') mfa_type = form.cleaned_data.get('mfa_type') try: self._do_check_user_mfa(code, mfa_type) + user, ip = self.get_user_from_session(), self.get_request_ip() + MFABlockUtils(user.username, ip).clean_failed_count() return redirect_to_guard_view('mfa_ok') except (errors.MFAFailedError, errors.BlockMFAError) as e: form.add_error('code', e.msg) diff --git a/apps/common/__init__.py b/apps/common/__init__.py index fdb34b225..cfb1f1a7f 100644 --- a/apps/common/__init__.py +++ b/apps/common/__init__.py @@ -2,4 +2,3 @@ from __future__ import absolute_import # This will make sure the app is always imported when # Django starts so that shared_task will use this app. - diff --git a/apps/common/api/action.py b/apps/common/api/action.py index 76585f353..8d7829d9e 100644 --- a/apps/common/api/action.py +++ b/apps/common/api/action.py @@ -3,13 +3,12 @@ from typing import Callable from django.utils.translation import ugettext as _ -from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.request import Request +from rest_framework.response import Response from common.const.http import POST - __all__ = ['SuggestionMixin', 'RenderToJsonMixin'] diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index ac75ff645..7f3d4b055 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -1,11 +1,15 @@ import abc -import json import codecs -from rest_framework import serializers +import json +import re + from django.utils.translation import ugettext_lazy as _ -from rest_framework.parsers import BaseParser +from rest_framework import serializers from rest_framework import status from rest_framework.exceptions import ParseError, APIException +from rest_framework.parsers import BaseParser + +from common.serializers.fields import ObjectRelatedField from common.utils import get_logger logger = get_logger(__file__) @@ -18,11 +22,11 @@ class FileContentOverflowedError(APIException): class BaseFileParser(BaseParser): - FILE_CONTENT_MAX_LENGTH = 1024 * 1024 * 10 serializer_cls = None serializer_fields = None + obj_pattern = re.compile(r'^(.+)\(([a-z0-9-]+)\)$') def check_content_length(self, meta): content_length = int(meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))) @@ -74,7 +78,7 @@ class BaseFileParser(BaseParser): return s.translate(trans_table) @classmethod - def process_row(cls, row): + def load_row(cls, row): """ 构建json数据前的行处理 """ @@ -84,33 +88,63 @@ class BaseFileParser(BaseParser): col = cls._replace_chinese_quote(col) # 列表/字典转换 if isinstance(col, str) and ( - (col.startswith('[') and col.endswith(']')) - or + (col.startswith('[') and col.endswith(']')) or (col.startswith("{") and col.endswith("}")) ): - col = json.loads(col) + try: + col = json.loads(col) + except json.JSONDecodeError as e: + logger.error('Json load error: ', e) + logger.error('col: ', col) new_row.append(col) return new_row + def id_name_to_obj(self, v): + if not v or not isinstance(v, str): + return v + matched = self.obj_pattern.match(v) + if not matched: + return v + obj_name, obj_id = matched.groups() + if len(obj_id) < 36: + obj_id = int(obj_id) + return {'pk': obj_id, 'name': obj_name} + + def parse_value(self, field, value): + if value is '-': + return None + elif hasattr(field, 'to_file_internal_value'): + value = field.to_file_internal_value(value) + elif isinstance(field, serializers.BooleanField): + value = value.lower() in ['true', '1', 'yes'] + elif isinstance(field, serializers.ChoiceField): + value = value + elif isinstance(field, ObjectRelatedField): + if field.many: + value = [self.id_name_to_obj(v) for v in value] + else: + value = self.id_name_to_obj(value) + elif isinstance(field, serializers.ListSerializer): + value = [self.parse_value(field.child, v) for v in value] + elif isinstance(field, serializers.Serializer): + value = self.id_name_to_obj(value) + elif isinstance(field, serializers.ManyRelatedField): + value = [self.parse_value(field.child_relation, v) for v in value] + elif isinstance(field, serializers.ListField): + value = [self.parse_value(field.child, v) for v in value] + + return value + def process_row_data(self, row_data): """ 构建json数据后的行数据处理 """ - new_row_data = {} - serializer_fields = self.serializer_fields + new_row = {} for k, v in row_data.items(): - if type(v) in [list, dict, int, bool] or (isinstance(v, str) and k.strip() and v.strip()): - # 处理类似disk_info为字符串的'{}'的问题 - if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField): - v = str(v) - # 处理 BooleanField 的问题, 导出是 'True', 'False' - if isinstance(v, str) and v.strip().lower() == 'true': - v = True - elif isinstance(v, str) and v.strip().lower() == 'false': - v = False - - new_row_data[k] = v - return new_row_data + field = self.serializer_fields.get(k) + v = self.parse_value(field, v) + new_row[k] = v + return new_row def generate_data(self, fields_name, rows): data = [] @@ -118,7 +152,7 @@ class BaseFileParser(BaseParser): # 空行不处理 if not any(row): continue - row = self.process_row(row) + row = self.load_row(row) row_data = dict(zip(fields_name, row)) row_data = self.process_row_data(row_data) data.append(row_data) @@ -139,7 +173,6 @@ class BaseFileParser(BaseParser): raise ParseError('The resource does not support imports!') self.check_content_length(meta) - try: stream_data = self.get_stream_data(stream) rows = self.generate_rows(stream_data) @@ -148,6 +181,7 @@ class BaseFileParser(BaseParser): # 给 `common.mixins.api.RenderToJsonMixin` 提供,暂时只能耦合 column_title_field_pairs = list(zip(column_titles, field_names)) + column_title_field_pairs = [(k, v) for k, v in column_title_field_pairs if k and v] if not hasattr(request, 'jms_context'): request.jms_context = {} request.jms_context['column_title_field_pairs'] = column_title_field_pairs @@ -157,4 +191,3 @@ class BaseFileParser(BaseParser): except Exception as e: logger.error(e, exc_info=True) raise ParseError(_('Parse file error: {}').format(e)) - diff --git a/apps/common/drf/parsers/excel.py b/apps/common/drf/parsers/excel.py index c5007866c..111aa9526 100644 --- a/apps/common/drf/parsers/excel.py +++ b/apps/common/drf/parsers/excel.py @@ -1,13 +1,17 @@ import pyexcel +from django.utils.translation import gettext as _ + from .base import BaseFileParser class ExcelFileParser(BaseFileParser): - media_type = 'text/xlsx' def generate_rows(self, stream_data): - workbook = pyexcel.get_book(file_type='xlsx', file_content=stream_data) + try: + workbook = pyexcel.get_book(file_type='xlsx', file_content=stream_data) + except Exception: + raise Exception(_('Invalid excel file')) # 默认获取第一个工作表sheet sheet = workbook.sheet_by_index(0) rows = sheet.rows() diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py index 8e1373b9d..5b2aa5612 100644 --- a/apps/common/drf/renders/base.py +++ b/apps/common/drf/renders/base.py @@ -1,8 +1,11 @@ import abc from datetime import datetime + +from rest_framework import serializers from rest_framework.renderers import BaseRenderer from rest_framework.utils import encoders, json +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.utils import get_logger logger = get_logger(__file__) @@ -38,18 +41,27 @@ class BaseFileRenderer(BaseRenderer): def get_rendered_fields(self): fields = self.serializer.fields if self.template == 'import': - return [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id'] + fields = [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id'] elif self.template == 'update': - return [v for k, v in fields.items() if not v.read_only and k != "org_id"] + fields = [v for k, v in fields.items() if not v.read_only and k != "org_id"] else: - return [v for k, v in fields.items() if not v.write_only and k != "org_id"] + fields = [v for k, v in fields.items() if not v.write_only and k != "org_id"] + + meta = getattr(self.serializer, 'Meta', None) + if meta: + fields_unexport = getattr(meta, 'fields_unexport', []) + fields = [v for v in fields if v.field_name not in fields_unexport] + return fields @staticmethod def get_column_titles(render_fields): - return [ - '*{}'.format(field.label) if field.required else str(field.label) - for field in render_fields - ] + titles = [] + for field in render_fields: + name = field.label + if field.required: + name = '*' + name + titles.append(name) + return titles def process_data(self, data): results = data['results'] if 'results' in data else data @@ -59,7 +71,6 @@ class BaseFileRenderer(BaseRenderer): if self.template == 'import': results = [results[0]] if results else results - else: # 限制数据数量 results = results[:10000] @@ -68,17 +79,53 @@ class BaseFileRenderer(BaseRenderer): return results @staticmethod - def generate_rows(data, render_fields): + def to_id_name(value): + if value is None: + return '-' + pk = str(value.get('id', '') or value.get('pk', '')) + name = value.get('name') or value.get('display_name', '') + return '{}({})'.format(name, pk) + + @staticmethod + def to_choice_name(value): + if value is None: + return '-' + value = value.get('value', '') + return value + + def render_value(self, field, value): + if value is None: + value = '-' + elif hasattr(field, 'to_file_representation'): + value = field.to_file_representation(value) + elif isinstance(value, bool): + value = 'Yes' if value else 'No' + elif isinstance(field, LabeledChoiceField): + value = value.get('value', '') + elif isinstance(field, ObjectRelatedField): + if field.many: + value = [self.to_id_name(v) for v in value] + else: + value = self.to_id_name(value) + elif isinstance(field, serializers.ListSerializer): + value = [self.render_value(field.child, v) for v in value] + elif isinstance(field, serializers.Serializer) and value.get('id'): + value = self.to_id_name(value) + elif isinstance(field, serializers.ManyRelatedField): + value = [self.render_value(field.child_relation, v) for v in value] + elif isinstance(field, serializers.ListField): + value = [self.render_value(field.child, v) for v in value] + + if not isinstance(value, str): + value = json.dumps(value, cls=encoders.JSONEncoder, ensure_ascii=False) + return str(value) + + def generate_rows(self, data, render_fields): for item in data: row = [] for field in render_fields: value = item.get(field.field_name) - if value is None: - value = '' - elif isinstance(value, dict): - value = json.dumps(value, ensure_ascii=False) - else: - value = str(value) + value = self.render_value(field, value) row.append(value) yield row @@ -101,6 +148,9 @@ class BaseFileRenderer(BaseRenderer): def get_rendered_value(self): raise NotImplementedError + def after_render(self): + pass + def render(self, data, accepted_media_type=None, renderer_context=None): if data is None: return bytes() @@ -129,11 +179,10 @@ class BaseFileRenderer(BaseRenderer): self.initial_writer() self.write_column_titles(column_titles) self.write_rows(rows) + self.after_render() value = self.get_rendered_value() except Exception as e: logger.debug(e, exc_info=True) value = 'Render error! ({})'.format(self.media_type).encode('utf-8') return value - return value - diff --git a/apps/common/drf/renders/excel.py b/apps/common/drf/renders/excel.py index e4cd9f0ce..1b3f8dacf 100644 --- a/apps/common/drf/renders/excel.py +++ b/apps/common/drf/renders/excel.py @@ -1,6 +1,6 @@ from openpyxl import Workbook -from openpyxl.writer.excel import save_virtual_workbook from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE +from openpyxl.writer.excel import save_virtual_workbook from .base import BaseFileRenderer @@ -19,12 +19,26 @@ class ExcelFileRenderer(BaseFileRenderer): def write_row(self, row): self.row_count += 1 + self.ws.row_dimensions[self.row_count].height = 20 column_count = 0 for cell_value in row: # 处理非法字符 column_count += 1 - cell_value = ILLEGAL_CHARACTERS_RE.sub(r'', cell_value) - self.ws.cell(row=self.row_count, column=column_count, value=cell_value) + cell_value = ILLEGAL_CHARACTERS_RE.sub(r'', str(cell_value)) + self.ws.cell(row=self.row_count, column=column_count, value=str(cell_value)) + + def after_render(self): + for col in self.ws.columns: + max_length = 0 + column = col[0].column_letter + for cell in col: + if len(str(cell.value)) > max_length: + max_length = len(cell.value) + adjusted_width = (max_length + 2) * 1.0 + adjusted_width = 300 if adjusted_width > 300 else adjusted_width + adjusted_width = 30 if adjusted_width < 30 else adjusted_width + self.ws.column_dimensions[column].width = adjusted_width + self.wb.save('/tmp/test.xlsx') def get_rendered_value(self): value = save_virtual_workbook(self.wb) diff --git a/apps/common/local.py b/apps/common/local.py index 947ae3d6b..41b0cffe3 100644 --- a/apps/common/local.py +++ b/apps/common/local.py @@ -1,7 +1,10 @@ from werkzeug.local import Local +from django.utils import translation + + thread_local = Local() -encrypted_field_set = set() +encrypted_field_set = {'password', 'secret'} def _find(attr): @@ -10,4 +13,5 @@ def _find(attr): def add_encrypted_field_set(label): if label: - encrypted_field_set.add(str(label)) + with translation.override('en'): + encrypted_field_set.add(str(label)) diff --git a/apps/common/plugins/es.py b/apps/common/plugins/es.py index 7419e2d1a..a5e027c90 100644 --- a/apps/common/plugins/es.py +++ b/apps/common/plugins/es.py @@ -114,26 +114,28 @@ class ES(object): self._ensure_index_exists() def _ensure_index_exists(self): - info = self.es.info() - version = info['version']['number'].split('.')[0] - if version == '6': - mappings = {'mappings': {'data': {'properties': self.properties}}} - else: - mappings = {'mappings': {'properties': self.properties}} - - if self.is_index_by_date: - mappings['aliases'] = { - self.query_index: {} - } - try: - self.es.indices.create(self.index, body=mappings) - return - except RequestError as e: - if e.error == 'resource_already_exists_exception': - logger.warning(e) + info = self.es.info() + version = info['version']['number'].split('.')[0] + if version == '6': + mappings = {'mappings': {'data': {'properties': self.properties}}} else: - logger.exception(e) + mappings = {'mappings': {'properties': self.properties}} + + if self.is_index_by_date: + mappings['aliases'] = { + self.query_index: {} + } + + try: + self.es.indices.create(self.index, body=mappings) + except RequestError as e: + if e.error == 'resource_already_exists_exception': + logger.warning(e) + else: + logger.exception(e) + except Exception as e: + logger.error(e, exc_info=True) def make_data(self, data): return [] diff --git a/apps/common/sdk/im/feishu/__init__.py b/apps/common/sdk/im/feishu/__init__.py index 1cfee9cdd..cb01b66da 100644 --- a/apps/common/sdk/im/feishu/__init__.py +++ b/apps/common/sdk/im/feishu/__init__.py @@ -3,6 +3,7 @@ import json from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException +from django.conf import settings from common.utils.common import get_logger from common.sdk.im.utils import digest from common.sdk.im.mixin import RequestMixin, BaseRequest @@ -11,14 +12,30 @@ logger = get_logger(__name__) class URL: - AUTHEN = 'https://open.feishu.cn/open-apis/authen/v1/index' - - GET_TOKEN = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/' - # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN - GET_USER_INFO_BY_CODE = 'https://open.feishu.cn/open-apis/authen/v1/access_token' + @property + def host(self): + if settings.FEISHU_VERSION == 'feishu': + h = 'https://open.feishu.cn' + else: + h = 'https://open.larksuite.com' + return h - SEND_MESSAGE = 'https://open.feishu.cn/open-apis/im/v1/messages' + @property + def authen(self): + return f'{self.host}/open-apis/authen/v1/index' + + @property + def get_token(self): + return f'{self.host}/open-apis/auth/v3/tenant_access_token/internal/' + + @property + def get_user_info_by_code(self): + return f'{self.host}/open-apis/authen/v1/access_token' + + @property + def send_message(self): + return f'{self.host}/open-apis/im/v1/messages' class ErrorCode: @@ -51,7 +68,7 @@ class FeishuRequests(BaseRequest): def request_access_token(self): data = {'app_id': self._app_id, 'app_secret': self._app_secret} - response = self.raw_request('post', url=URL.GET_TOKEN, data=data) + response = self.raw_request('post', url=URL().get_token, data=data) self.check_errcode_is_0(response) access_token = response['tenant_access_token'] @@ -86,7 +103,7 @@ class FeiShu(RequestMixin): 'code': code } - data = self._requests.post(URL.GET_USER_INFO_BY_CODE, json=body, check_errcode_is_0=False) + 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'] @@ -107,7 +124,7 @@ class FeiShu(RequestMixin): try: logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}') - self._requests.post(URL.SEND_MESSAGE, params=params, json=body) + self._requests.post(URL().send_message, params=params, json=body) except APIException as e: # 只处理可预知的错误 logger.exception(e) diff --git a/apps/common/serializers/mixin.py b/apps/common/serializers/mixin.py index 7f4678557..4d3e6ddc8 100644 --- a/apps/common/serializers/mixin.py +++ b/apps/common/serializers/mixin.py @@ -55,9 +55,11 @@ class BulkSerializerMixin(object): # add update_lookup_field field back to validated data # since super by default strips out read-only fields # hence id will no longer be present in validated_data - if all((isinstance(self.root, BulkListSerializer), - id_attr, - request_method in ('PUT', 'PATCH'))): + if all([ + isinstance(self.root, BulkListSerializer), + id_attr, + request_method in ('PUT', 'PATCH') + ]): id_field = self.fields.get("id") or self.fields.get('pk') if data.get("id"): id_value = id_field.to_internal_value(data.get("id")) @@ -135,7 +137,7 @@ class BulkListSerializerMixin: pk = item["pk"] else: raise ValidationError("id or pk not in data") - child = self.instance.get(id=pk) + child = self.instance.get(pk=pk) self.child.instance = child self.child.initial_data = item # raw diff --git a/apps/common/signal_handlers.py b/apps/common/signal_handlers.py index d03c76798..6beb2d187 100644 --- a/apps/common/signal_handlers.py +++ b/apps/common/signal_handlers.py @@ -32,7 +32,7 @@ class Counter: return self.counter == other.counter -def on_request_finished_logging_db_query(sender, **kwargs): +def digest_sql_query(): queries = connection.queries counters = defaultdict(Counter) table_queries = defaultdict(list) @@ -79,6 +79,9 @@ def on_request_finished_logging_db_query(sender, **kwargs): counter.counter, counter.time, name) ) + +def on_request_finished_logging_db_query(sender, **kwargs): + digest_sql_query() on_request_finished_release_local(sender, **kwargs) diff --git a/apps/common/tasks.py b/apps/common/tasks.py index b4c3d3488..ee4880ee7 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -10,7 +10,7 @@ from .utils import get_logger logger = get_logger(__file__) -def task_activity_callback(self, subject, message, recipient_list, **kwargs): +def task_activity_callback(self, subject, message, recipient_list, *args, **kwargs): from users.models import User email_list = recipient_list resource_ids = list(User.objects.filter(email__in=email_list).values_list('id', flat=True)) diff --git a/apps/common/utils/connection.py b/apps/common/utils/connection.py index 95e827299..cbe961968 100644 --- a/apps/common/utils/connection.py +++ b/apps/common/utils/connection.py @@ -108,7 +108,7 @@ class Subscription: try: self.sub.close() except Exception as e: - logger.error('Unsubscribe msg error: {}'.format(e)) + logger.debug('Unsubscribe msg error: {}'.format(e)) def retry(self, _next, error, complete): logger.info('Retry subscribe channel: {}'.format(self.ch)) diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index 36cd4f224..5a48261da 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -98,7 +98,7 @@ def ssh_private_key_gen(private_key, password=None): def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): private_key = ssh_private_key_gen(private_key, password=password) - if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)): + if not isinstance(private_key, _supported_paramiko_ssh_key_types): raise IOError('Invalid private key') public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % { diff --git a/apps/common/utils/translate.py b/apps/common/utils/translate.py index 2a9a74ca8..ec465502b 100644 --- a/apps/common/utils/translate.py +++ b/apps/common/utils/translate.py @@ -35,7 +35,10 @@ def i18n_trans(s): tpl, args = s.split(' % ', 1) args = args.split(', ') args = [gettext(arg) for arg in args] - return gettext(tpl) % tuple(args) + try: + return gettext(tpl) % tuple(args) + except TypeError: + return gettext(tpl) def hello(): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b192cd993..343224675 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -214,7 +214,7 @@ class Config(dict): 'REDIS_DB_WS': 6, 'GLOBAL_ORG_DISPLAY_NAME': '', - 'SITE_URL': 'http://localhost:8080', + 'SITE_URL': 'http://127.0.0.1', 'USER_GUIDE_URL': '', 'ANNOUNCEMENT_ENABLED': True, 'ANNOUNCEMENT': {}, @@ -376,6 +376,7 @@ class Config(dict): 'AUTH_FEISHU': False, 'FEISHU_APP_ID': '', 'FEISHU_APP_SECRET': '', + 'FEISHU_VERSION': 'feishu', 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2 'LOGIN_REDIRECT_MSG_ENABLED': True, diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 4d5f9a4cf..9966228dc 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -20,7 +20,7 @@ default_context = { 'LOGIN_WECOM_logo_logout': static('img/login_wecom_logo.png'), 'LOGIN_DINGTALK_logo_logout': static('img/login_dingtalk_logo.png'), 'LOGIN_FEISHU_logo_logout': static('img/login_feishu_logo.png'), - 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2022', + 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2023', 'INTERFACE': default_interface, } diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index f1fec05cd..2eab9ebd8 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -137,6 +137,7 @@ DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET AUTH_FEISHU = CONFIG.AUTH_FEISHU FEISHU_APP_ID = CONFIG.FEISHU_APP_ID FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET +FEISHU_VERSION = CONFIG.FEISHU_VERSION # Saml2 auth AUTH_SAML2 = CONFIG.AUTH_SAML2 diff --git a/apps/jumpserver/utils.py b/apps/jumpserver/utils.py index 7e29b17fd..24f2b10d7 100644 --- a/apps/jumpserver/utils.py +++ b/apps/jumpserver/utils.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # -from functools import partial -from werkzeug.local import LocalProxy from datetime import datetime +from functools import partial from django.conf import settings +from werkzeug.local import LocalProxy + from common.local import thread_local @@ -34,7 +35,7 @@ def get_xpack_license_info() -> dict: corporation = info.get('corporation', '') else: current_year = datetime.now().year - corporation = f'Copyright - FIT2CLOUD 飞致云 © 2014-{current_year}' + corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}' info = { 'corporation': corporation } diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index cfe8bd469..472b329b3 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:8c2600b7094db2a9e64862169ff1c826d5064fae9b9e71744545a1cea88cbc65 -size 136280 +oid sha256:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72 +size 137935 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index c5cda4241..d4b4ecc20 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-02-23 19:11+0800\n" +"POT-Creation-Date: 2023-03-14 17:34+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -169,7 +169,7 @@ msgstr "作成のみ" #: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19 #: assets/serializers/label.py:27 audits/models.py:48 #: authentication/models/connection_token.py:33 -#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:35 #: terminal/backends/command/models.py:20 terminal/models/session/session.py:32 #: terminal/notifications.py:95 terminal/serializers/command.py:17 #: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212 @@ -182,7 +182,7 @@ msgid "Su from" msgstr "から切り替え" #: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:29 +#: settings/serializers/auth/feishu.py:20 terminal/models/applet/applet.py:29 msgid "Version" msgstr "バージョン" @@ -195,9 +195,9 @@ msgstr "ソース" #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 #: acls/models/base.py:102 acls/serializers/base.py:57 -#: assets/serializers/asset/common.py:124 assets/serializers/gateway.py:28 +#: assets/serializers/asset/common.py:131 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:32 +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40 #: terminal/backends/command/models.py:21 terminal/models/session/session.py:34 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 msgid "Account" @@ -236,7 +236,7 @@ msgid "Can change asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを変更できます" #: accounts/models/automations/backup_account.py:27 -#: accounts/models/automations/change_secret.py:47 +#: accounts/models/automations/change_secret.py:65 #: accounts/serializers/account/backup.py:34 #: accounts/serializers/automations/change_secret.py:57 msgid "Recipient" @@ -327,7 +327,7 @@ msgstr "プッシュ アカウントの実行を表示する" msgid "Can add push account execution" msgstr "プッシュ アカウントの作成の実行" -#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 #: accounts/serializers/account/account.py:134 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 @@ -336,52 +336,52 @@ msgstr "プッシュ アカウントの作成の実行" msgid "Secret type" msgstr "鍵の種類" -#: accounts/models/automations/change_secret.py:19 -#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/models/automations/change_secret.py:20 +#: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 #: settings/serializers/auth/radius.py:19 msgid "Secret" msgstr "ひみつ" -#: accounts/models/automations/change_secret.py:22 +#: accounts/models/automations/change_secret.py:23 #: accounts/serializers/automations/change_secret.py:40 msgid "Secret strategy" msgstr "鍵ポリシー" -#: accounts/models/automations/change_secret.py:24 +#: accounts/models/automations/change_secret.py:25 msgid "Password rules" msgstr "パスワードルール" -#: accounts/models/automations/change_secret.py:27 +#: accounts/models/automations/change_secret.py:28 msgid "SSH key change strategy" msgstr "SSHキープッシュ方式" -#: accounts/models/automations/change_secret.py:54 +#: accounts/models/automations/change_secret.py:72 msgid "Change secret automation" msgstr "自動暗号化" -#: accounts/models/automations/change_secret.py:71 +#: accounts/models/automations/change_secret.py:89 msgid "Old secret" msgstr "以前のパスワード" -#: accounts/models/automations/change_secret.py:73 +#: accounts/models/automations/change_secret.py:91 msgid "Date started" msgstr "開始日" -#: accounts/models/automations/change_secret.py:74 +#: accounts/models/automations/change_secret.py:92 #: assets/models/automations/base.py:115 ops/models/base.py:56 #: ops/models/celery.py:64 ops/models/job.py:192 #: terminal/models/applet/host.py:110 msgid "Date finished" msgstr "終了日" -#: accounts/models/automations/change_secret.py:76 assets/const/automation.py:8 +#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8 #: common/const/choices.py:20 msgid "Error" msgstr "間違い" -#: accounts/models/automations/change_secret.py:80 +#: accounts/models/automations/change_secret.py:98 msgid "Change secret record" msgstr "パスワード レコードの変更" @@ -394,7 +394,7 @@ msgid "Date last login" msgstr "最終ログイン日" #: accounts/models/automations/gather_account.py:15 -#: accounts/models/automations/push_account.py:13 accounts/models/base.py:34 +#: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 #: acls/serializers/base.py:18 acls/serializers/base.py:49 #: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25 #: authentication/forms.py:27 authentication/models/temp_token.py:9 @@ -419,11 +419,11 @@ msgstr "自動収集アカウント" msgid "Gather asset accounts" msgstr "アカウントのコレクション" -#: accounts/models/automations/push_account.py:12 +#: accounts/models/automations/push_account.py:14 msgid "Triggers" msgstr "トリガー方式" -#: accounts/models/automations/push_account.py:14 acls/models/base.py:81 +#: accounts/models/automations/push_account.py:16 acls/models/base.py:81 #: acls/serializers/base.py:81 acls/serializers/login_acl.py:25 #: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:109 @@ -431,7 +431,7 @@ msgstr "トリガー方式" msgid "Action" msgstr "アクション" -#: accounts/models/automations/push_account.py:40 +#: accounts/models/automations/push_account.py:59 msgid "Push asset account" msgstr "アカウントプッシュ" @@ -446,8 +446,8 @@ msgstr "アカウントの確認" #: 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:21 assets/models/platform.py:76 -#: assets/serializers/asset/common.py:67 assets/serializers/asset/common.py:143 -#: assets/serializers/platform.py:91 assets/serializers/platform.py:136 +#: assets/serializers/asset/common.py:74 assets/serializers/asset/common.py:151 +#: assets/serializers/platform.py:133 #: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:91 ops/models/playbook.py:23 ops/serializers/job.py:19 @@ -470,7 +470,7 @@ msgstr "特権アカウント" #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:107 -#: terminal/models/applet/applet.py:32 users/serializers/user.py:161 +#: terminal/models/applet/applet.py:32 users/serializers/user.py:162 msgid "Is active" msgstr "アクティブです。" @@ -516,24 +516,24 @@ msgstr "" "情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" #: accounts/serializers/account/account.py:65 -#: assets/serializers/asset/common.py:65 settings/serializers/auth/sms.py:75 +#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75 msgid "Template" msgstr "テンプレート" #: accounts/serializers/account/account.py:68 -#: assets/serializers/asset/common.py:62 +#: assets/serializers/asset/common.py:69 msgid "Push now" msgstr "今すぐプッシュ" #: accounts/serializers/account/account.py:70 -#: accounts/serializers/account/base.py:62 +#: accounts/serializers/account/base.py:64 msgid "Has secret" msgstr "エスクローされたパスワード" #: accounts/serializers/account/account.py:75 applications/models.py:11 #: assets/models/label.py:21 assets/models/platform.py:77 -#: assets/serializers/asset/common.py:120 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:97 assets/serializers/platform.py:137 +#: assets/serializers/asset/common.py:127 assets/serializers/cagegory.py:8 +#: assets/serializers/platform.py:94 assets/serializers/platform.py:134 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" @@ -544,7 +544,7 @@ msgstr "カテゴリ" #: 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:78 -#: assets/serializers/asset/common.py:121 assets/serializers/platform.py:96 +#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93 #: audits/serializers.py:48 #: authentication/serializers/connect_token_secret.py:116 ops/models/job.py:102 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31 @@ -591,8 +591,8 @@ msgstr "キー/パスワード" msgid "Key password" msgstr "キーパスワード" -#: accounts/serializers/account/base.py:79 -#: assets/serializers/asset/common.py:291 +#: accounts/serializers/account/base.py:81 +#: assets/serializers/asset/common.py:301 msgid "Spec info" msgstr "特別情報" @@ -741,7 +741,7 @@ msgstr "アクティブ" #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:23 rbac/builtin.py:118 +#: perms/serializers/permission.py:31 rbac/builtin.py:122 #: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19 #: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 #: terminal/notifications.py:96 terminal/notifications.py:144 @@ -828,7 +828,7 @@ msgstr "" "192.168.10.1、192.168.1.0/24、10.1.1.1-10.1.1.20、2001:db8:2de::e13、2001:" "db8:1a:1110:::/64 (ドメイン名サポート)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:36 +#: acls/serializers/base.py:40 assets/serializers/asset/host.py:35 msgid "IP/Host" msgstr "IP/ホスト" @@ -908,7 +908,7 @@ msgstr "アプリケーション" msgid "Can match application" msgstr "アプリケーションを一致させることができます" -#: assets/api/asset/asset.py:142 +#: assets/api/asset/asset.py:143 msgid "Cannot create asset directly, you should create a host or other" msgstr "" "資産を直接作成することはできません。ホストまたはその他を作成する必要がありま" @@ -934,7 +934,7 @@ msgstr "削除に失敗し、ノードにアセットが含まれています。 msgid "App assets" msgstr "アプリ資産" -#: assets/automations/base/manager.py:114 +#: assets/automations/base/manager.py:113 msgid "{} disabled" msgstr "{} 無効" @@ -944,10 +944,8 @@ msgid "No account" msgstr "アカウントなし" #: assets/automations/ping_gateway/manager.py:36 -#, fuzzy -#| msgid "Assets amount" msgid "Asset, {}, using account {}" -msgstr "資産額" +msgstr "資産, {}, アカウントを使用 {}" #: assets/automations/ping_gateway/manager.py:55 #, python-brace-format @@ -998,7 +996,7 @@ msgid "Device" msgstr "インターネット機器" #: assets/const/category.py:13 assets/models/asset/database.py:9 -#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:108 +#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:115 msgid "Database" msgstr "データベース" @@ -1011,6 +1009,18 @@ msgstr "クラウド サービス" msgid "Web" msgstr "Web" +#: assets/const/cloud.py:7 +msgid "Public cloud" +msgstr "パブリック クラウド" + +#: assets/const/cloud.py:8 +msgid "Private cloud" +msgstr "私有雲" + +#: assets/const/cloud.py:9 +msgid "Kubernetes" +msgstr "" + #: assets/const/device.py:7 terminal/models/applet/applet.py:24 #: tickets/const.py:8 msgid "General" @@ -1028,6 +1038,10 @@ msgstr "ルーター" msgid "Firewall" msgstr "ファイアウォール" +#: assets/const/host.py:12 rbac/tree.py:28 +msgid "Other" +msgstr "その他" + #: assets/const/types.py:200 msgid "All types" msgstr "いろんなタイプ" @@ -1046,7 +1060,7 @@ msgid "Basic" msgstr "基本" #: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/asset/common.py:116 assets/serializers/platform.py:40 +#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40 msgid "Script" msgstr "脚本" @@ -1175,7 +1189,7 @@ msgstr "クラウド サービス" msgid "Port" msgstr "ポート" -#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:144 +#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152 msgid "Address" msgstr "アドレス" @@ -1212,7 +1226,7 @@ msgstr "アセットを一致させることができます" msgid "Can change asset nodes" msgstr "資産ノードを変更できます" -#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:109 +#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116 #: settings/serializers/email.py:37 msgid "Use SSL" msgstr "SSLの使用" @@ -1229,7 +1243,7 @@ msgstr "クライアント証明書" msgid "Client key" msgstr "クライアントキー" -#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:110 +#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117 msgid "Allow invalid cert" msgstr "証明書チェックを無視" @@ -1237,23 +1251,23 @@ msgstr "証明書チェックを無視" msgid "Autofill" msgstr "自動充填" -#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:113 +#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120 #: assets/serializers/platform.py:32 msgid "Username selector" msgstr "ユーザー名ピッカー" -#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:114 +#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121 #: assets/serializers/platform.py:35 msgid "Password selector" msgstr "パスワードセレクター" -#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:115 +#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122 #: assets/serializers/platform.py:38 msgid "Submit selector" msgstr "ボタンセレクターを確認する" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:290 rbac/tree.py:35 +#: assets/serializers/asset/common.py:300 rbac/tree.py:35 msgid "Accounts" msgstr "アカウント" @@ -1269,7 +1283,7 @@ msgstr "アセットの自動化タスク" #: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:183 #: terminal/models/applet/applet.py:157 terminal/models/applet/host.py:108 #: terminal/models/component/status.py:27 terminal/serializers/applet.py:18 -#: terminal/serializers/applet_host.py:93 tickets/models/ticket/general.py:283 +#: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 #: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164 #: xpack/plugins/cloud/models.py:216 @@ -1293,7 +1307,7 @@ msgid "Date verified" msgstr "確認済みの日付" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 -#: perms/serializers/permission.py:25 users/models/group.py:25 +#: perms/serializers/permission.py:33 users/models/group.py:25 #: users/models/user.py:723 msgid "User group" msgstr "ユーザーグループ" @@ -1355,7 +1369,7 @@ msgstr "システム" msgid "Value" msgstr "値" -#: assets/models/label.py:40 assets/serializers/asset/common.py:122 +#: assets/models/label.py:40 assets/serializers/asset/common.py:129 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:114 #: common/serializers/common.py:79 settings/serializers/sms.py:7 @@ -1382,7 +1396,7 @@ msgstr "フルバリュー" msgid "Parent key" msgstr "親キー" -#: assets/models/node.py:558 perms/serializers/permission.py:28 +#: assets/models/node.py:558 perms/serializers/permission.py:36 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "ノード" @@ -1459,23 +1473,23 @@ msgstr "メタ" msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:83 assets/serializers/platform.py:94 +#: assets/models/platform.py:83 assets/serializers/platform.py:91 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:85 assets/serializers/platform.py:122 +#: assets/models/platform.py:85 assets/serializers/platform.py:119 msgid "Domain enabled" msgstr "ドメインを有効にする" -#: assets/models/platform.py:87 assets/serializers/platform.py:121 +#: assets/models/platform.py:87 assets/serializers/platform.py:118 msgid "Su enabled" msgstr "アカウントの切り替えを有効にする" -#: assets/models/platform.py:88 assets/serializers/platform.py:104 +#: assets/models/platform.py:88 assets/serializers/platform.py:101 msgid "Su method" msgstr "アカウントの切り替え方法" -#: assets/models/platform.py:90 assets/serializers/platform.py:101 +#: assets/models/platform.py:90 assets/serializers/platform.py:98 msgid "Automation" msgstr "オートメーション" @@ -1484,35 +1498,36 @@ msgstr "オートメーション" msgid "%(value)s is not an even number" msgstr "%(value)s は偶数ではありません" -#: assets/serializers/asset/common.py:112 +#: assets/serializers/asset/common.py:119 msgid "Auto fill" msgstr "自動充填" -#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:99 +#: assets/serializers/asset/common.py:130 assets/serializers/platform.py:96 #: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:66 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 msgid "Protocols" msgstr "プロトコル" -#: assets/serializers/asset/common.py:142 -#: assets/serializers/asset/common.py:292 -msgid "Auto info" -msgstr "自動情報" - -#: assets/serializers/asset/common.py:145 +#: assets/serializers/asset/common.py:132 +#: assets/serializers/asset/common.py:153 msgid "Node path" msgstr "ノードパスです" -#: assets/serializers/asset/common.py:218 +#: assets/serializers/asset/common.py:150 +#: assets/serializers/asset/common.py:302 +msgid "Auto info" +msgstr "自動情報" + +#: assets/serializers/asset/common.py:226 msgid "Platform not exist" msgstr "プラットフォームが存在しません" -#: assets/serializers/asset/common.py:253 +#: assets/serializers/asset/common.py:261 msgid "port out of range (1-65535)" msgstr "ポート番号が範囲外です (1-65535)" -#: assets/serializers/asset/common.py:260 +#: assets/serializers/asset/common.py:268 msgid "Protocol is required: {}" msgstr "プロトコルが必要です: {}" @@ -1524,56 +1539,56 @@ msgstr "プロトコルが必要です: {}" msgid "This field is required." msgstr "このフィールドは必須です。" -#: assets/serializers/asset/host.py:12 +#: assets/serializers/asset/host.py:11 msgid "Vendor" msgstr "ベンダー" -#: assets/serializers/asset/host.py:13 +#: assets/serializers/asset/host.py:12 msgid "Model" msgstr "モデル" -#: assets/serializers/asset/host.py:14 tickets/models/ticket/general.py:299 +#: assets/serializers/asset/host.py:13 tickets/models/ticket/general.py:299 msgid "Serial number" msgstr "シリアル番号" -#: assets/serializers/asset/host.py:15 +#: assets/serializers/asset/host.py:14 msgid "CPU model" msgstr "CPU モデル" -#: assets/serializers/asset/host.py:16 +#: assets/serializers/asset/host.py:15 msgid "CPU count" msgstr "CPU カウント" -#: assets/serializers/asset/host.py:17 +#: assets/serializers/asset/host.py:16 msgid "CPU cores" msgstr "CPU カラー" -#: assets/serializers/asset/host.py:18 +#: assets/serializers/asset/host.py:17 msgid "CPU vcpus" msgstr "CPU 合計" -#: assets/serializers/asset/host.py:19 +#: assets/serializers/asset/host.py:18 msgid "Memory" msgstr "メモリ" -#: assets/serializers/asset/host.py:20 +#: assets/serializers/asset/host.py:19 msgid "Disk total" msgstr "ディスクの合計" -#: assets/serializers/asset/host.py:22 +#: assets/serializers/asset/host.py:21 #: authentication/serializers/connect_token_secret.py:105 msgid "OS" msgstr "OS" -#: assets/serializers/asset/host.py:23 +#: assets/serializers/asset/host.py:22 msgid "OS version" msgstr "システムバージョン" -#: assets/serializers/asset/host.py:24 +#: assets/serializers/asset/host.py:23 msgid "OS arch" msgstr "システムアーキテクチャ" -#: assets/serializers/asset/host.py:28 +#: assets/serializers/asset/host.py:27 msgid "Info" msgstr "情報" @@ -1629,7 +1644,7 @@ msgstr "アカウントの収集方法" msgid "Primary" msgstr "主要" -#: assets/serializers/platform.py:123 +#: assets/serializers/platform.py:120 msgid "Default Domain" msgstr "デフォルト ドメイン" @@ -1799,11 +1814,11 @@ msgstr "タスク" msgid "-" msgstr "-" -#: audits/handler.py:116 +#: audits/handler.py:115 msgid "Yes" msgstr "是" -#: audits/handler.py:116 +#: audits/handler.py:115 msgid "No" msgstr "否" @@ -1940,20 +1955,21 @@ msgid "Auth Token" msgstr "認証トークン" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:177 +#: authentication/views/login.py:74 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:778 msgid "WeCom" msgstr "企業微信" #: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:144 -#: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: settings/serializers/auth/feishu.py:10 users/models/user.py:780 +#: 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 msgid "FeiShu" msgstr "本を飛ばす" #: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:79 notifications/backends/__init__.py:12 +#: authentication/views/login.py:80 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:779 msgid "DingTalk" msgstr "DingTalk" @@ -1971,19 +1987,19 @@ msgstr "監査セッション タスク ログのクリーンアップ" msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" -#: authentication/api/connection_token.py:268 +#: authentication/api/connection_token.py:295 msgid "Account not found" msgstr "アカウントが見つかりません" -#: authentication/api/connection_token.py:271 +#: authentication/api/connection_token.py:298 msgid "Permission expired" msgstr "承認の有効期限が切れています" -#: authentication/api/connection_token.py:283 +#: authentication/api/connection_token.py:310 msgid "ACL action is reject" msgstr "ACL アクションは拒否です" -#: authentication/api/connection_token.py:287 +#: authentication/api/connection_token.py:314 msgid "ACL action is review" msgstr "ACL アクションはレビューです" @@ -2237,15 +2253,15 @@ msgstr "本を飛ばすは拘束されていません" msgid "Your password is invalid" msgstr "パスワードが無効です" -#: authentication/errors/redirect.py:85 authentication/mixins.py:306 +#: authentication/errors/redirect.py:85 authentication/mixins.py:307 msgid "Your password is too simple, please change it for security" msgstr "パスワードがシンプルすぎるので、セキュリティのために変更してください" -#: authentication/errors/redirect.py:93 authentication/mixins.py:313 +#: authentication/errors/redirect.py:93 authentication/mixins.py:314 msgid "You should to change your password before login" msgstr "ログインする前にパスワードを変更する必要があります" -#: authentication/errors/redirect.py:101 authentication/mixins.py:320 +#: authentication/errors/redirect.py:101 authentication/mixins.py:321 msgid "Your password has expired, please reset before logging in" msgstr "" "パスワードの有効期限が切れました。ログインする前にリセットしてください。" @@ -2348,11 +2364,11 @@ msgstr "無効にする電話番号をクリアする" msgid "Authentication failed (before login check failed): {}" msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" -#: authentication/mixins.py:256 +#: authentication/mixins.py:257 msgid "The MFA type ({}) is not enabled" msgstr "MFAタイプ ({}) が有効になっていない" -#: authentication/mixins.py:296 +#: authentication/mixins.py:297 msgid "Please change your password" msgstr "パスワードを変更してください" @@ -2419,7 +2435,7 @@ msgstr "ユーザーなしまたは期限切れのユーザー" msgid "No asset or inactive asset" msgstr "アセットがないか、有効化されていないアセット" -#: authentication/models/connection_token.py:257 +#: authentication/models/connection_token.py:258 msgid "Super connection token" msgstr "スーパー接続トークン" @@ -2478,16 +2494,16 @@ msgid "Ticket info" msgstr "作業指示情報" #: authentication/serializers/connection_token.py:20 -#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:60 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37 +#: perms/serializers/permission.py:70 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "アクション" #: authentication/serializers/connection_token.py:41 -#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 -#: users/serializers/user.py:93 users/serializers/user.py:164 +#: perms/serializers/permission.py:39 perms/serializers/permission.py:71 +#: users/serializers/user.py:93 users/serializers/user.py:165 msgid "Is expired" msgstr "期限切れです" @@ -2506,9 +2522,9 @@ msgstr "メール" msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:62 users/serializers/user.py:94 -#: users/serializers/user.py:162 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:38 +#: perms/serializers/permission.py:72 users/serializers/user.py:94 +#: users/serializers/user.py:163 msgid "Is valid" msgstr "有効です" @@ -2539,7 +2555,7 @@ msgstr "表示" #: authentication/templates/authentication/_access_key_modal.html:66 #: settings/serializers/security.py:39 users/models/user.py:601 -#: users/serializers/profile.py:115 users/templates/users/mfa_setting.html:61 +#: users/serializers/profile.py:115 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "無効化" @@ -2590,7 +2606,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:416 +#: jumpserver/conf.py:417 #: 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 @@ -2812,19 +2828,23 @@ msgstr "本を飛ばすからユーザーを取得できませんでした" msgid "Please login with a password and then bind the FeiShu" msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" -#: authentication/views/login.py:181 +#: authentication/views/login.py:182 msgid "Redirecting" msgstr "リダイレクト" -#: authentication/views/login.py:182 +#: authentication/views/login.py:183 msgid "Redirecting to {} authentication" msgstr "{} 認証へのリダイレクト" -#: authentication/views/login.py:205 +#: authentication/views/login.py:206 msgid "Please enable cookies and try again." msgstr "クッキーを有効にして、もう一度お試しください。" -#: authentication/views/login.py:307 +#: authentication/views/login.py:247 +msgid "User email already exists ({})" +msgstr "ユーザー メールボックスは既に存在します ({})" + +#: authentication/views/login.py:325 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2832,15 +2852,15 @@ msgstr "" "{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n" " このページを閉じないでください" -#: authentication/views/login.py:312 +#: authentication/views/login.py:330 msgid "No ticket found" msgstr "チケットが見つかりません" -#: authentication/views/login.py:348 +#: authentication/views/login.py:366 msgid "Logout success" msgstr "ログアウト成功" -#: authentication/views/login.py:349 +#: authentication/views/login.py:367 msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" @@ -2872,7 +2892,7 @@ msgstr "企業の微信からユーザーを取得できませんでした" msgid "Please login with a password and then bind the WeCom" msgstr "パスワードでログインしてからWeComをバインドしてください" -#: common/api/action.py:52 +#: common/api/action.py:51 msgid "Request file format may be wrong" msgstr "リクエストファイルの形式が間違っている可能性があります" @@ -2967,14 +2987,20 @@ msgstr "オブジェクト" msgid "Organization ID" msgstr "組織 ID" -#: common/drf/parsers/base.py:17 +#: common/drf/parsers/base.py:21 msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "ファイルの内容がオーバーフローしました (最大長 '{}' バイト)" -#: common/drf/parsers/base.py:159 +#: common/drf/parsers/base.py:189 msgid "Parse file error: {}" msgstr "解析ファイルエラー: {}" +#: common/drf/parsers/excel.py:14 +#, fuzzy +#| msgid "Invalid zip file" +msgid "Invalid excel file" +msgstr "zip ファイルが無効です" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -3110,7 +3136,7 @@ msgstr "無効な IP" msgid "Invalid address" msgstr "無効なアドレス。" -#: common/utils/translate.py:42 +#: common/utils/translate.py:45 #, python-format msgid "Hello %s" msgstr "こんにちは %s" @@ -3140,11 +3166,11 @@ msgstr "選択項目のみエクスポート" msgid "Export filtered: %s" msgstr "検索のエクスポート: %s" -#: jumpserver/conf.py:415 +#: jumpserver/conf.py:416 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:417 +#: jumpserver/conf.py:418 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -3216,11 +3242,11 @@ msgstr "投稿サイトニュース" msgid "No account available" msgstr "利用可能なアカウントがありません" -#: ops/ansible/inventory.py:186 +#: ops/ansible/inventory.py:189 msgid "Ansible disabled" msgstr "Ansible 無効" -#: ops/ansible/inventory.py:202 +#: ops/ansible/inventory.py:205 msgid "Skip hosts below:" msgstr "次のホストをスキップします: " @@ -3236,7 +3262,11 @@ msgstr "タスクは存在しません" msgid "Task {} args or kwargs error" msgstr "タスク実行パラメータエラー" -#: ops/api/playbook.py:83 +#: ops/api/playbook.py:38 +msgid "Currently playbook is being used in a job" +msgstr "現在プレイブックは1つのジョブで使用されています" + +#: ops/api/playbook.py:92 msgid "Unsupported file content" msgstr "サポートされていないファイルの内容" @@ -3509,15 +3539,15 @@ msgstr "終了しました" msgid "Time cost" msgstr "時を過ごす" -#: ops/tasks.py:32 +#: ops/tasks.py:34 msgid "Run ansible task" msgstr "Ansible タスクを実行する" -#: ops/tasks.py:61 +#: ops/tasks.py:63 msgid "Run ansible task execution" msgstr "Ansible タスクの実行を開始する" -#: ops/tasks.py:77 +#: ops/tasks.py:79 msgid "Clear celery periodic tasks" msgstr "タスクログを定期的にクリアする" @@ -3735,7 +3765,7 @@ msgstr "内部の役割は、破壊することはできません" msgid "The role has been bound to users, can't be destroy" msgstr "ロールはユーザーにバインドされており、破壊することはできません" -#: rbac/api/role.py:83 +#: rbac/api/role.py:87 msgid "Internal role, can't be update" msgstr "内部ロール、更新できません" @@ -3747,27 +3777,27 @@ msgstr "{} 少なくとも1つのシステムロール" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:109 +#: rbac/builtin.py:113 msgid "SystemAdmin" msgstr "システム管理者" -#: rbac/builtin.py:112 +#: rbac/builtin.py:116 msgid "SystemAuditor" msgstr "システム監査人" -#: rbac/builtin.py:115 +#: rbac/builtin.py:119 msgid "SystemComponent" msgstr "システムコンポーネント" -#: rbac/builtin.py:121 +#: rbac/builtin.py:125 msgid "OrgAdmin" msgstr "組織管理者" -#: rbac/builtin.py:124 +#: rbac/builtin.py:128 msgid "OrgAuditor" msgstr "監査員を組織する" -#: rbac/builtin.py:127 +#: rbac/builtin.py:131 msgid "OrgUser" msgstr "組織ユーザー" @@ -3880,10 +3910,6 @@ msgstr "監査ビュー" msgid "System setting" msgstr "システム設定" -#: rbac/tree.py:28 -msgid "Other" -msgstr "その他" - #: rbac/tree.py:37 msgid "Session audits" msgstr "セッション監査" @@ -4104,7 +4130,7 @@ msgstr "そうでない場合はユーザーを作成" msgid "Enable DingTalk Auth" msgstr "ピン認証の有効化" -#: settings/serializers/auth/feishu.py:14 +#: settings/serializers/auth/feishu.py:16 msgid "Enable FeiShu Auth" msgstr "飛本認証の有効化" @@ -4155,12 +4181,12 @@ msgstr "" "する方法、username, name,emailはjumpserverのユーザーが必要とする属性です" #: settings/serializers/auth/ldap.py:77 -msgid "Connect timeout" -msgstr "接続タイムアウト" +msgid "Connect timeout (s)" +msgstr "接続タイムアウト (秒)" #: settings/serializers/auth/ldap.py:79 -msgid "Search paged size" -msgstr "ページサイズを検索" +msgid "Search paged size (piece)" +msgstr "ページサイズを検索 (じょう)" #: settings/serializers/auth/ldap.py:81 msgid "Enable LDAP auth" @@ -4281,8 +4307,8 @@ msgid "Scopes" msgstr "スコープ" #: settings/serializers/auth/oidc.py:90 -msgid "Id token max age" -msgstr "IDトークンの最大年齢" +msgid "Id token max age (s)" +msgstr "IDトークンの最大年齢 (秒)" #: settings/serializers/auth/oidc.py:93 msgid "Id token include claims" @@ -5408,9 +5434,9 @@ msgstr "セッション" msgid "Risk level" msgstr "リスクレベル" -#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 -#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 -#: terminal/connect_methods.py:51 +#: terminal/connect_methods.py:54 terminal/connect_methods.py:55 +#: terminal/connect_methods.py:56 terminal/connect_methods.py:57 +#: terminal/connect_methods.py:58 msgid "DB Client" msgstr "データベース クライアント" @@ -5468,7 +5494,7 @@ msgstr "無効なアプレット パッケージ、ファイル {} がありま msgid "Hosting" msgstr "ホスト マシン" -#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:43 +#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:53 msgid "Deploy options" msgstr "展開パラメーター" @@ -5626,23 +5652,23 @@ msgstr "リプレイ" msgid "Date end" msgstr "終了日" -#: terminal/models/session/session.py:239 +#: terminal/models/session/session.py:243 msgid "Session record" msgstr "セッション記録" -#: terminal/models/session/session.py:241 +#: terminal/models/session/session.py:245 msgid "Can monitor session" msgstr "セッションを監視できます" -#: terminal/models/session/session.py:242 +#: terminal/models/session/session.py:246 msgid "Can share session" msgstr "セッションを共有できます" -#: terminal/models/session/session.py:243 +#: terminal/models/session/session.py:247 msgid "Can terminate session" msgstr "セッションを終了できます" -#: terminal/models/session/session.py:244 +#: terminal/models/session/session.py:248 msgid "Can validate session action perm" msgstr "セッションアクションのパーマを検証できます" @@ -5718,35 +5744,58 @@ msgstr "セッションごと" msgid "Per Device" msgstr "デバイスごと" -#: terminal/serializers/applet_host.py:32 -msgid "API Server" -msgstr "API 仕える" - #: terminal/serializers/applet_host.py:33 -msgid "RDS Licensing" -msgstr "RDS ライセンス" +msgid "Core API" +msgstr "コア サービス アドレス" #: terminal/serializers/applet_host.py:34 +msgid "" +" \n" +" Tips: The application release machine communicates with the Core " +"service. \n" +" If the release machine and the Core service are on the same network " +"segment, \n" +" it is recommended to fill in the intranet address, otherwise fill in " +"the current site URL \n" +"
\n" +" eg: https://172.16.10.110 or https://dev.jumpserver.com\n" +" " +msgstr "" +"ヒント: アプリケーション リリース マシンは、コア サービスと通信します。リリー" +"ス マシンとコア サービスが同じネットワーク セグメント上にある場合は、イントラ" +"ネット アドレスを入力することをお勧めします。それ以外の場合は、現在のサイト " +"URL を入力します。
例: https://172.16.10.110 または https://dev." +"jumpserver.com" + +#: terminal/serializers/applet_host.py:42 terminal/serializers/storage.py:168 +msgid "Ignore Certificate Verification" +msgstr "証明書の検証を無視する" + +#: terminal/serializers/applet_host.py:43 +msgid "Existing RDS license" +msgstr "既存の RDS 証明書" + +#: terminal/serializers/applet_host.py:44 msgid "RDS License Server" msgstr "RDS ライセンス サーバー" -#: terminal/serializers/applet_host.py:35 +#: terminal/serializers/applet_host.py:45 msgid "RDS Licensing Mode" msgstr "RDS 認可モード" -#: terminal/serializers/applet_host.py:37 +#: terminal/serializers/applet_host.py:47 msgid "RDS Single Session Per User" msgstr "RDS シングル ユーザー シングル セッション" -#: terminal/serializers/applet_host.py:38 +#: terminal/serializers/applet_host.py:48 msgid "RDS Max Disconnection Time" msgstr "最大切断時間" -#: terminal/serializers/applet_host.py:39 +#: terminal/serializers/applet_host.py:49 msgid "RDS Remote App Logoff Time Limit" msgstr "RDS 远程应用注销时间限制" -#: terminal/serializers/applet_host.py:45 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:55 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "ロードステータス" @@ -5778,6 +5827,12 @@ msgstr "" "Oracle プロキシサーバーがリッスンするポートは動的です。追加の Oracle データ" "ベースインスタンスはポートリスナーを追加します" +#: terminal/serializers/endpoint.py:35 +msgid "Visit IP/Host, if empty, use the current request instead" +msgstr "" +"IP/ホストにアクセスします。空の場合は、代わりに現在のリクエストのアドレスを使" +"用します" + #: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" @@ -5879,10 +5934,6 @@ msgstr "インデックス" msgid "Doc type" msgstr "Docタイプ" -#: terminal/serializers/storage.py:168 -msgid "Ignore Certificate Verification" -msgstr "証明書の検証を無視する" - #: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 msgid "Not found" msgstr "見つかりません" @@ -5899,11 +5950,11 @@ msgstr "孤立したセッションをクリアする" msgid "Upload session replay to external storage" msgstr "セッションの記録を外部ストレージにアップロードする" -#: terminal/tasks.py:83 +#: terminal/tasks.py:84 msgid "Run applet host deployment" msgstr "アプリケーション マシンの展開を実行する" -#: terminal/tasks.py:90 +#: terminal/tasks.py:94 msgid "Install applet" msgstr "アプリをインストールする" @@ -6388,7 +6439,7 @@ msgstr "公開キー" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:729 users/serializers/user.py:163 +#: users/models/user.py:729 users/serializers/user.py:164 msgid "Is service account" msgstr "サービスアカウントです" @@ -6417,7 +6468,7 @@ msgid "Secret key" msgstr "秘密キー" #: users/models/user.py:758 users/serializers/profile.py:147 -#: users/serializers/user.py:160 +#: users/serializers/user.py:161 msgid "Is first login" msgstr "最初のログインです" @@ -6524,7 +6575,7 @@ msgstr "MFAフォース有効化" msgid "Login blocked" msgstr "ログインブロック" -#: users/serializers/user.py:95 users/serializers/user.py:168 +#: users/serializers/user.py:95 users/serializers/user.py:169 msgid "Is OTP bound" msgstr "仮想MFAがバインドされているか" @@ -6532,19 +6583,25 @@ msgstr "仮想MFAがバインドされているか" msgid "Can public key authentication" msgstr "公開鍵認証が可能" -#: users/serializers/user.py:165 +#: users/serializers/user.py:166 msgid "Avatar url" msgstr "アバターURL" -#: users/serializers/user.py:275 +#: users/serializers/user.py:171 +#, fuzzy +#| msgid "One level" +msgid "MFA level" +msgstr "1つのレベル" + +#: users/serializers/user.py:277 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:276 +#: users/serializers/user.py:278 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:309 +#: users/serializers/user.py:311 msgid "name not unique" msgstr "名前が一意ではない" @@ -6660,6 +6717,10 @@ msgstr "MFA強制有効化、無効化できません" msgid "MFA setting" msgstr "MFAの設定" +#: users/templates/users/mfa_setting.html:61 +msgid "Reset" +msgstr "リセット" + #: users/templates/users/reset_password.html:23 msgid "Your password must satisfy" msgstr "パスワードを満たす必要があります" @@ -7388,30 +7449,3 @@ msgstr "究極のエディション" #: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "コミュニティ版" - -#, fuzzy -#~| msgid "Only admin users" -#~ msgid "Unix admin user" -#~ msgstr "管理者のみ" - -#, fuzzy -#~| msgid "Only admin users" -#~ msgid "Windows admin user" -#~ msgstr "管理者のみ" - -#, fuzzy -#~| msgid "Only admin users" -#~ msgid "Linux admin user" -#~ msgstr "管理者のみ" - -#~ msgid "Can push account to asset" -#~ msgstr "アカウントをアセットにプッシュできます" - -#~ msgid "Add asset to node" -#~ msgstr "ノードにアセットを追加する" - -#~ msgid "Move asset to node" -#~ msgstr "アセットをノードに移動する" - -#~ msgid "Remove asset from node" -#~ msgstr "ノードからアセットを削除" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 11c2ca99d..360ae7c4b 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:a29193d2982b254444285cfb2d61f7ef7355ae2bab181cdf366446e879ab32fb -size 111963 +oid sha256:9819889a6d8b2934b06c5b242e3f63f404997f30851919247a405f542e8a03bc +size 113244 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index b8db40723..ba0a34c84 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-02-23 19:11+0800\n" +"POT-Creation-Date: 2023-03-14 17:34+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -168,7 +168,7 @@ msgstr "仅创建" #: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19 #: assets/serializers/label.py:27 audits/models.py:48 #: authentication/models/connection_token.py:33 -#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:35 #: terminal/backends/command/models.py:20 terminal/models/session/session.py:32 #: terminal/notifications.py:95 terminal/serializers/command.py:17 #: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212 @@ -181,7 +181,7 @@ msgid "Su from" msgstr "切换自" #: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:29 +#: settings/serializers/auth/feishu.py:20 terminal/models/applet/applet.py:29 msgid "Version" msgstr "版本" @@ -194,9 +194,9 @@ msgstr "来源" #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 #: acls/models/base.py:102 acls/serializers/base.py:57 -#: assets/serializers/asset/common.py:124 assets/serializers/gateway.py:28 +#: assets/serializers/asset/common.py:131 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:32 +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40 #: terminal/backends/command/models.py:21 terminal/models/session/session.py:34 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 msgid "Account" @@ -235,7 +235,7 @@ msgid "Can change asset account template secret" msgstr "可以更改资产账号模版密码" #: accounts/models/automations/backup_account.py:27 -#: accounts/models/automations/change_secret.py:47 +#: accounts/models/automations/change_secret.py:65 #: accounts/serializers/account/backup.py:34 #: accounts/serializers/automations/change_secret.py:57 msgid "Recipient" @@ -326,7 +326,7 @@ msgstr "查看推送账号执行" msgid "Can add push account execution" msgstr "创建推送账号执行" -#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 #: accounts/serializers/account/account.py:134 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 @@ -335,52 +335,52 @@ msgstr "创建推送账号执行" msgid "Secret type" msgstr "密文类型" -#: accounts/models/automations/change_secret.py:19 -#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/models/automations/change_secret.py:20 +#: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 #: settings/serializers/auth/radius.py:19 msgid "Secret" msgstr "密钥" -#: accounts/models/automations/change_secret.py:22 +#: accounts/models/automations/change_secret.py:23 #: accounts/serializers/automations/change_secret.py:40 msgid "Secret strategy" msgstr "密文策略" -#: accounts/models/automations/change_secret.py:24 +#: accounts/models/automations/change_secret.py:25 msgid "Password rules" msgstr "密码规则" -#: accounts/models/automations/change_secret.py:27 +#: accounts/models/automations/change_secret.py:28 msgid "SSH key change strategy" msgstr "SSH 密钥推送方式" -#: accounts/models/automations/change_secret.py:54 +#: accounts/models/automations/change_secret.py:72 msgid "Change secret automation" msgstr "自动化改密" -#: accounts/models/automations/change_secret.py:71 +#: accounts/models/automations/change_secret.py:89 msgid "Old secret" msgstr "原密码" -#: accounts/models/automations/change_secret.py:73 +#: accounts/models/automations/change_secret.py:91 msgid "Date started" msgstr "开始日期" -#: accounts/models/automations/change_secret.py:74 +#: accounts/models/automations/change_secret.py:92 #: assets/models/automations/base.py:115 ops/models/base.py:56 #: ops/models/celery.py:64 ops/models/job.py:192 #: terminal/models/applet/host.py:110 msgid "Date finished" msgstr "结束日期" -#: accounts/models/automations/change_secret.py:76 assets/const/automation.py:8 +#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8 #: common/const/choices.py:20 msgid "Error" msgstr "错误" -#: accounts/models/automations/change_secret.py:80 +#: accounts/models/automations/change_secret.py:98 msgid "Change secret record" msgstr "改密记录" @@ -393,7 +393,7 @@ msgid "Date last login" msgstr "最后登录日期" #: accounts/models/automations/gather_account.py:15 -#: accounts/models/automations/push_account.py:13 accounts/models/base.py:34 +#: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 #: acls/serializers/base.py:18 acls/serializers/base.py:49 #: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25 #: authentication/forms.py:27 authentication/models/temp_token.py:9 @@ -418,11 +418,11 @@ msgstr "自动化收集账号" msgid "Gather asset accounts" msgstr "收集账号" -#: accounts/models/automations/push_account.py:12 +#: accounts/models/automations/push_account.py:14 msgid "Triggers" msgstr "触发方式" -#: accounts/models/automations/push_account.py:14 acls/models/base.py:81 +#: accounts/models/automations/push_account.py:16 acls/models/base.py:81 #: acls/serializers/base.py:81 acls/serializers/login_acl.py:25 #: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:109 @@ -430,7 +430,7 @@ msgstr "触发方式" msgid "Action" msgstr "动作" -#: accounts/models/automations/push_account.py:40 +#: accounts/models/automations/push_account.py:59 msgid "Push asset account" msgstr "账号推送" @@ -445,8 +445,8 @@ msgstr "账号验证" #: 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:21 assets/models/platform.py:76 -#: assets/serializers/asset/common.py:67 assets/serializers/asset/common.py:143 -#: assets/serializers/platform.py:91 assets/serializers/platform.py:136 +#: assets/serializers/asset/common.py:74 assets/serializers/asset/common.py:151 +#: assets/serializers/platform.py:133 #: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:91 ops/models/playbook.py:23 ops/serializers/job.py:19 @@ -469,7 +469,7 @@ msgstr "特权账号" #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:107 -#: terminal/models/applet/applet.py:32 users/serializers/user.py:161 +#: terminal/models/applet/applet.py:32 users/serializers/user.py:162 msgid "Is active" msgstr "激活" @@ -512,24 +512,24 @@ msgstr "" "密密码" #: accounts/serializers/account/account.py:65 -#: assets/serializers/asset/common.py:65 settings/serializers/auth/sms.py:75 +#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75 msgid "Template" msgstr "模板" #: accounts/serializers/account/account.py:68 -#: assets/serializers/asset/common.py:62 +#: assets/serializers/asset/common.py:69 msgid "Push now" msgstr "立即推送" #: accounts/serializers/account/account.py:70 -#: accounts/serializers/account/base.py:62 +#: accounts/serializers/account/base.py:64 msgid "Has secret" msgstr "已托管密码" #: accounts/serializers/account/account.py:75 applications/models.py:11 #: assets/models/label.py:21 assets/models/platform.py:77 -#: assets/serializers/asset/common.py:120 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:97 assets/serializers/platform.py:137 +#: assets/serializers/asset/common.py:127 assets/serializers/cagegory.py:8 +#: assets/serializers/platform.py:94 assets/serializers/platform.py:134 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" @@ -540,7 +540,7 @@ msgstr "类别" #: 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:78 -#: assets/serializers/asset/common.py:121 assets/serializers/platform.py:96 +#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93 #: audits/serializers.py:48 #: authentication/serializers/connect_token_secret.py:116 ops/models/job.py:102 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31 @@ -587,8 +587,8 @@ msgstr "密钥/密码" msgid "Key password" msgstr "密钥密码" -#: accounts/serializers/account/base.py:79 -#: assets/serializers/asset/common.py:291 +#: accounts/serializers/account/base.py:81 +#: assets/serializers/asset/common.py:301 msgid "Spec info" msgstr "特殊信息" @@ -737,7 +737,7 @@ msgstr "激活中" #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:23 rbac/builtin.py:118 +#: perms/serializers/permission.py:31 rbac/builtin.py:122 #: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19 #: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 #: terminal/notifications.py:96 terminal/notifications.py:144 @@ -823,7 +823,7 @@ msgstr "" "格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:36 +#: acls/serializers/base.py:40 assets/serializers/asset/host.py:35 msgid "IP/Host" msgstr "IP/主机" @@ -902,7 +902,7 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" -#: assets/api/asset/asset.py:142 +#: assets/api/asset/asset.py:143 msgid "Cannot create asset directly, you should create a host or other" msgstr "不能直接创建资产, 你应该创建主机或其他资产" @@ -926,7 +926,7 @@ msgstr "删除失败,节点包含资产" msgid "App assets" msgstr "资产管理" -#: assets/automations/base/manager.py:114 +#: assets/automations/base/manager.py:113 msgid "{} disabled" msgstr "{} 已禁用" @@ -936,10 +936,8 @@ msgid "No account" msgstr "没有账号" #: assets/automations/ping_gateway/manager.py:36 -#, fuzzy -#| msgid "Assets amount" msgid "Asset, {}, using account {}" -msgstr "资产数量" +msgstr "资产, {}, 使用账号 {}" #: assets/automations/ping_gateway/manager.py:55 #, python-brace-format @@ -990,7 +988,7 @@ msgid "Device" msgstr "网络设备" #: assets/const/category.py:13 assets/models/asset/database.py:9 -#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:108 +#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:115 msgid "Database" msgstr "数据库" @@ -1003,6 +1001,18 @@ msgstr "云服务" msgid "Web" msgstr "Web" +#: assets/const/cloud.py:7 +msgid "Public cloud" +msgstr "公有云" + +#: assets/const/cloud.py:8 +msgid "Private cloud" +msgstr "私有云" + +#: assets/const/cloud.py:9 +msgid "Kubernetes" +msgstr "" + #: assets/const/device.py:7 terminal/models/applet/applet.py:24 #: tickets/const.py:8 msgid "General" @@ -1020,6 +1030,10 @@ msgstr "路由器" msgid "Firewall" msgstr "防火墙" +#: assets/const/host.py:12 rbac/tree.py:28 +msgid "Other" +msgstr "其它" + #: assets/const/types.py:200 msgid "All types" msgstr "所有类型" @@ -1038,7 +1052,7 @@ msgid "Basic" msgstr "基本" #: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/asset/common.py:116 assets/serializers/platform.py:40 +#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40 msgid "Script" msgstr "脚本" @@ -1167,7 +1181,7 @@ msgstr "云服务" msgid "Port" msgstr "端口" -#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:144 +#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152 msgid "Address" msgstr "地址" @@ -1204,7 +1218,7 @@ msgstr "可以匹配资产" msgid "Can change asset nodes" msgstr "可以修改资产节点" -#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:109 +#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116 #: settings/serializers/email.py:37 msgid "Use SSL" msgstr "使用 SSL" @@ -1221,7 +1235,7 @@ msgstr "客户端证书" msgid "Client key" msgstr "客户端密钥" -#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:110 +#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117 msgid "Allow invalid cert" msgstr "忽略证书校验" @@ -1229,23 +1243,23 @@ msgstr "忽略证书校验" msgid "Autofill" msgstr "自动代填" -#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:113 +#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120 #: assets/serializers/platform.py:32 msgid "Username selector" msgstr "用户名选择器" -#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:114 +#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121 #: assets/serializers/platform.py:35 msgid "Password selector" msgstr "密码选择器" -#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:115 +#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122 #: assets/serializers/platform.py:38 msgid "Submit selector" msgstr "确认按钮选择器" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:290 rbac/tree.py:35 +#: assets/serializers/asset/common.py:300 rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" @@ -1261,7 +1275,7 @@ msgstr "资产自动化任务" #: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:183 #: terminal/models/applet/applet.py:157 terminal/models/applet/host.py:108 #: terminal/models/component/status.py:27 terminal/serializers/applet.py:18 -#: terminal/serializers/applet_host.py:93 tickets/models/ticket/general.py:283 +#: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 #: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164 #: xpack/plugins/cloud/models.py:216 @@ -1285,7 +1299,7 @@ msgid "Date verified" msgstr "校验日期" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 -#: perms/serializers/permission.py:25 users/models/group.py:25 +#: perms/serializers/permission.py:33 users/models/group.py:25 #: users/models/user.py:723 msgid "User group" msgstr "用户组" @@ -1347,7 +1361,7 @@ msgstr "系统" msgid "Value" msgstr "值" -#: assets/models/label.py:40 assets/serializers/asset/common.py:122 +#: assets/models/label.py:40 assets/serializers/asset/common.py:129 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:114 #: common/serializers/common.py:79 settings/serializers/sms.py:7 @@ -1374,7 +1388,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:558 perms/serializers/permission.py:28 +#: assets/models/node.py:558 perms/serializers/permission.py:36 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "节点" @@ -1451,23 +1465,23 @@ msgstr "元数据" msgid "Internal" msgstr "内置" -#: assets/models/platform.py:83 assets/serializers/platform.py:94 +#: assets/models/platform.py:83 assets/serializers/platform.py:91 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:85 assets/serializers/platform.py:122 +#: assets/models/platform.py:85 assets/serializers/platform.py:119 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:87 assets/serializers/platform.py:121 +#: assets/models/platform.py:87 assets/serializers/platform.py:118 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:88 assets/serializers/platform.py:104 +#: assets/models/platform.py:88 assets/serializers/platform.py:101 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:90 assets/serializers/platform.py:101 +#: assets/models/platform.py:90 assets/serializers/platform.py:98 msgid "Automation" msgstr "自动化" @@ -1476,35 +1490,36 @@ msgstr "自动化" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/serializers/asset/common.py:112 +#: assets/serializers/asset/common.py:119 msgid "Auto fill" msgstr "自动代填" -#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:99 +#: assets/serializers/asset/common.py:130 assets/serializers/platform.py:96 #: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:66 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 msgid "Protocols" msgstr "协议组" -#: assets/serializers/asset/common.py:142 -#: assets/serializers/asset/common.py:292 -msgid "Auto info" -msgstr "自动化信息" - -#: assets/serializers/asset/common.py:145 +#: assets/serializers/asset/common.py:132 +#: assets/serializers/asset/common.py:153 msgid "Node path" msgstr "节点路径" -#: assets/serializers/asset/common.py:218 +#: assets/serializers/asset/common.py:150 +#: assets/serializers/asset/common.py:302 +msgid "Auto info" +msgstr "自动化信息" + +#: assets/serializers/asset/common.py:226 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:253 +#: assets/serializers/asset/common.py:261 msgid "port out of range (1-65535)" msgstr "端口超出范围 (1-65535)" -#: assets/serializers/asset/common.py:260 +#: assets/serializers/asset/common.py:268 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" @@ -1516,56 +1531,56 @@ msgstr "协议是必填的: {}" msgid "This field is required." msgstr "该字段是必填项。" -#: assets/serializers/asset/host.py:12 +#: assets/serializers/asset/host.py:11 msgid "Vendor" msgstr "制造商" -#: assets/serializers/asset/host.py:13 +#: assets/serializers/asset/host.py:12 msgid "Model" msgstr "型号" -#: assets/serializers/asset/host.py:14 tickets/models/ticket/general.py:299 +#: assets/serializers/asset/host.py:13 tickets/models/ticket/general.py:299 msgid "Serial number" msgstr "序列号" -#: assets/serializers/asset/host.py:15 +#: assets/serializers/asset/host.py:14 msgid "CPU model" msgstr "CPU型号" -#: assets/serializers/asset/host.py:16 +#: assets/serializers/asset/host.py:15 msgid "CPU count" msgstr "CPU数量" -#: assets/serializers/asset/host.py:17 +#: assets/serializers/asset/host.py:16 msgid "CPU cores" msgstr "CPU核数" -#: assets/serializers/asset/host.py:18 +#: assets/serializers/asset/host.py:17 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/serializers/asset/host.py:19 +#: assets/serializers/asset/host.py:18 msgid "Memory" msgstr "内存" -#: assets/serializers/asset/host.py:20 +#: assets/serializers/asset/host.py:19 msgid "Disk total" msgstr "硬盘大小" -#: assets/serializers/asset/host.py:22 +#: assets/serializers/asset/host.py:21 #: authentication/serializers/connect_token_secret.py:105 msgid "OS" msgstr "操作系统" -#: assets/serializers/asset/host.py:23 +#: assets/serializers/asset/host.py:22 msgid "OS version" msgstr "系统版本" -#: assets/serializers/asset/host.py:24 +#: assets/serializers/asset/host.py:23 msgid "OS arch" msgstr "系统架构" -#: assets/serializers/asset/host.py:28 +#: assets/serializers/asset/host.py:27 msgid "Info" msgstr "信息" @@ -1621,7 +1636,7 @@ msgstr "收集账号方式" msgid "Primary" msgstr "主要的" -#: assets/serializers/platform.py:123 +#: assets/serializers/platform.py:120 msgid "Default Domain" msgstr "默认网域" @@ -1789,11 +1804,11 @@ msgstr "任务" msgid "-" msgstr "-" -#: audits/handler.py:116 +#: audits/handler.py:115 msgid "Yes" msgstr "是" -#: audits/handler.py:116 +#: audits/handler.py:115 msgid "No" msgstr "否" @@ -1930,20 +1945,21 @@ msgid "Auth Token" msgstr "认证令牌" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:177 +#: authentication/views/login.py:74 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:778 msgid "WeCom" msgstr "企业微信" #: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:144 -#: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: settings/serializers/auth/feishu.py:10 users/models/user.py:780 +#: 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 msgid "FeiShu" msgstr "飞书" #: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:79 notifications/backends/__init__.py:12 +#: authentication/views/login.py:80 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:779 msgid "DingTalk" msgstr "钉钉" @@ -1961,19 +1977,19 @@ msgstr "清理审计会话任务日志" msgid "This action require verify your MFA" msgstr "该操作需要验证您的 MFA, 请先开启并配置" -#: authentication/api/connection_token.py:268 +#: authentication/api/connection_token.py:295 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:271 +#: authentication/api/connection_token.py:298 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:283 +#: authentication/api/connection_token.py:310 msgid "ACL action is reject" msgstr "ACL 动作是拒绝" -#: authentication/api/connection_token.py:287 +#: authentication/api/connection_token.py:314 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2217,15 +2233,15 @@ msgstr "没有绑定飞书" msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/errors/redirect.py:85 authentication/mixins.py:306 +#: authentication/errors/redirect.py:85 authentication/mixins.py:307 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors/redirect.py:93 authentication/mixins.py:313 +#: authentication/errors/redirect.py:93 authentication/mixins.py:314 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors/redirect.py:101 authentication/mixins.py:320 +#: authentication/errors/redirect.py:101 authentication/mixins.py:321 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -2326,11 +2342,11 @@ msgstr "清空手机号码禁用" msgid "Authentication failed (before login check failed): {}" msgstr "认证失败(登录前检查失败): {}" -#: authentication/mixins.py:256 +#: authentication/mixins.py:257 msgid "The MFA type ({}) is not enabled" msgstr "该 MFA ({}) 方式没有启用" -#: authentication/mixins.py:296 +#: authentication/mixins.py:297 msgid "Please change your password" msgstr "请修改密码" @@ -2397,7 +2413,7 @@ msgstr "没有用户或用户失效" msgid "No asset or inactive asset" msgstr "没有资产或资产未激活" -#: authentication/models/connection_token.py:257 +#: authentication/models/connection_token.py:258 msgid "Super connection token" msgstr "超级连接令牌" @@ -2456,16 +2472,16 @@ msgid "Ticket info" msgstr "工单信息" #: authentication/serializers/connection_token.py:20 -#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:60 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37 +#: perms/serializers/permission.py:70 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "动作" #: authentication/serializers/connection_token.py:41 -#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 -#: users/serializers/user.py:93 users/serializers/user.py:164 +#: perms/serializers/permission.py:39 perms/serializers/permission.py:71 +#: users/serializers/user.py:93 users/serializers/user.py:165 msgid "Is expired" msgstr "已过期" @@ -2484,9 +2500,9 @@ msgstr "邮箱" msgid "The {} cannot be empty" msgstr "{} 不能为空" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:62 users/serializers/user.py:94 -#: users/serializers/user.py:162 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:38 +#: perms/serializers/permission.py:72 users/serializers/user.py:94 +#: users/serializers/user.py:163 msgid "Is valid" msgstr "是否有效" @@ -2517,7 +2533,7 @@ msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 #: settings/serializers/security.py:39 users/models/user.py:601 -#: users/serializers/profile.py:115 users/templates/users/mfa_setting.html:61 +#: users/serializers/profile.py:115 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "禁用" @@ -2568,7 +2584,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:416 +#: jumpserver/conf.py:417 #: 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 @@ -2782,19 +2798,23 @@ msgstr "从飞书获取用户失败" msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" -#: authentication/views/login.py:181 +#: authentication/views/login.py:182 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:182 +#: authentication/views/login.py:183 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:205 +#: authentication/views/login.py:206 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:307 +#: authentication/views/login.py:247 +msgid "User email already exists ({})" +msgstr "用户邮箱已存在 ({})" + +#: authentication/views/login.py:325 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2802,15 +2822,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:312 +#: authentication/views/login.py:330 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:348 +#: authentication/views/login.py:366 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:349 +#: authentication/views/login.py:367 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2842,7 +2862,7 @@ msgstr "从企业微信获取用户失败" msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" -#: common/api/action.py:52 +#: common/api/action.py:51 msgid "Request file format may be wrong" msgstr "上传的文件格式错误 或 其它类型资源的文件" @@ -2937,14 +2957,20 @@ msgstr "对象" msgid "Organization ID" msgstr "组织 ID" -#: common/drf/parsers/base.py:17 +#: common/drf/parsers/base.py:21 msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容太大 (最大长度 `{}` 字节)" -#: common/drf/parsers/base.py:159 +#: common/drf/parsers/base.py:189 msgid "Parse file error: {}" msgstr "解析文件错误: {}" +#: common/drf/parsers/excel.py:14 +#, fuzzy +#| msgid "Invalid zip file" +msgid "Invalid excel file" +msgstr "无效的 zip 文件" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -3080,7 +3106,7 @@ msgstr "无效 IP" msgid "Invalid address" msgstr "无效地址" -#: common/utils/translate.py:42 +#: common/utils/translate.py:45 #, python-format msgid "Hello %s" msgstr "你好 %s" @@ -3110,11 +3136,11 @@ msgstr "仅导出选择项" msgid "Export filtered: %s" msgstr "导出搜素: %s" -#: jumpserver/conf.py:415 +#: jumpserver/conf.py:416 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:417 +#: jumpserver/conf.py:418 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -3181,11 +3207,11 @@ msgstr "发布站内消息" msgid "No account available" msgstr "无可用账号" -#: ops/ansible/inventory.py:186 +#: ops/ansible/inventory.py:189 msgid "Ansible disabled" msgstr "Ansible 已禁用" -#: ops/ansible/inventory.py:202 +#: ops/ansible/inventory.py:205 msgid "Skip hosts below:" msgstr "跳过以下主机: " @@ -3201,7 +3227,11 @@ msgstr "任务 {} 不存在" msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" -#: ops/api/playbook.py:83 +#: ops/api/playbook.py:38 +msgid "Currently playbook is being used in a job" +msgstr "当前 playbook 正在作业中使用" + +#: ops/api/playbook.py:92 msgid "Unsupported file content" msgstr "不支持的文件内容" @@ -3474,15 +3504,15 @@ msgstr "是否完成" msgid "Time cost" msgstr "花费时间" -#: ops/tasks.py:32 +#: ops/tasks.py:34 msgid "Run ansible task" msgstr "运行 Ansible 任务" -#: ops/tasks.py:61 +#: ops/tasks.py:63 msgid "Run ansible task execution" msgstr "开始执行 Ansible 任务" -#: ops/tasks.py:77 +#: ops/tasks.py:79 msgid "Clear celery periodic tasks" msgstr "清理周期任务" @@ -3699,7 +3729,7 @@ msgstr "内部角色,不能删除" msgid "The role has been bound to users, can't be destroy" msgstr "角色已绑定用户,不能删除" -#: rbac/api/role.py:83 +#: rbac/api/role.py:87 msgid "Internal role, can't be update" msgstr "内部角色,不能更新" @@ -3711,27 +3741,27 @@ msgstr "{} 至少有一个系统角色" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:109 +#: rbac/builtin.py:113 msgid "SystemAdmin" msgstr "系统管理员" -#: rbac/builtin.py:112 +#: rbac/builtin.py:116 msgid "SystemAuditor" msgstr "系统审计员" -#: rbac/builtin.py:115 +#: rbac/builtin.py:119 msgid "SystemComponent" msgstr "系统组件" -#: rbac/builtin.py:121 +#: rbac/builtin.py:125 msgid "OrgAdmin" msgstr "组织管理员" -#: rbac/builtin.py:124 +#: rbac/builtin.py:128 msgid "OrgAuditor" msgstr "组织审计员" -#: rbac/builtin.py:127 +#: rbac/builtin.py:131 msgid "OrgUser" msgstr "组织用户" @@ -3843,10 +3873,6 @@ msgstr "审计台" msgid "System setting" msgstr "系统设置" -#: rbac/tree.py:28 -msgid "Other" -msgstr "其它" - #: rbac/tree.py:37 msgid "Session audits" msgstr "会话审计" @@ -4067,7 +4093,7 @@ msgstr "创建用户(如果不存在)" msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" -#: settings/serializers/auth/feishu.py:14 +#: settings/serializers/auth/feishu.py:16 msgid "Enable FeiShu Auth" msgstr "启用飞书认证" @@ -4118,12 +4144,12 @@ msgstr "" "email 是jumpserver的用户需要属性" #: settings/serializers/auth/ldap.py:77 -msgid "Connect timeout" -msgstr "连接超时时间" +msgid "Connect timeout (s)" +msgstr "连接超时时间 (秒)" #: settings/serializers/auth/ldap.py:79 -msgid "Search paged size" -msgstr "搜索分页数量" +msgid "Search paged size (piece)" +msgstr "搜索分页数量 (条)" #: settings/serializers/auth/ldap.py:81 msgid "Enable LDAP auth" @@ -4244,8 +4270,8 @@ msgid "Scopes" msgstr "连接范围" #: settings/serializers/auth/oidc.py:90 -msgid "Id token max age" -msgstr "令牌有效时间" +msgid "Id token max age (s)" +msgstr "令牌有效时间 (秒)" #: settings/serializers/auth/oidc.py:93 msgid "Id token include claims" @@ -5336,9 +5362,9 @@ msgstr "会话" msgid "Risk level" msgstr "风险等级" -#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 -#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 -#: terminal/connect_methods.py:51 +#: terminal/connect_methods.py:54 terminal/connect_methods.py:55 +#: terminal/connect_methods.py:56 terminal/connect_methods.py:57 +#: terminal/connect_methods.py:58 msgid "DB Client" msgstr "数据库客户端" @@ -5396,7 +5422,7 @@ msgstr "Applet pkg 无效,缺少文件 {}" msgid "Hosting" msgstr "宿主机" -#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:43 +#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:53 msgid "Deploy options" msgstr "部署参数" @@ -5554,23 +5580,23 @@ msgstr "回放" msgid "Date end" msgstr "结束日期" -#: terminal/models/session/session.py:239 +#: terminal/models/session/session.py:243 msgid "Session record" msgstr "会话记录" -#: terminal/models/session/session.py:241 +#: terminal/models/session/session.py:245 msgid "Can monitor session" msgstr "可以监控会话" -#: terminal/models/session/session.py:242 +#: terminal/models/session/session.py:246 msgid "Can share session" msgstr "可以分享会话" -#: terminal/models/session/session.py:243 +#: terminal/models/session/session.py:247 msgid "Can terminate session" msgstr "可以终断会话" -#: terminal/models/session/session.py:244 +#: terminal/models/session/session.py:248 msgid "Can validate session action perm" msgstr "可以验证会话动作权限" @@ -5646,35 +5672,56 @@ msgstr "每用户" msgid "Per Device" msgstr "每设备" -#: terminal/serializers/applet_host.py:32 -msgid "API Server" -msgstr "API 服务" - #: terminal/serializers/applet_host.py:33 -msgid "RDS Licensing" -msgstr "RDS 许可证" +msgid "Core API" +msgstr "Core 服务地址" #: terminal/serializers/applet_host.py:34 +msgid "" +" \n" +" Tips: The application release machine communicates with the Core " +"service. \n" +" If the release machine and the Core service are on the same network " +"segment, \n" +" it is recommended to fill in the intranet address, otherwise fill in " +"the current site URL \n" +"
\n" +" eg: https://172.16.10.110 or https://dev.jumpserver.com\n" +" " +msgstr "" +"提示:应用发布机和 Core 服务进行通信使用,如果发布机和 Core 服务在同一网段," +"建议填写内网地址,否则填写当前站点 URL
例如:https://172.16.10.110 or " +"https://dev.jumpserver.com" + +#: terminal/serializers/applet_host.py:42 terminal/serializers/storage.py:168 +msgid "Ignore Certificate Verification" +msgstr "忽略证书认证" + +#: terminal/serializers/applet_host.py:43 +msgid "Existing RDS license" +msgstr "已有 RDS 许可证" + +#: terminal/serializers/applet_host.py:44 msgid "RDS License Server" msgstr "RDS 许可服务器" -#: terminal/serializers/applet_host.py:35 +#: terminal/serializers/applet_host.py:45 msgid "RDS Licensing Mode" msgstr "RDS 授权模式" -#: terminal/serializers/applet_host.py:37 +#: terminal/serializers/applet_host.py:47 msgid "RDS Single Session Per User" msgstr "RDS 单用户单会话" -#: terminal/serializers/applet_host.py:38 +#: terminal/serializers/applet_host.py:48 msgid "RDS Max Disconnection Time" msgstr "RDS 最大断开时间" -#: terminal/serializers/applet_host.py:39 +#: terminal/serializers/applet_host.py:49 msgid "RDS Remote App Logoff Time Limit" msgstr "RDS 远程应用注销时间限制" -#: terminal/serializers/applet_host.py:45 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:55 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "负载状态" @@ -5706,6 +5753,10 @@ msgstr "" "Oracle 代理服务器监听端口是动态的,每增加一个 Oracle 数据库实例,就会增加一个" "端口监听" +#: terminal/serializers/endpoint.py:35 +msgid "Visit IP/Host, if empty, use the current request instead" +msgstr "访问IP/Host,如果为空,则使用当前请求的地址代替" + #: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" @@ -5805,10 +5856,6 @@ msgstr "索引" msgid "Doc type" msgstr "文档类型" -#: terminal/serializers/storage.py:168 -msgid "Ignore Certificate Verification" -msgstr "忽略证书认证" - #: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 msgid "Not found" msgstr "没有发现" @@ -5825,11 +5872,11 @@ msgstr "清除孤儿会话" msgid "Upload session replay to external storage" msgstr "上传会话录像到外部存储" -#: terminal/tasks.py:83 +#: terminal/tasks.py:84 msgid "Run applet host deployment" msgstr "运行应用机部署" -#: terminal/tasks.py:90 +#: terminal/tasks.py:94 msgid "Install applet" msgstr "安装应用" @@ -6308,7 +6355,7 @@ msgstr "SSH公钥" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:729 users/serializers/user.py:163 +#: users/models/user.py:729 users/serializers/user.py:164 msgid "Is service account" msgstr "服务账号" @@ -6337,7 +6384,7 @@ msgid "Secret key" msgstr "Secret key" #: users/models/user.py:758 users/serializers/profile.py:147 -#: users/serializers/user.py:160 +#: users/serializers/user.py:161 msgid "Is first login" msgstr "首次登录" @@ -6444,27 +6491,33 @@ msgstr "强制 MFA" msgid "Login blocked" msgstr "登录被阻塞" -#: users/serializers/user.py:95 users/serializers/user.py:168 +#: users/serializers/user.py:95 users/serializers/user.py:169 msgid "Is OTP bound" msgstr "是否绑定了虚拟 MFA" #: users/serializers/user.py:97 msgid "Can public key authentication" -msgstr "公钥认证" +msgstr "可以使用公钥认证" -#: users/serializers/user.py:165 +#: users/serializers/user.py:166 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:275 +#: users/serializers/user.py:171 +#, fuzzy +#| msgid "One level" +msgid "MFA level" +msgstr "1 级" + +#: users/serializers/user.py:277 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:276 +#: users/serializers/user.py:278 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:309 +#: users/serializers/user.py:311 msgid "name not unique" msgstr "名称重复" @@ -6578,6 +6631,10 @@ msgstr "MFA 已强制启用,无法禁用" msgid "MFA setting" msgstr "设置 MFA 多因子认证" +#: users/templates/users/mfa_setting.html:61 +msgid "Reset" +msgstr "重置" + #: users/templates/users/reset_password.html:23 msgid "Your password must satisfy" msgstr "您的密码必须满足:" @@ -7294,6 +7351,9 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "API Server" +#~ msgstr "API 服务" + #~ msgid "Unix admin user" #~ msgstr "Unix 管理员" diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index ba3be6bde..2dbfc44ef 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -17,7 +17,7 @@ class JMSInventory: :param account_policy: privileged_only, privileged_first, skip """ self.assets = self.clean_assets(assets) - self.account_prefer = account_prefer + self.account_prefer = self.get_account_prefer(account_prefer) self.account_policy = account_policy self.host_callback = host_callback self.exclude_hosts = {} @@ -140,36 +140,51 @@ class JMSInventory: return host def get_asset_accounts(self, asset): - return list(asset.accounts.filter(is_active=True)) + from assets.const import Connectivity + accounts = asset.accounts.filter(is_active=True).order_by('-privileged', '-date_updated') + accounts_connectivity_ok = list(accounts.filter(connectivity=Connectivity.OK)) + accounts_connectivity_no = list(accounts.exclude(connectivity=Connectivity.OK)) + return accounts_connectivity_ok + accounts_connectivity_no + + @staticmethod + def get_account_prefer(account_prefer): + account_usernames = [] + if isinstance(account_prefer, str) and account_prefer: + account_usernames = list(map(lambda x: x.lower(), account_prefer.split(','))) + return account_usernames + + def get_refer_account(self, accounts): + account = None + if accounts: + account = list(filter( + lambda a: a.username.lower() in self.account_prefer, accounts + )) + account = account[0] if account else None + return account def select_account(self, asset): accounts = self.get_asset_accounts(asset) - if not accounts: + if not accounts or self.account_policy == 'skip': return None account_selected = None - account_usernames = self.account_prefer - if isinstance(self.account_prefer, str): - account_usernames = self.account_prefer.split(',') - - # 优先使用提供的名称 - if account_usernames: - account_matched = list(filter(lambda account: account.username in account_usernames, accounts)) - account_selected = account_matched[0] if account_matched else None - - if account_selected or self.account_policy == 'skip': - return account_selected + # 首先找到特权账号 + privileged_accounts = list(filter(lambda account: account.privileged, accounts)) + # 不同类型的账号选择,优先使用提供的名称 + refer_privileged_account = self.get_refer_account(privileged_accounts) if self.account_policy in ['privileged_only', 'privileged_first']: - account_matched = list(filter(lambda account: account.privileged, accounts)) - account_selected = account_matched[0] if account_matched else None + first_privileged = privileged_accounts[0] if privileged_accounts else None + account_selected = refer_privileged_account or first_privileged - if account_selected: + # 此策略不管是否匹配到账号都需强制返回 + if self.account_policy == 'privileged_only': return account_selected - if self.account_policy == 'privileged_first': - account_selected = accounts[0] if accounts else None - return account_selected + if not account_selected: + account_selected = self.get_refer_account(accounts) + + return account_selected or accounts[0] def generate(self, path_dir): hosts = [] diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 8a1bbf9f5..72d970dee 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -83,7 +83,7 @@ class CeleryResultApi(generics.RetrieveAPIView): def get_object(self): pk = self.kwargs.get('pk') - return AsyncResult(pk) + return AsyncResult(str(pk)) class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet): diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index 12e5b0659..d848e029c 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -32,6 +32,15 @@ class PlaybookViewSet(OrgBulkModelViewSet): model = Playbook search_fields = ('name', 'comment') + def perform_destroy(self, instance): + instance = self.get_object() + if instance.job_set.exists(): + raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")}) + instance_id = instance.id + super().perform_destroy(instance) + dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__()) + shutil.rmtree(dest_path) + def get_queryset(self): queryset = super().get_queryset() queryset = queryset.filter(creator=self.request.user) @@ -62,10 +71,10 @@ class PlaybookFileBrowserAPIView(APIView): rbac_perms = () permission_classes = (RBACPermission,) rbac_perms = { - 'GET': 'ops.change_playbooks', - 'POST': 'ops.change_playbooks', - 'DELETE': 'ops.change_playbooks', - 'PATCH': 'ops.change_playbooks', + 'GET': 'ops.change_playbook', + 'POST': 'ops.change_playbook', + 'DELETE': 'ops.change_playbook', + 'PATCH': 'ops.change_playbook', } protected_files = ['root', 'main.yml'] diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 833e20ea0..11ad27aa4 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -3,8 +3,10 @@ from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded from django.utils.translation import ugettext_lazy as _ +from django_celery_beat.models import PeriodicTask from common.utils import get_logger, get_object_or_none +from ops.celery import app from orgs.utils import tmp_to_org, tmp_to_root_org from .celery.decorator import ( register_as_period_task, after_app_ready_start @@ -19,7 +21,7 @@ from .notifications import ServerPerformanceCheckUtil logger = get_logger(__file__) -def job_task_activity_callback(self, job_id, trigger): +def job_task_activity_callback(self, job_id, *args, **kwargs): job = get_object_or_none(Job, id=job_id) if not job: return @@ -48,7 +50,7 @@ def run_ops_job(job_id): logger.error("Start adhoc execution error: {}".format(e)) -def job_execution_task_activity_callback(self, execution_id, trigger): +def job_execution_task_activity_callback(self, execution_id, *args, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) if not execution: return @@ -78,16 +80,14 @@ def run_ops_job_execution(execution_id, **kwargs): @after_app_ready_start def clean_celery_periodic_tasks(): """清除celery定时任务""" - need_cleaned_tasks = [ - 'handle_be_interrupted_change_auth_task_periodic', - ] - logger.info('Start clean celery periodic tasks: {}'.format(need_cleaned_tasks)) - for task_name in need_cleaned_tasks: - logger.info('Start clean task: {}'.format(task_name)) - task = get_celery_periodic_task(task_name) - if task is None: - logger.info('Task does not exist: {}'.format(task_name)) + logger.info('Start clean celery periodic tasks.') + register_tasks = PeriodicTask.objects.all() + for task in register_tasks: + if task.task in app.tasks: continue + + task_name = task.name + logger.info('Start clean task: {}'.format(task_name)) disable_celery_periodic_task(task_name) delete_celery_periodic_task(task_name) task = get_celery_periodic_task(task_name) diff --git a/apps/ops/views.py b/apps/ops/views.py index 85aa94c65..ccff2aa21 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -13,7 +13,7 @@ class CeleryTaskLogView(PermissionsMixin, TemplateView): template_name = 'ops/celery_task_log.html' permission_classes = [RBACPermission] rbac_perms = { - 'GET': 'ops.view_celerytask' + 'GET': 'ops.view_celerytaskexecution' } def get_context_data(self, **kwargs): diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 1eb54e037..b1f8fbdea 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -114,9 +114,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache): @staticmethod def compute_total_count_today_active_assets(): t = local_zero_hour() - return Session.objects.filter( - date_start__gte=t, is_success=False - ).values('asset_id').distinct().count() + return Session.objects.filter(date_start__gte=t).values('asset_id').distinct().count() @staticmethod def compute_total_count_today_failed_sessions(): diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 7349d9623..b4e6001e3 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -102,7 +102,10 @@ def on_post_delete_refresh_org_resource_statistics_cache(sender, instance, **kwa def _refresh_session_org_resource_statistics_cache(instance: Session): - cache_field_name = ['total_count_online_users', 'total_count_online_sessions', 'total_count_today_failed_sessions'] + cache_field_name = [ + 'total_count_online_users', 'total_count_online_sessions', + 'total_count_today_active_assets','total_count_today_failed_sessions' + ] org_cache = OrgResourceStatisticsCache(instance.org) org_cache.expire(*cache_field_name) diff --git a/apps/perms/api/user_permission/assets.py b/apps/perms/api/user_permission/assets.py index 0daa23791..f94854164 100644 --- a/apps/perms/api/user_permission/assets.py +++ b/apps/perms/api/user_permission/assets.py @@ -30,6 +30,12 @@ class BaseUserPermedAssetsApi(SelfOrPKUserMixin, ListAPIView): filterset_class = AssetFilterSet serializer_class = serializers.AssetPermedSerializer + def get_serializer_class(self): + serializer_class = super().get_serializer_class() + if self.request.query_params.get('id'): + serializer_class = serializers.AssetPermedDetailSerializer + return serializer_class + def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Asset.objects.none() diff --git a/apps/perms/migrations/0029_auto_20220728_1728.py b/apps/perms/migrations/0029_auto_20220728_1728.py index fce1f43a4..0230e9aff 100644 --- a/apps/perms/migrations/0029_auto_20220728_1728.py +++ b/apps/perms/migrations/0029_auto_20220728_1728.py @@ -23,6 +23,7 @@ def migrate_app_perms_to_assets(apps, schema_editor): asset_permission = asset_permission_model() for attr in attrs: setattr(asset_permission, attr, getattr(app_perm, attr)) + asset_permission.name = f"App-{app_perm.name}" asset_permissions.append(asset_permission) asset_permission_model.objects.bulk_create(asset_permissions, ignore_conflicts=True) diff --git a/apps/perms/migrations/0030_auto_20220816_1132.py b/apps/perms/migrations/0030_auto_20220816_1132.py index d6b2710fc..3915b8b1a 100644 --- a/apps/perms/migrations/0030_auto_20220816_1132.py +++ b/apps/perms/migrations/0030_auto_20220816_1132.py @@ -9,11 +9,11 @@ def migrate_system_user_to_accounts(apps, schema_editor): bulk_size = 10000 while True: asset_permissions = asset_permission_model.objects \ - .prefetch_related('system_users')[count:bulk_size] + .prefetch_related('system_users')[count:bulk_size] if not asset_permissions: break - count += len(asset_permissions) + count += len(asset_permissions) updated = [] for asset_permission in asset_permissions: asset_permission.accounts = [s.username for s in asset_permission.system_users.all()] diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 85799f011..27bc90d89 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # -from django.db.models import Q +from django.db.models import Q, QuerySet from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from accounts.models import AccountTemplate, Account +from accounts.tasks import push_accounts_to_assets_task from assets.models import Asset, Node from common.serializers.fields import BitChoicesField, ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -18,6 +20,12 @@ class ActionChoicesField(BitChoicesField): def __init__(self, **kwargs): super().__init__(choice_cls=ActionChoices, **kwargs) + def to_file_representation(self, value): + return [v['value'] for v in value] + + def to_file_internal_value(self, data): + return data + class AssetPermissionSerializer(BulkOrgResourceModelSerializer): users = ObjectRelatedField(queryset=User.objects, many=True, required=False, label=_('User')) @@ -31,6 +39,8 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): is_expired = serializers.BooleanField(read_only=True, label=_("Is expired")) accounts = serializers.ListField(label=_("Account"), required=False) + template_accounts = AccountTemplate.objects.none() + class Meta: model = AssetPermission fields_mini = ["id", "name"] @@ -73,8 +83,55 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): actions.default = list(actions.choices.keys()) @staticmethod - def validate_accounts(accounts): - return list(set(accounts)) + def get_all_assets(nodes, assets): + node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) + direct_asset_ids = [asset.id for asset in assets] + asset_ids = set(direct_asset_ids + list(node_asset_ids)) + return Asset.objects.filter(id__in=asset_ids) + + def create_accounts(self, assets): + need_create_accounts = [] + account_attribute = [ + 'name', 'username', 'secret_type', 'secret', 'privileged', 'is_active', 'org_id' + ] + for asset in assets: + asset_exist_accounts = Account.objects.none() + for template in self.template_accounts: + asset_exist_accounts |= asset.accounts.filter( + username=template.username, + secret_type=template.secret_type, + ) + username_secret_type_dict = asset_exist_accounts.values('username', 'secret_type') + for template in self.template_accounts: + condition = { + 'username': template.username, + 'secret_type': template.secret_type + } + if condition in username_secret_type_dict: + continue + account_data = {key: getattr(template, key) for key in account_attribute} + account_data['name'] = f"{account_data['name']}-clone" + need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) + return Account.objects.bulk_create(need_create_accounts) + + def create_and_push_account(self, nodes, assets): + if not self.template_accounts: + return + assets = self.get_all_assets(nodes, assets) + accounts = self.create_accounts(assets) + push_accounts_to_assets_task.delay([str(account.id) for account in accounts]) + + def validate_accounts(self, usernames: list[str]): + template_ids = [] + account_usernames = [] + for username in usernames: + if username.startswith('%'): + template_ids.append(username[1:]) + else: + account_usernames.append(username) + self.template_accounts = AccountTemplate.objects.filter(id__in=template_ids) + template_usernames = list(self.template_accounts.values_list('username', flat=True)) + return list(set(account_usernames + template_usernames)) @classmethod def setup_eager_loading(cls, queryset): @@ -112,6 +169,13 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): ).distinct() instance.nodes.add(*nodes_to_set) + def validate(self, attrs): + self.create_and_push_account( + attrs.get("nodes", []), + attrs.get("assets", []) + ) + return super().validate(attrs) + def create(self, validated_data): display = { "users_display": validated_data.pop("users_display", ""), diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 34143d936..c9582cc11 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -15,7 +15,7 @@ from perms.serializers.permission import ActionChoicesField __all__ = [ 'NodePermedSerializer', 'AssetPermedSerializer', - 'AccountsPermedSerializer' + 'AssetPermedDetailSerializer', 'AccountsPermedSerializer' ] @@ -46,6 +46,12 @@ class AssetPermedSerializer(OrgResourceModelSerializerMixin): return queryset +class AssetPermedDetailSerializer(AssetPermedSerializer): + class Meta(AssetPermedSerializer.Meta): + fields = AssetPermedSerializer.Meta.fields + ['spec_info'] + read_only_fields = fields + + class NodePermedSerializer(serializers.ModelSerializer): class Meta: model = Node diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index 16e8bb344..b66b247d8 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,5 +1,6 @@ from collections import defaultdict +from orgs.utils import tmp_to_org from accounts.models import Account from accounts.const import AliasAccount from .permission import AssetPermissionUtil @@ -16,10 +17,11 @@ class PermAccountUtil(AssetPermissionUtil): :param asset: Asset :param account_name: 可能是 @USER @INPUT 字符串 """ - permed_accounts = self.get_permed_accounts_for_user(user, asset) - accounts_mapper = {account.alias: account for account in permed_accounts} - account = accounts_mapper.get(account_name) - return account + with tmp_to_org(asset.org): + permed_accounts = self.get_permed_accounts_for_user(user, asset) + accounts_mapper = {account.alias: account for account in permed_accounts} + account = accounts_mapper.get(account_name) + return account def get_permed_accounts_for_user(self, user, asset): """ 获取授权给用户某个资产的账号 """ diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index e15386bea..cf80c64ab 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -18,14 +18,19 @@ user_perms = ( ('assets', 'asset', 'match', 'asset'), ('assets', 'systemuser', 'match', 'systemuser'), ('assets', 'node', 'match', 'node'), + ("ops", "adhoc", "*", "*"), + ("ops", "playbook", "*", "*"), + ("ops", "job", "*", "*"), + ("ops", "jobexecution", "*", "*"), + ("ops", "celerytaskexecution", "view", "*"), ) system_user_perms = ( - ('authentication', 'connectiontoken', 'add,change,view', 'connectiontoken'), - ('authentication', 'temptoken', 'add,change,view', 'temptoken'), - ('authentication', 'accesskey', '*', '*'), - ('tickets', 'ticket', 'view', 'ticket'), -) + user_perms + _view_all_joined_org_perms + ('authentication', 'connectiontoken', 'add,change,view', 'connectiontoken'), + ('authentication', 'temptoken', 'add,change,view', 'temptoken'), + ('authentication', 'accesskey', '*', '*'), + ('tickets', 'ticket', 'view', 'ticket'), + ) + user_perms + _view_all_joined_org_perms _auditor_perms = ( ('rbac', 'menupermission', 'view', 'audit'), @@ -41,7 +46,6 @@ auditor_perms = user_perms + _auditor_perms system_auditor_perms = system_user_perms + _auditor_perms + _view_root_perms - app_exclude_perms = [ ('users', 'user', 'add,delete', 'user'), ('orgs', 'org', 'add,delete,change', 'org'), diff --git a/apps/rbac/const.py b/apps/rbac/const.py index eeb09b46e..4ab48161c 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -135,7 +135,7 @@ only_system_permissions = ( ('xpack', 'license', '*', '*'), ('settings', 'setting', '*', '*'), ('tickets', '*', '*', '*'), - ('ops', 'task', 'view', 'taskmonitor'), + ('ops', 'celerytask', 'view', 'taskmonitor'), ('terminal', 'terminal', '*', '*'), ('terminal', 'commandstorage', '*', '*'), ('terminal', 'replaystorage', '*', '*'), diff --git a/apps/rbac/permissions.py b/apps/rbac/permissions.py index 7cc35d370..7c3f21610 100644 --- a/apps/rbac/permissions.py +++ b/apps/rbac/permissions.py @@ -97,13 +97,13 @@ class RBACPermission(permissions.DjangoModelPermissions): else: model_cls = queryset.model except AssertionError as e: - logger.error(f'Error get model cls: {e}') + # logger.error(f'Error get model cls: {e}') model_cls = None except AttributeError as e: - logger.error(f'Error get model cls: {e}') + # logger.error(f'Error get model cls: {e}') model_cls = None except Exception as e: - logger.error('Error get model class: {} of {}'.format(e, view)) + # logger.error('Error get model class: {} of {}'.format(e, view)) raise e return model_cls diff --git a/apps/settings/api/email.py b/apps/settings/api/email.py index 2116b969d..45bec7514 100644 --- a/apps/settings/api/email.py +++ b/apps/settings/api/email.py @@ -42,7 +42,7 @@ class MailTestingAPI(APIView): # if k.startswith('EMAIL'): # setattr(settings, k, v) try: - subject = settings.EMAIL_SUBJECT_PREFIX + "Test" + subject = settings.EMAIL_SUBJECT_PREFIX or '' + "Test" message = "Test smtp setting" email_from = email_from or email_host_user email_recipient = email_recipient or email_from diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index 1443a244c..a06d41b23 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -9,6 +9,13 @@ __all__ = ['FeiShuSettingSerializer'] class FeiShuSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('FeiShu') + VERSION_CHOICES = ( + ('feishu', _('FeiShu')), + ('lark', 'Lark') + ) + AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') - AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) + FEISHU_VERSION = serializers.ChoiceField( + choices=VERSION_CHOICES, default='feishu', label=_('Version') + ) diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index c530aa80c..c40aec530 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -74,9 +74,9 @@ class LDAPSettingSerializer(serializers.Serializer): ) AUTH_LDAP_CONNECT_TIMEOUT = serializers.IntegerField( min_value=1, max_value=300, - required=False, label=_('Connect timeout'), + required=False, label=_('Connect timeout (s)'), ) - AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size')) + AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size (piece)')) AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth')) diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index cbbcf3386..2c1aa8411 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -87,7 +87,7 @@ class OIDCSettingSerializer(KeycloakSettingSerializer): ) AUTH_OPENID_SCOPES = serializers.CharField(required=False, max_length=1024, label=_('Scopes')) AUTH_OPENID_ID_TOKEN_MAX_AGE = serializers.IntegerField( - required=False, label=_('Id token max age') + required=False, label=_('Id token max age (s)') ) AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS = serializers.BooleanField( required=False, label=_('Id token include claims') diff --git a/apps/templates/_base_only_content.html b/apps/templates/_base_only_content.html index 87a4c9870..af02b18e7 100644 --- a/apps/templates/_base_only_content.html +++ b/apps/templates/_base_only_content.html @@ -18,6 +18,10 @@ margin: 0 auto; padding: 100px 20px 20px 20px; } + + .ibox-content { + padding: 30px; + } {% block custom_head_css_js %} {% endblock %} @@ -30,7 +34,7 @@

{% block title %}{% endblock %}

-
+
{% block content %} {% endblock %}
diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index 61b431b9a..ad81c1141 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -23,7 +23,7 @@
{% if has_cancel %} -
+
{% trans 'Cancel' %} @@ -43,7 +43,7 @@ {% endblock %} {% block custom_foot_js %} -