diff --git a/.dockerignore b/.dockerignore index a504fb4ec..81c9033ba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,4 +8,4 @@ celerybeat.pid ### Vagrant ### .vagrant/ apps/xpack/.git - +.history/ diff --git a/.github/ISSUE_TEMPLATE/----.md b/.github/ISSUE_TEMPLATE/----.md index b407e8a25..147f42db4 100644 --- a/.github/ISSUE_TEMPLATE/----.md +++ b/.github/ISSUE_TEMPLATE/----.md @@ -3,7 +3,10 @@ name: 需求建议 about: 提出针对本项目的想法和建议 title: "[Feature] " labels: 类型:需求 -assignees: ibuler +assignees: + - ibuler + - baijiangjie + --- diff --git a/.github/ISSUE_TEMPLATE/bug---.md b/.github/ISSUE_TEMPLATE/bug---.md index 3c590459c..e4a21adde 100644 --- a/.github/ISSUE_TEMPLATE/bug---.md +++ b/.github/ISSUE_TEMPLATE/bug---.md @@ -3,7 +3,9 @@ name: Bug 提交 about: 提交产品缺陷帮助我们更好的改进 title: "[Bug] " labels: 类型:bug -assignees: wojiushixiaobai +assignees: + - wojiushixiaobai + - baijiangjie --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 1dd2a68d6..b15719590 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -3,7 +3,9 @@ name: 问题咨询 about: 提出针对本项目安装部署、使用及其他方面的相关问题 title: "[Question] " labels: 类型:提问 -assignees: wojiushixiaobai +assignees: + - wojiushixiaobai + - baijiangjie --- diff --git a/.gitignore b/.gitignore index 316630d9f..9573a70b7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ releashe /apps/script.py data/* test.py +.history/ diff --git a/Dockerfile b/Dockerfile index a7d6d2b9c..4cad1e0bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${TOOLS} \ && mkdir -p /root/.ssh/ \ - && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ + && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \ && echo "set mouse-=a" > ~/.vimrc \ && echo "no" | dpkg-reconfigure dash \ && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 index 2d44b00df..629623a85 100644 --- a/Dockerfile.loong64 +++ b/Dockerfile.loong64 @@ -53,7 +53,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${TOOLS} \ && mkdir -p /root/.ssh/ \ - && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ + && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \ && echo "set mouse-=a" > ~/.vimrc \ && echo "no" | dpkg-reconfigure dash \ && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ diff --git a/README.md b/README.md index c5a1d2d2a..ca31cce08 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 - [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/) - [产品文档](https://docs.jumpserver.org) +- [在线学习](https://edu.fit2cloud.com/page/2635362) - [知识库](https://kb.fit2cloud.com/categories/jumpserver) ## 案例研究 diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index d12f11549..7b9991988 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -1,20 +1,21 @@ from django.shortcuts import get_object_or_404 from rest_framework.decorators import action -from rest_framework.generics import ListAPIView +from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK from accounts import serializers from accounts.filters import AccountFilterSet from accounts.models import Account from assets.models import Asset, Node -from common.permissions import UserConfirmation, ConfirmType +from common.permissions import UserConfirmation, ConfirmType, IsValidUser from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission __all__ = [ 'AccountViewSet', 'AccountSecretsViewSet', - 'AccountHistoriesSecretAPI' + 'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi', ] @@ -28,7 +29,7 @@ class AccountViewSet(OrgBulkModelViewSet): rbac_perms = { 'partial_update': ['accounts.change_account'], 'su_from_accounts': 'accounts.view_account', - 'username_suggestions': 'accounts.view_account', + 'clear_secret': 'accounts.change_account', } @action(methods=['get'], detail=False, url_path='su-from-accounts') @@ -48,7 +49,10 @@ class AccountViewSet(OrgBulkModelViewSet): serializer = serializers.AccountSerializer(accounts, many=True) return Response(data=serializer.data) - @action(methods=['get'], detail=False, url_path='username-suggestions') + @action( + methods=['get'], detail=False, url_path='username-suggestions', + permission_classes=[IsValidUser] + ) def username_suggestions(self, request, *args, **kwargs): asset_ids = request.query_params.get('assets') node_keys = request.query_params.get('keys') @@ -71,6 +75,12 @@ class AccountViewSet(OrgBulkModelViewSet): usernames = common + others return Response(data=usernames) + @action(methods=['patch'], detail=False, url_path='clear-secret') + def clear_secret(self, request, *args, **kwargs): + account_ids = request.data.get('account_ids', []) + self.model.objects.filter(id__in=account_ids).update(secret=None) + return Response(status=HTTP_200_OK) + class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): """ @@ -87,6 +97,20 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): } +class AssetAccountBulkCreateApi(CreateAPIView): + serializer_class = serializers.AssetAccountBulkSerializer + rbac_perms = { + 'POST': 'accounts.add_account', + } + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.create(serializer.validated_data) + serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True) + return Response(data=serializer.data, status=HTTP_200_OK) + + class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView): model = Account.history.model serializer_class = serializers.AccountHistorySerializer diff --git a/apps/accounts/api/account/task.py b/apps/accounts/api/account/task.py index a042323f5..2f3f11dae 100644 --- a/apps/accounts/api/account/task.py +++ b/apps/accounts/api/account/task.py @@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView): else: account = accounts[0] asset = account.asset - if not asset.auto_info['ansible_enabled'] or \ - not asset.auto_info['ping_enabled']: + if not asset.auto_config['ansible_enabled'] or \ + not asset.auto_config['ping_enabled']: raise NotSupportedTemporarilyError() task = verify_accounts_connectivity_task.delay(account_ids) diff --git a/apps/accounts/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py index 3abca94dd..e6a846368 100644 --- a/apps/accounts/api/automations/gather_accounts.py +++ b/apps/accounts/api/automations/gather_accounts.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from accounts import serializers from accounts.const import AutomationTypes -from accounts.const import Source from accounts.filters import GatheredAccountFilterSet from accounts.models import GatherAccountsAutomation from accounts.models import GatheredAccount @@ -50,22 +48,12 @@ class GatheredAccountViewSet(OrgBulkModelViewSet): 'default': serializers.GatheredAccountSerializer, } rbac_perms = { - 'sync_account': 'assets.add_gatheredaccount', + 'sync_accounts': 'assets.add_gatheredaccount', } - @action(methods=['post'], detail=True, url_path='sync') - def sync_account(self, request, *args, **kwargs): - gathered_account = super().get_object() - asset = gathered_account.asset - username = gathered_account.username - accounts = asset.accounts.filter(username=username) - - if accounts.exists(): - accounts.update(source=Source.COLLECTED) - else: - asset.accounts.model.objects.create( - asset=asset, username=username, - name=f'{username}-{_("Collected")}', - source=Source.COLLECTED - ) + @action(methods=['post'], detail=False, url_path='sync-accounts') + def sync_accounts(self, request, *args, **kwargs): + gathered_account_ids = request.data.get('gathered_account_ids') + gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids) + self.model.sync_accounts(gathered_accounts) return Response(status=status.HTTP_201_CREATED) diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml index 3e3daae7f..4bb571f62 100644 --- a/apps/accounts/automations/change_secret/host/aix/main.yml +++ b/apps/accounts/automations/change_secret/host/aix/main.yml @@ -18,18 +18,18 @@ - name: remove jumpserver ssh key ansible.builtin.lineinfile: - dest: "{{ kwargs.dest }}" - regexp: "{{ kwargs.regexp }}" + dest: "{{ ssh_params.dest }}" + regexp: "{{ ssh_params.regexp }}" state: absent when: - account.secret_type == "ssh_key" - - kwargs.strategy == "set_jms" + - ssh_params.strategy == "set_jms" - name: Change SSH key ansible.builtin.authorized_key: user: "{{ account.username }}" key: "{{ account.secret }}" - exclusive: "{{ kwargs.exclusive }}" + exclusive: "{{ ssh_params.exclusive }}" when: account.secret_type == "ssh_key" - name: Refresh connection diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml index 932f3cade..8dea25c12 100644 --- a/apps/accounts/automations/change_secret/host/posix/main.yml +++ b/apps/accounts/automations/change_secret/host/posix/main.yml @@ -18,18 +18,18 @@ - name: remove jumpserver ssh key ansible.builtin.lineinfile: - dest: "{{ kwargs.dest }}" - regexp: "{{ kwargs.regexp }}" + dest: "{{ ssh_params.dest }}" + regexp: "{{ ssh_params.regexp }}" state: absent when: - account.secret_type == "ssh_key" - - kwargs.strategy == "set_jms" + - ssh_params.strategy == "set_jms" - name: Change SSH key ansible.builtin.authorized_key: user: "{{ account.username }}" key: "{{ account.secret }}" - exclusive: "{{ kwargs.exclusive }}" + exclusive: "{{ ssh_params.exclusive }}" when: account.secret_type == "ssh_key" - name: Refresh connection diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index 41bad5bda..05e2b1349 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -42,7 +42,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): def method_type(cls): return AutomationTypes.change_secret - def get_kwargs(self, account, secret, secret_type): + def get_ssh_params(self, account, secret, secret_type): kwargs = {} if secret_type != SecretType.SSH_KEY: return kwargs @@ -76,6 +76,11 @@ class ChangeSecretManager(AccountBasePlaybookManager): accounts = accounts.filter(id__in=self.account_ids) if self.secret_type: accounts = accounts.filter(secret_type=self.secret_type) + + if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED: + accounts = accounts.filter(privileged=False).exclude( + username__in=['root', 'administrator'] + ) return accounts def host_callback( @@ -106,6 +111,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): print(f'Windows {asset} does not support ssh key push') return inventory_hosts + host['ssh_params'] = {} for account in accounts: h = deepcopy(host) secret_type = account.secret_type @@ -124,7 +130,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): 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, secret_type) + h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type)) h['account'] = { 'name': account.name, 'username': account.username, diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py index 2ecd3d2e1..c4ba6b5a0 100644 --- a/apps/accounts/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -12,6 +12,7 @@ class GatherAccountsManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.host_asset_mapper = {} + self.is_sync_account = self.execution.snapshot.get('is_sync_account') @classmethod def method_type(cls): @@ -25,26 +26,38 @@ class GatherAccountsManager(AccountBasePlaybookManager): def filter_success_result(self, tp, result): result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result) return result - @staticmethod - def update_or_create_gathered_accounts(asset, result): + def generate_data(asset, result): + data = [] + for username, info in result.items(): + d = {'asset': asset, 'username': username, 'present': True} + if info.get('date'): + d['date_last_login'] = info['date'] + if info.get('address'): + d['address_last_login'] = info['address'][:32] + data.append(d) + return data + + def update_or_create_accounts(self, asset, result): + data = self.generate_data(asset, result) with tmp_to_org(asset.org_id): + gathered_accounts = [] GatheredAccount.objects.filter(asset=asset, present=True).update(present=False) - for username, data in result.items(): - d = {'asset': asset, 'username': username, 'present': True} - if data.get('date'): - d['date_last_login'] = data['date'] - if data.get('address'): - d['address_last_login'] = data['address'][:32] - GatheredAccount.objects.update_or_create( + for d in data: + username = d['username'] + gathered_account, __ = GatheredAccount.objects.update_or_create( defaults=d, asset=asset, username=username, ) + gathered_accounts.append(gathered_account) + if not self.is_sync_account: + return + GatheredAccount.sync_accounts(gathered_accounts) 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: result = self.filter_success_result(asset.type, info) - self.update_or_create_gathered_accounts(asset, result) + self.update_or_create_accounts(asset, result) else: logger.error("Not found info".format(host)) diff --git a/apps/accounts/automations/methods.py b/apps/accounts/automations/methods.py index be5890701..557202184 100644 --- a/apps/accounts/automations/methods.py +++ b/apps/accounts/automations/methods.py @@ -1,30 +1,6 @@ import os -import copy -from accounts.const import AutomationTypes from assets.automations.methods import get_platform_automation_methods - -def copy_change_secret_to_push_account(methods): - push_account = AutomationTypes.push_account - change_secret = AutomationTypes.change_secret - copy_methods = copy.deepcopy(methods) - for method in copy_methods: - if not method['id'].startswith(change_secret): - continue - copy_method = copy.deepcopy(method) - copy_method['method'] = push_account.value - copy_method['id'] = copy_method['id'].replace( - change_secret, push_account - ) - copy_method['name'] = copy_method['name'].replace( - 'Change secret', 'Push account' - ) - methods.append(copy_method) - return methods - - BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -automation_methods = get_platform_automation_methods(BASE_DIR) - -platform_automation_methods = copy_change_secret_to_push_account(automation_methods) +platform_automation_methods = get_platform_automation_methods(BASE_DIR) diff --git a/apps/accounts/automations/push_account/database/mongodb/main.yml b/apps/accounts/automations/push_account/database/mongodb/main.yml new file mode 100644 index 000000000..42ccd78ea --- /dev/null +++ b/apps/accounts/automations/push_account/database/mongodb/main.yml @@ -0,0 +1,58 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MongoDB connection + mongodb_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + ssl: "{{ jms_asset.spec_info.use_ssl }}" + ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}" + ssl_certfile: "{{ jms_asset.secret_info.client_key }}" + connection_options: + - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" + register: db_info + + - name: Display MongoDB version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change MongoDB password + mongodb_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + ssl: "{{ jms_asset.spec_info.use_ssl }}" + ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}" + ssl_certfile: "{{ jms_asset.secret_info.client_key }}" + connection_options: + - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" + db: "{{ jms_asset.spec_info.db_name }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + mongodb_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + ssl: "{{ jms_asset.spec_info.use_ssl }}" + ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}" + ssl_certfile: "{{ jms_asset.secret_info.client_key }}" + connection_options: + - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/accounts/automations/push_account/database/mongodb/manifest.yml b/apps/accounts/automations/push_account/database/mongodb/manifest.yml new file mode 100644 index 000000000..6c91977bf --- /dev/null +++ b/apps/accounts/automations/push_account/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_mongodb +name: Push account for MongoDB +category: database +type: + - mongodb +method: push_account diff --git a/apps/accounts/automations/push_account/database/mysql/main.yml b/apps/accounts/automations/push_account/database/mysql/main.yml new file mode 100644 index 000000000..26858c94e --- /dev/null +++ b/apps/accounts/automations/push_account/database/mysql/main.yml @@ -0,0 +1,43 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + db_name: "{{ jms_asset.spec_info.db_name }}" + + tasks: + - name: Test MySQL connection + community.mysql.mysql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version + register: db_info + + - name: MySQL version + debug: + var: db_info.version.full + + - name: Change MySQL password + community.mysql.mysql_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + host: "%" + priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + community.mysql.mysql_info: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version + when: + - db_info is succeeded + - change_info is succeeded \ No newline at end of file diff --git a/apps/accounts/automations/push_account/database/mysql/manifest.yml b/apps/accounts/automations/push_account/database/mysql/manifest.yml new file mode 100644 index 000000000..712d0bfb8 --- /dev/null +++ b/apps/accounts/automations/push_account/database/mysql/manifest.yml @@ -0,0 +1,7 @@ +id: push_account_mysql +name: Push account for MySQL +category: database +type: + - mysql + - mariadb +method: push_account diff --git a/apps/accounts/automations/push_account/database/oracle/main.yml b/apps/accounts/automations/push_account/database/oracle/main.yml new file mode 100644 index 000000000..ad58e0584 --- /dev/null +++ b/apps/accounts/automations/push_account/database/oracle/main.yml @@ -0,0 +1,44 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test Oracle connection + oracle_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + mode: "{{ jms_account.mode }}" + register: db_info + + - name: Display Oracle version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change Oracle password + oracle_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + mode: "{{ jms_account.mode }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + oracle_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/accounts/automations/push_account/database/oracle/manifest.yml b/apps/accounts/automations/push_account/database/oracle/manifest.yml new file mode 100644 index 000000000..f60215892 --- /dev/null +++ b/apps/accounts/automations/push_account/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_oracle +name: Push account for Oracle +category: database +type: + - oracle +method: push_account diff --git a/apps/accounts/automations/push_account/database/postgresql/main.yml b/apps/accounts/automations/push_account/database/postgresql/main.yml new file mode 100644 index 000000000..dbb11af12 --- /dev/null +++ b/apps/accounts/automations/push_account/database/postgresql/main.yml @@ -0,0 +1,46 @@ +- hosts: postgre + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test PostgreSQL connection + community.postgresql.postgresql_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_db: "{{ jms_asset.spec_info.db_name }}" + register: result + failed_when: not result.is_available + + - name: Display PostgreSQL version + debug: + var: result.server_version.full + when: result is succeeded + + - name: Change PostgreSQL password + community.postgresql.postgresql_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + db: "{{ jms_asset.spec_info.db_name }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + role_attr_flags: LOGIN + when: result is succeeded + register: change_info + + - name: Verify password + community.postgresql.postgresql_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + db: "{{ jms_asset.spec_info.db_name }}" + when: + - result is succeeded + - change_info is succeeded + register: result + failed_when: not result.is_available diff --git a/apps/accounts/automations/push_account/database/postgresql/manifest.yml b/apps/accounts/automations/push_account/database/postgresql/manifest.yml new file mode 100644 index 000000000..6488ddd5a --- /dev/null +++ b/apps/accounts/automations/push_account/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_postgresql +name: Push account for PostgreSQL +category: database +type: + - postgresql +method: push_account diff --git a/apps/accounts/automations/push_account/database/sqlserver/main.yml b/apps/accounts/automations/push_account/database/sqlserver/main.yml new file mode 100644 index 000000000..da0427f5c --- /dev/null +++ b/apps/accounts/automations/push_account/database/sqlserver/main.yml @@ -0,0 +1,69 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: | + SELECT @@version + register: db_info + + - name: SQLServer version + set_fact: + info: + version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}" + - debug: + var: info + + - name: Check whether SQLServer User exist + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" + when: db_info is succeeded + register: user_exist + + - name: Change SQLServer password + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version" + when: user_exist.query_results[0] | length != 0 + register: change_info + + - name: Add SQLServer user + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version" + when: user_exist.query_results[0] | length == 0 + register: change_info + + - name: Verify password + community.general.mssql_script: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: | + SELECT @@version + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/accounts/automations/push_account/database/sqlserver/manifest.yml b/apps/accounts/automations/push_account/database/sqlserver/manifest.yml new file mode 100644 index 000000000..f1dc32b66 --- /dev/null +++ b/apps/accounts/automations/push_account/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_sqlserver +name: Push account for SQLServer +category: database +type: + - sqlserver +method: push_account diff --git a/apps/accounts/automations/push_account/host/aix/main.yml b/apps/accounts/automations/push_account/host/aix/main.yml new file mode 100644 index 000000000..9ac68d20e --- /dev/null +++ b/apps/accounts/automations/push_account/host/aix/main.yml @@ -0,0 +1,93 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Test privileged account + ansible.builtin.ping: + + - name: Push user + ansible.builtin.user: + name: "{{ account.username }}" + shell: "{{ params.shell }}" + home: "{{ '/home/' + account.username }}" + groups: "{{ params.groups }}" + expires: -1 + state: present + + - name: "Add {{ account.username }} group" + ansible.builtin.group: + name: "{{ account.username }}" + state: present + + - name: Check home dir exists + ansible.builtin.stat: + path: "{{ '/home/' + account.username }}" + register: home_existed + + - name: Set home dir permission + ansible.builtin.file: + path: "{{ '/home/' + account.username }}" + owner: "{{ account.username }}" + group: "{{ account.username }}" + mode: "0700" + when: + - home_existed.stat.exists == true + + - name: Add user groups + ansible.builtin.user: + name: "{{ account.username }}" + groups: "{{ params.groups }}" + when: params.groups + + - name: Push user password + ansible.builtin.user: + name: "{{ account.username }}" + password: "{{ account.secret | password_hash('sha512') }}" + update_password: always + when: account.secret_type == "password" + + - name: remove jumpserver ssh key + ansible.builtin.lineinfile: + dest: "{{ ssh_params.dest }}" + regexp: "{{ ssh_params.regexp }}" + state: absent + when: + - account.secret_type == "ssh_key" + - ssh_params.strategy == "set_jms" + + - name: Push SSH key + ansible.builtin.authorized_key: + user: "{{ account.username }}" + key: "{{ account.secret }}" + exclusive: "{{ ssh_params.exclusive }}" + when: account.secret_type == "ssh_key" + + - name: Set sudo setting + ansible.builtin.lineinfile: + dest: /etc/sudoers + state: present + regexp: "^{{ account.username }} ALL=" + line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}" + validate: visudo -cf %s + when: + - params.sudo + + - name: Refresh connection + ansible.builtin.meta: reset_connection + + - name: Verify password + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + ansible_become: no + when: account.secret_type == "password" + + - name: Verify SSH key + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no + when: account.secret_type == "ssh_key" diff --git a/apps/accounts/automations/push_account/host/aix/manifest.yml b/apps/accounts/automations/push_account/host/aix/manifest.yml new file mode 100644 index 000000000..ccc051eac --- /dev/null +++ b/apps/accounts/automations/push_account/host/aix/manifest.yml @@ -0,0 +1,24 @@ +id: push_account_aix +name: Push account for aix +category: host +type: + - AIX +method: push_account +params: + - name: sudo + type: str + label: 'Sudo' + default: '/bin/whoami' + help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + + - name: shell + type: str + label: 'Shell' + default: '/bin/bash' + + - name: groups + type: str + label: '用户组' + default: '' + help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + diff --git a/apps/accounts/automations/push_account/host/posix/main.yml b/apps/accounts/automations/push_account/host/posix/main.yml new file mode 100644 index 000000000..9ac68d20e --- /dev/null +++ b/apps/accounts/automations/push_account/host/posix/main.yml @@ -0,0 +1,93 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Test privileged account + ansible.builtin.ping: + + - name: Push user + ansible.builtin.user: + name: "{{ account.username }}" + shell: "{{ params.shell }}" + home: "{{ '/home/' + account.username }}" + groups: "{{ params.groups }}" + expires: -1 + state: present + + - name: "Add {{ account.username }} group" + ansible.builtin.group: + name: "{{ account.username }}" + state: present + + - name: Check home dir exists + ansible.builtin.stat: + path: "{{ '/home/' + account.username }}" + register: home_existed + + - name: Set home dir permission + ansible.builtin.file: + path: "{{ '/home/' + account.username }}" + owner: "{{ account.username }}" + group: "{{ account.username }}" + mode: "0700" + when: + - home_existed.stat.exists == true + + - name: Add user groups + ansible.builtin.user: + name: "{{ account.username }}" + groups: "{{ params.groups }}" + when: params.groups + + - name: Push user password + ansible.builtin.user: + name: "{{ account.username }}" + password: "{{ account.secret | password_hash('sha512') }}" + update_password: always + when: account.secret_type == "password" + + - name: remove jumpserver ssh key + ansible.builtin.lineinfile: + dest: "{{ ssh_params.dest }}" + regexp: "{{ ssh_params.regexp }}" + state: absent + when: + - account.secret_type == "ssh_key" + - ssh_params.strategy == "set_jms" + + - name: Push SSH key + ansible.builtin.authorized_key: + user: "{{ account.username }}" + key: "{{ account.secret }}" + exclusive: "{{ ssh_params.exclusive }}" + when: account.secret_type == "ssh_key" + + - name: Set sudo setting + ansible.builtin.lineinfile: + dest: /etc/sudoers + state: present + regexp: "^{{ account.username }} ALL=" + line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}" + validate: visudo -cf %s + when: + - params.sudo + + - name: Refresh connection + ansible.builtin.meta: reset_connection + + - name: Verify password + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + ansible_become: no + when: account.secret_type == "password" + + - name: Verify SSH key + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no + when: account.secret_type == "ssh_key" diff --git a/apps/accounts/automations/push_account/host/posix/manifest.yml b/apps/accounts/automations/push_account/host/posix/manifest.yml new file mode 100644 index 000000000..382b48add --- /dev/null +++ b/apps/accounts/automations/push_account/host/posix/manifest.yml @@ -0,0 +1,25 @@ +id: push_account_posix +name: Push account for posix +category: host +type: + - unix + - linux +method: push_account +params: + - name: sudo + type: str + label: 'Sudo' + default: '/bin/whoami' + help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + + - name: shell + type: str + label: 'Shell' + default: '/bin/bash' + help_text: '' + + - name: groups + type: str + label: '用户组' + default: '' + help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' diff --git a/apps/accounts/automations/push_account/host/windows/main.yml b/apps/accounts/automations/push_account/host/windows/main.yml new file mode 100644 index 000000000..8a2a0aef0 --- /dev/null +++ b/apps/accounts/automations/push_account/host/windows/main.yml @@ -0,0 +1,30 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Test privileged account + ansible.windows.win_ping: + +# - name: Print variables +# debug: +# msg: "Username: {{ account.username }}, Password: {{ account.secret }}" + + - name: Push user password + ansible.windows.win_user: + fullname: "{{ account.username}}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + password_never_expires: yes + groups: "{{ params.groups }}" + groups_action: add + update_password: always + when: account.secret_type == "password" + + - name: Refresh connection + ansible.builtin.meta: reset_connection + + - name: Verify password + ansible.windows.win_ping: + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + when: account.secret_type == "password" diff --git a/apps/accounts/automations/push_account/host/windows/manifest.yml b/apps/accounts/automations/push_account/host/windows/manifest.yml new file mode 100644 index 000000000..05e3127f9 --- /dev/null +++ b/apps/accounts/automations/push_account/host/windows/manifest.yml @@ -0,0 +1,13 @@ +id: push_account_local_windows +name: Push account local account for Windows +version: 1 +method: push_account +category: host +type: + - windows +params: + - name: groups + type: str + label: '用户组' + default: 'Users,Remote Desktop Users' + help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py index 92fde0b37..fe117f018 100644 --- a/apps/accounts/automations/push_account/manager.py +++ b/apps/accounts/automations/push_account/manager.py @@ -31,6 +31,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): print(msg) return inventory_hosts + host['ssh_params'] = {} for account in accounts: h = deepcopy(host) secret_type = account.secret_type @@ -49,7 +50,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): 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, secret_type) + h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type)) h['account'] = { 'name': account.name, 'username': account.username, diff --git a/apps/accounts/const/account.py b/apps/accounts/const/account.py index 109044934..55fa02d80 100644 --- a/apps/accounts/const/account.py +++ b/apps/accounts/const/account.py @@ -18,3 +18,10 @@ class AliasAccount(TextChoices): class Source(TextChoices): LOCAL = 'local', _('Local') COLLECTED = 'collected', _('Collected') + TEMPLATE = 'template', _('Template') + + +class AccountInvalidPolicy(TextChoices): + SKIP = 'skip', _('Skip') + UPDATE = 'update', _('Update') + ERROR = 'error', _('Failed') diff --git a/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py b/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py new file mode 100644 index 000000000..058a7dd7e --- /dev/null +++ b/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-03-23 08:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_account_usernames_to_ids'), + ] + + operations = [ + migrations.AddField( + model_name='gatheraccountsautomation', + name='is_sync_account', + field=models.BooleanField(blank=True, default=False, verbose_name='Is sync account'), + ), + ] diff --git a/apps/accounts/migrations/0011_account_source_id.py b/apps/accounts/migrations/0011_account_source_id.py new file mode 100644 index 000000000..ff9734404 --- /dev/null +++ b/apps/accounts/migrations/0011_account_source_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-03-23 07:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_gatheraccountsautomation_is_sync_account'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='source_id', + field=models.CharField(max_length=128, null=True, blank=True, verbose_name='Source ID'), + ), + ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 008318c7e..4094018e1 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount): version = models.IntegerField(default=0, verbose_name=_('Version')) history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version']) source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source')) + source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID')) class Meta: verbose_name = _('Account') diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index 01f903141..dd9b5c862 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -1,7 +1,9 @@ from django.db import models +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from accounts.const import AutomationTypes +from accounts.const import AutomationTypes, Source +from accounts.models import Account from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation @@ -19,6 +21,25 @@ class GatheredAccount(JMSOrgBaseModel): def address(self): return self.asset.address + @staticmethod + def sync_accounts(gathered_accounts): + account_objs = [] + for gathered_account in gathered_accounts: + asset_id = gathered_account.asset_id + username = gathered_account.username + accounts = Account.objects.filter( + Q(asset_id=asset_id, username=username) | + Q(asset_id=asset_id, name=username) + ) + if accounts.exists(): + continue + account = Account( + asset_id=asset_id, username=username, + name=username, source=Source.COLLECTED + ) + account_objs.append(account) + Account.objects.bulk_create(account_objs) + class Meta: verbose_name = _('Gather account automation') unique_together = [ @@ -31,6 +52,17 @@ class GatheredAccount(JMSOrgBaseModel): class GatherAccountsAutomation(AccountBaseAutomation): + is_sync_account = models.BooleanField( + default=False, blank=True, verbose_name=_("Is sync account") + ) + + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'is_sync_account': self.is_sync_account, + }) + return attr_json + def save(self, *args, **kwargs): self.type = AutomationTypes.gather_accounts super().save(*args, **kwargs) diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py index f189a5fbd..d3a14411f 100644 --- a/apps/accounts/models/automations/push_account.py +++ b/apps/accounts/models/automations/push_account.py @@ -51,7 +51,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ - 'username': self.username + 'username': self.username, + 'params': self.params, }) return attr_json diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index 5b7bbdbce..b4c5076cc 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -1,75 +1,172 @@ +import uuid + +from django.db import IntegrityError +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator -from accounts.const import SecretType, Source +from accounts.const import SecretType, Source, AccountInvalidPolicy from accounts.models import Account, AccountTemplate from accounts.tasks import push_accounts_to_assets_task from assets.const import Category, AllTypes from assets.models import Asset -from common.serializers import SecretReadableMixin, BulkModelSerializer +from common.serializers import SecretReadableMixin from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.utils import get_logger from .base import BaseAccountSerializer +logger = get_logger(__name__) -class AccountSerializerCreateValidateMixin: - from_id: str - template: bool - push_now: bool - replace_attrs: callable - def to_internal_value(self, data): - from_id = data.pop('id', None) - ret = super().to_internal_value(data) - self.from_id = from_id - return ret +class AccountCreateUpdateSerializerMixin(serializers.Serializer): + template = serializers.PrimaryKeyRelatedField( + queryset=AccountTemplate.objects, + required=False, label=_("Template"), write_only=True + ) + push_now = serializers.BooleanField( + default=False, label=_("Push now"), write_only=True + ) + params = serializers.JSONField( + decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'} + ) + on_invalid = LabeledChoiceField( + choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR, + write_only=True, label=_('Exist policy') + ) - def set_secret(self, attrs): - _id = self.from_id - template = attrs.pop('template', None) + class Meta: + fields = ['template', 'push_now', 'params', 'on_invalid'] - if _id and template: - account_template = AccountTemplate.objects.get(id=_id) - attrs['secret'] = account_template.secret - elif _id and not template: - account = Account.objects.get(id=_id) - attrs['secret'] = account.secret - return attrs + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_initial_value() + + def set_initial_value(self): + if not getattr(self, 'initial_data', None): + return + if isinstance(self.initial_data, dict): + initial_data = [self.initial_data] + else: + initial_data = self.initial_data + + for data in initial_data: + if not data.get('asset') and not self.instance: + raise serializers.ValidationError({'asset': 'Asset is required'}) + asset = data.get('asset') or self.instance.asset + self.from_template_if_need(data) + self.set_uniq_name_if_need(data, asset) + + def set_uniq_name_if_need(self, initial_data, asset): + name = initial_data.get('name') + if name is None: + return + if not name: + name = initial_data.get('username') + if self.instance and self.instance.name == name: + return + if Account.objects.filter(name=name, asset=asset).exists(): + name = name + '_' + uuid.uuid4().hex[:4] + initial_data['name'] = name + + @staticmethod + def from_template_if_need(initial_data): + template_id = initial_data.get('template') + if not template_id: + return + if isinstance(template_id, (str, uuid.UUID)): + template = AccountTemplate.objects.filter(id=template_id).first() + else: + template = template_id + if not template: + raise serializers.ValidationError({'template': 'Template not found'}) + + # Set initial data from template + ignore_fields = ['id', 'date_created', 'date_updated', 'org_id'] + field_names = [ + field.name for field in template._meta.fields + if field.name not in ignore_fields + ] + attrs = {} + for name in field_names: + value = getattr(template, name, None) + if value is None: + continue + attrs[name] = value + initial_data.update(attrs) + + @staticmethod + def push_account_if_need(instance, push_now, params, stat): + if not push_now or stat != 'created': + return + push_accounts_to_assets_task.delay([str(instance.id)], params) + + def get_validators(self): + _validators = super().get_validators() + if getattr(self, 'initial_data', None) is None: + return _validators + on_invalid = self.initial_data.get('on_invalid') + if on_invalid == AccountInvalidPolicy.ERROR: + return _validators + _validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)] + return _validators + + @staticmethod + def do_create(vd): + on_invalid = vd.pop('on_invalid', None) + + q = Q() + if vd.get('name'): + q |= Q(name=vd['name']) + if vd.get('username'): + q |= Q(username=vd['username'], secret_type=vd.get('secret_type')) + + instance = Account.objects.filter(asset=vd['asset']).filter(q).first() + # 不存在这个资产,不用关系策略 + if not instance: + instance = Account.objects.create(**vd) + return instance, 'created' + + if on_invalid == AccountInvalidPolicy.SKIP: + return instance, 'skipped' + elif on_invalid == AccountInvalidPolicy.UPDATE: + for k, v in vd.items(): + setattr(instance, k, v) + instance.save() + return instance, 'updated' + else: + raise serializers.ValidationError('Account already exists') def validate(self, attrs): attrs = super().validate(attrs) - return self.set_secret(attrs) + if self.instance: + return attrs - @staticmethod - def push_account(instance, push_now): - if not push_now: - return - push_accounts_to_assets_task.delay([str(instance.id)]) + template = attrs.pop('template', None) + if template: + attrs['source'] = Source.TEMPLATE + attrs['source_id'] = str(template.id) + return attrs def create(self, validated_data): push_now = validated_data.pop('push_now', None) - instance = super().create(validated_data) - self.push_account(instance, push_now) + params = validated_data.pop('params', None) + instance, stat = self.do_create(validated_data) + self.push_account_if_need(instance, push_now, params, stat) return instance def update(self, instance, validated_data): # account cannot be modified validated_data.pop('username', None) + validated_data.pop('on_invalid', None) push_now = validated_data.pop('push_now', None) + params = validated_data.pop('params', None) + validated_data['source_id'] = None instance = super().update(instance, validated_data) - self.push_account(instance, push_now) + self.push_account_if_need(instance, push_now, params, 'updated') return instance -class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer): - template = serializers.BooleanField( - default=False, label=_("Template"), write_only=True - ) - push_now = serializers.BooleanField( - default=False, label=_("Push now"), write_only=True - ) - has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) - - class AccountAssetSerializer(serializers.ModelSerializer): platform = ObjectRelatedField(read_only=True) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) @@ -77,11 +174,11 @@ class AccountAssetSerializer(serializers.ModelSerializer): class Meta: model = Asset - fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info'] + fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config'] def to_internal_value(self, data): if isinstance(data, dict): - i = data.get('id') + i = data.get('id') or data.get('pk') else: i = data @@ -91,9 +188,10 @@ class AccountAssetSerializer(serializers.ModelSerializer): raise serializers.ValidationError(_('Asset not found')) -class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): +class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer): asset = AccountAssetSerializer(label=_('Asset')) source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True) + has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) su_from = ObjectRelatedField( required=False, queryset=Account.objects, allow_null=True, allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username') @@ -102,27 +200,179 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): class Meta(BaseAccountSerializer.Meta): model = Account fields = BaseAccountSerializer.Meta.fields + [ - 'su_from', 'asset', 'template', 'version', - 'push_now', 'source', 'connectivity', + 'su_from', 'asset', 'version', + 'source', 'source_id', 'connectivity', + ] + AccountCreateUpdateSerializerMixin.Meta.fields + read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [ + 'source', 'source_id', 'connectivity' ] extra_kwargs = { **BaseAccountSerializer.Meta.extra_kwargs, - 'name': {'required': False, 'allow_null': True}, + 'name': {'required': False}, } - def validate_name(self, value): - if not value: - value = self.initial_data.get('username') - return value - @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset \ - .prefetch_related('asset', 'asset__platform', 'asset__platform__automation') + queryset = queryset.prefetch_related( + 'asset', 'asset__platform', + 'asset__platform__automation' + ) return queryset +class AssetAccountBulkSerializerResultSerializer(serializers.Serializer): + asset = serializers.CharField(read_only=True, label=_('Asset')) + state = serializers.CharField(read_only=True, label=_('State')) + error = serializers.CharField(read_only=True, label=_('Error')) + changed = serializers.BooleanField(read_only=True, label=_('Changed')) + + +class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer): + assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets')) + + class Meta: + model = Account + fields = [ + 'name', 'username', 'secret', 'secret_type', + 'privileged', 'is_active', 'comment', 'template', + 'on_invalid', 'push_now', 'assets', + ] + extra_kwargs = { + 'name': {'required': False}, + 'secret_type': {'required': False}, + } + + def set_initial_value(self): + if not getattr(self, 'initial_data', None): + return + initial_data = self.initial_data + self.from_template_if_need(initial_data) + + @staticmethod + def get_filter_lookup(vd): + return { + 'username': vd['username'], + 'secret_type': vd['secret_type'], + 'asset': vd['asset'], + } + + @staticmethod + def get_uniq_name(vd): + return vd['name'] + '-' + uuid.uuid4().hex[:4] + + @staticmethod + def _handle_update_create(vd, lookup): + ori = Account.objects.filter(**lookup).first() + if ori and ori.secret == vd['secret']: + return ori, False, 'skipped' + + instance, value = Account.objects.update_or_create(defaults=vd, **lookup) + state = 'created' if value else 'updated' + return instance, True, state + + @staticmethod + def _handle_skip_create(vd, lookup): + instance, value = Account.objects.get_or_create(defaults=vd, **lookup) + state = 'created' if value else 'skipped' + return instance, value, state + + @staticmethod + def _handle_err_create(vd, lookup): + instance, value = Account.objects.get_or_create(defaults=vd, **lookup) + if not value: + raise serializers.ValidationError(_('Account already exists')) + return instance, True, 'created' + + def perform_create(self, vd, handler): + lookup = self.get_filter_lookup(vd) + try: + instance, changed, state = handler(vd, lookup) + except IntegrityError: + vd['name'] = self.get_uniq_name(vd) + instance, changed, state = handler(vd, lookup) + return instance, changed, state + + def get_create_handler(self, on_invalid): + if on_invalid == 'update': + handler = self._handle_update_create + elif on_invalid == 'skip': + handler = self._handle_skip_create + else: + handler = self._handle_err_create + return handler + + def perform_bulk_create(self, vd): + assets = vd.pop('assets') + on_invalid = vd.pop('on_invalid', 'skip') + secret_type = vd.get('secret_type', 'password') + + if not vd.get('name'): + vd['name'] = vd.get('username') + + create_handler = self.get_create_handler(on_invalid) + asset_ids = [asset.id for asset in assets] + secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type) + + _results = {} + for asset in assets: + if asset not in secret_type_supports: + _results[asset] = { + 'error': _('Asset does not support this secret type: %s') % secret_type, + 'state': 'error', + } + continue + + vd = vd.copy() + vd['asset'] = asset + try: + instance, changed, state = self.perform_create(vd, create_handler) + _results[asset] = { + 'changed': changed, 'instance': instance.id, 'state': state + } + except serializers.ValidationError as e: + _results[asset] = {'error': e.detail[0], 'state': 'error'} + except Exception as e: + logger.exception(e) + _results[asset] = {'error': str(e), 'state': 'error'} + + results = [{'asset': asset, **result} for asset, result in _results.items()] + state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0} + results = sorted(results, key=lambda x: state_score.get(x['state'], 4)) + + if on_invalid != 'error': + return results + + errors = [] + errors.extend([result for result in results if result['state'] == 'error']) + for result in results: + if result['state'] != 'skipped': + continue + errors.append({ + 'error': _('Account has exist'), + 'state': 'error', + 'asset': str(result['asset']) + }) + if errors: + raise serializers.ValidationError(errors) + return results + + @staticmethod + def push_accounts_if_need(results, push_now): + if not push_now: + return + accounts = [str(v['instance']) for v in results if v.get('instance')] + push_accounts_to_assets_task.delay(accounts) + + def create(self, validated_data): + push_now = validated_data.pop('push_now', False) + results = self.perform_bulk_create(validated_data) + self.push_accounts_if_need(results, push_now) + for res in results: + res['asset'] = str(res['asset']) + return results + + class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): class Meta(AccountSerializer.Meta): extra_kwargs = { diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py index f31f19196..4e2b1a1df 100644 --- a/apps/accounts/serializers/account/base.py +++ b/apps/accounts/serializers/account/base.py @@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer'] class AuthValidateMixin(serializers.Serializer): secret_type = LabeledChoiceField( - choices=SecretType.choices, required=True, label=_('Secret type') + choices=SecretType.choices, label=_('Secret type'), default='password' ) secret = EncryptedField( - label=_('Secret/Password'), required=False, max_length=40960, allow_blank=True, + label=_('Secret'), required=False, max_length=40960, allow_blank=True, allow_null=True, write_only=True, ) passphrase = serializers.CharField( @@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): 'date_verified', 'created_by', 'date_created', ] extra_kwargs = { - 'name': {'required': True}, 'spec_info': {'label': _('Spec info')}, } diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index a72565cf5..45190f051 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,4 +1,5 @@ -from accounts.models import AccountTemplate +from accounts.models import AccountTemplate, Account +from assets.models import Asset from common.serializers import SecretReadableMixin from .base import BaseAccountSerializer @@ -7,17 +8,47 @@ class AccountTemplateSerializer(BaseAccountSerializer): class Meta(BaseAccountSerializer.Meta): model = AccountTemplate - # @classmethod - # def validate_required(cls, attrs): - # # TODO 选择模版后检查一些必填项 - # required_field_dict = {} - # error = _('This field is required.') - # for k, v in cls().fields.items(): - # if v.required and k not in attrs: - # required_field_dict[k] = error - # if not required_field_dict: - # return - # raise serializers.ValidationError(required_field_dict) + @staticmethod + def bulk_update_accounts(instance, diff): + accounts = Account.objects.filter(source_id=instance.id) + if not accounts: + return + + secret_type = diff.pop('secret_type', None) + diff.pop('secret', None) + update_accounts = [] + for account in accounts: + for field, value in diff.items(): + setattr(account, field, value) + update_accounts.append(account) + if update_accounts: + Account.objects.bulk_update(update_accounts, diff.keys()) + + if secret_type is None: + return + + update_accounts = [] + asset_ids = accounts.values_list('asset_id', flat=True) + secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type) + asset_ids_supports = [asset.id for asset in secret_type_supports] + for account in accounts: + asset_id = account.asset_id + if asset_id not in asset_ids_supports: + continue + account.secret_type = secret_type + account.secret = instance.secret + update_accounts.append(account) + if update_accounts: + Account.objects.bulk_update(update_accounts, ['secret', 'secret_type']) + + def update(self, instance, validated_data): + diff = { + k: v for k, v in validated_data.items() + if getattr(instance, k) != v + } + instance = super().update(instance, validated_data) + self.bulk_update_accounts(instance, diff) + return instance class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer): diff --git a/apps/accounts/serializers/automations/gather_accounts.py b/apps/accounts/serializers/automations/gather_accounts.py index b906e7881..fd09773de 100644 --- a/apps/accounts/serializers/automations/gather_accounts.py +++ b/apps/accounts/serializers/automations/gather_accounts.py @@ -17,7 +17,8 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer): class Meta: model = GatherAccountsAutomation read_only_fields = BaseAutomationSerializer.Meta.read_only_fields - fields = BaseAutomationSerializer.Meta.fields + read_only_fields + fields = BaseAutomationSerializer.Meta.fields \ + + ['is_sync_account'] + read_only_fields extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs diff --git a/apps/accounts/serializers/automations/push_account.py b/apps/accounts/serializers/automations/push_account.py index b9982300b..1d7bb3d36 100644 --- a/apps/accounts/serializers/automations/push_account.py +++ b/apps/accounts/serializers/automations/push_account.py @@ -7,9 +7,10 @@ from .change_secret import ( class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer): + class Meta(ChangeSecretAutomationSerializer.Meta): model = PushAccountAutomation - fields = [ + fields = ['params'] + [ n for n in ChangeSecretAutomationSerializer.Meta.fields if n not in ['recipients'] ] diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py index b47588192..cf09842cc 100644 --- a/apps/accounts/signal_handlers.py +++ b/apps/accounts/signal_handlers.py @@ -8,8 +8,8 @@ logger = get_logger(__name__) @receiver(pre_save, sender=Account) -def on_account_pre_save(sender, instance, created=False, **kwargs): - if created: +def on_account_pre_save(sender, instance, **kwargs): + if instance.version == 0: instance.version = 1 else: instance.version = instance.history.count() diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py index 2a753cc1a..623481be9 100644 --- a/apps/accounts/tasks/push_account.py +++ b/apps/accounts/tasks/push_account.py @@ -15,7 +15,7 @@ __all__ = [ queue="ansible", verbose_name=_('Push accounts to assets'), activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None) ) -def push_accounts_to_assets_task(account_ids): +def push_accounts_to_assets_task(account_ids, params=None): from accounts.models import PushAccountAutomation from accounts.models import Account @@ -26,6 +26,7 @@ def push_accounts_to_assets_task(account_ids): task_snapshot = { 'accounts': [str(account.id) for account in accounts], 'assets': [str(account.asset_id) for account in accounts], + 'params': params or {}, } tp = AutomationTypes.push_account diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 59a70b3cf..5c57ad67b 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record') urlpatterns = [ + path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'), path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'), path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'), diff --git a/apps/assets/api/asset/__init__.py b/apps/assets/api/asset/__init__.py index c20e44573..0f1d81825 100644 --- a/apps/assets/api/asset/__init__.py +++ b/apps/assets/api/asset/__init__.py @@ -1,7 +1,8 @@ from .asset import * -from .host import * -from .database import * -from .web import * from .cloud import * +from .custom import * +from .database import * from .device import * +from .host import * from .permission import * +from .web import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index c917e09d9..aa20bded9 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -2,15 +2,17 @@ # import django_filters from django.db.models import Q +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task from assets import serializers from assets.exceptions import NotSupportedTemporarilyError from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend -from assets.models import Asset, Gateway +from assets.models import Asset, Gateway, Platform from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual from common.api import SuggestionMixin from common.drf.filters import BaseFilterSet @@ -18,6 +20,7 @@ from common.utils import get_logger, is_uuid from orgs.mixins import generics from orgs.mixins.api import OrgBulkModelViewSet from ..mixin import NodeFilterMixin +from ...notifications import BulkUpdatePlatformSkipAssetUserMsg logger = get_logger(__file__) __all__ = [ @@ -99,16 +102,16 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ("platform", serializers.PlatformSerializer), ("suggestion", serializers.MiniAssetSerializer), ("gateways", serializers.GatewaySerializer), - ("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"), + ("gathered_info", "assets.view_asset"), ) extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] + skip_assets = [] def get_serializer_class(self): cls = super().get_serializer_class() @@ -124,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): serializer = super().get_serializer(instance=asset.platform) return Response(serializer.data) - @action(methods=["GET"], detail=True, url_path="spec-info") - def spec_info(self, *args, **kwargs): - asset = super().get_object() - return Response(asset.spec_info) - @action(methods=["GET"], detail=True, url_path="gateways") def gateways(self, *args, **kwargs): asset = self.get_object() @@ -144,6 +142,31 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): return Response({'error': error}, status=400) return super().create(request, *args, **kwargs) + def filter_bulk_update_data(self): + bulk_data = [] + for data in self.request.data: + pk = data.get('id') + platform = data.get('platform') + if not platform: + bulk_data.append(data) + continue + asset = get_object_or_404(Asset, pk=pk) + platform = get_object_or_404(Platform, **platform) + if platform.type == asset.type: + bulk_data.append(data) + continue + self.skip_assets.append(asset) + return bulk_data + + def bulk_update(self, request, *args, **kwargs): + bulk_data = self.filter_bulk_update_data() + request._full_data = bulk_data + response = super().bulk_update(request, *args, **kwargs) + if response.status_code == HTTP_200_OK and self.skip_assets: + user = request.user + BulkUpdatePlatformSkipAssetUserMsg(user, self.skip_assets).publish() + return response + class AssetsTaskMixin: def perform_assets_task(self, serializer): @@ -154,8 +177,8 @@ class AssetsTaskMixin: task = update_assets_hardware_info_manual(assets) else: asset = assets[0] - if not asset.auto_info['ansible_enabled'] or \ - not asset.auto_info['ping_enabled']: + if not asset.auto_config['ansible_enabled'] or \ + not asset.auto_config['ping_enabled']: raise NotSupportedTemporarilyError() task = test_assets_connectivity_manual(assets) return task diff --git a/apps/assets/api/asset/custom.py b/apps/assets/api/asset/custom.py new file mode 100644 index 000000000..ca5058ed6 --- /dev/null +++ b/apps/assets/api/asset/custom.py @@ -0,0 +1,16 @@ +from assets.models import Custom, Asset +from assets.serializers import CustomSerializer + +from .asset import AssetViewSet + +__all__ = ['CustomViewSet'] + + +class CustomViewSet(AssetViewSet): + model = Custom + perm_model = Asset + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = CustomSerializer + return serializer_classes diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py index b92448bfb..d2ddc954d 100644 --- a/apps/assets/api/asset/host.py +++ b/apps/assets/api/asset/host.py @@ -1,8 +1,5 @@ -from rest_framework.decorators import action -from rest_framework.response import Response - from assets.models import Host, Asset -from assets.serializers import HostSerializer, HostInfoSerializer +from assets.serializers import HostSerializer from .asset import AssetViewSet __all__ = ['HostViewSet'] @@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet): def get_serializer_classes(self): serializer_classes = super().get_serializer_classes() serializer_classes['default'] = HostSerializer - serializer_classes['info'] = HostInfoSerializer return serializer_classes - - @action(methods=["GET"], detail=True, url_path="info") - def info(self, *args, **kwargs): - asset = super().get_object() - 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/api/mixin.py b/apps/assets/api/mixin.py index 6cc198169..d7cb90121 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -8,6 +8,15 @@ from common.utils import lazyproperty, timeit class SerializeToTreeNodeMixin: + request: Request + + @lazyproperty + def is_sync(self): + sync_paths = ['/api/v1/perms/users/self/nodes/all-with-assets/tree/'] + for p in sync_paths: + if p == self.request.path: + return True + return False @timeit def serialize_nodes(self, nodes: List[Node], with_asset_amount=False): @@ -17,6 +26,16 @@ class SerializeToTreeNodeMixin: else: def _name(node: Node): return node.value + + def _open(node): + if not self.is_sync: + # 异步加载资产树时,默认展开节点 + return True + if not node.parent_key: + return True + else: + return False + data = [ { 'id': node.key, @@ -24,7 +43,7 @@ class SerializeToTreeNodeMixin: 'title': _name(node), 'pId': node.parent_key, 'isParent': True, - 'open': True, + 'open': _open(node), 'meta': { 'data': { "id": node.id, @@ -52,7 +71,7 @@ class SerializeToTreeNodeMixin: { 'id': str(asset.id), 'name': asset.name, - 'title': asset.address, + 'title': f'{asset.address}\n{asset.comment}', 'pId': get_pid(asset), 'isParent': False, 'open': False, @@ -64,6 +83,8 @@ class SerializeToTreeNodeMixin: 'platform_type': asset.platform.type, 'org_name': asset.org_name, 'sftp': asset.platform_id in sftp_enabled_platform, + 'name': asset.name, + 'address': asset.address }, } } diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py index be9456ff7..61f4db985 100644 --- a/apps/assets/api/platform.py +++ b/apps/assets/api/platform.py @@ -1,10 +1,16 @@ +from rest_framework import generics +from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.response import Response + from assets.const import AllTypes -from assets.models import Platform +from assets.models import Platform, Node, Asset from assets.serializers import PlatformSerializer from common.api import JMSModelViewSet +from common.permissions import IsValidUser from common.serializers import GroupedChoiceSerializer -__all__ = ['AssetPlatformViewSet'] +__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi'] class AssetPlatformViewSet(JMSModelViewSet): @@ -18,12 +24,13 @@ class AssetPlatformViewSet(JMSModelViewSet): rbac_perms = { 'categories': 'assets.view_platform', 'type_constraints': 'assets.view_platform', - 'ops_methods': 'assets.view_platform' + 'ops_methods': 'assets.view_platform', + 'filter_nodes_assets': 'assets.view_platform' } def get_queryset(self): queryset = super().get_queryset() - queryset = queryset.filter(type__in=AllTypes.get_types()) + queryset = queryset.filter(type__in=AllTypes.get_types_values()) return queryset def get_object(self): @@ -38,3 +45,44 @@ class AssetPlatformViewSet(JMSModelViewSet): request, message={"detail": "Internal platform"} ) return super().check_object_permissions(request, obj) + + @action(methods=['post'], detail=False, url_path='filter-nodes-assets') + def filter_nodes_assets(self, request, *args, **kwargs): + node_ids = request.data.get('node_ids', []) + asset_ids = request.data.get('asset_ids', []) + nodes = Node.objects.filter(id__in=node_ids) + node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) + direct_asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True) + platform_ids = Asset.objects.filter( + id__in=set(list(direct_asset_ids) + list(node_asset_ids)) + ).values_list('platform_id', flat=True) + platforms = Platform.objects.filter(id__in=platform_ids) + serializer = self.get_serializer(platforms, many=True) + return Response(serializer.data) + + +class PlatformAutomationMethodsApi(generics.ListAPIView): + permission_classes = (IsValidUser,) + + @staticmethod + def automation_methods(): + return AllTypes.get_automation_methods() + + def generate_serializer_fields(self): + data = self.automation_methods() + fields = { + i['id']: i['params_serializer']() + if i['params_serializer'] else None + for i in data + } + return fields + + def get_serializer_class(self): + fields = self.generate_serializer_fields() + serializer_name = 'AutomationMethodsSerializer' + return type(serializer_name, (serializers.Serializer,), fields) + + def list(self, request, *args, **kwargs): + data = self.generate_serializer_fields() + serializer = self.get_serializer(data) + return Response(serializer.data) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 7184d91f7..9207bb33e 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -41,6 +41,26 @@ class BasePlaybookManager: self.method_hosts_mapper = defaultdict(list) self.playbooks = [] self.gateway_servers = dict() + params = self.execution.snapshot.get('params') + self.params = params or {} + + def get_params(self, automation, method_type): + method_attr = '{}_method'.format(method_type) + method_params = '{}_params'.format(method_type) + method_id = getattr(automation, method_attr) + automation_params = getattr(automation, method_params) + serializer = self.method_id_meta_mapper[method_id]['params_serializer'] + + if serializer is None: + return {} + + data = self.params.get(method_id, {}) + params = serializer(data).data + return { + field_name: automation_params.get(field_name, '') + if not params[field_name] else params[field_name] + for field_name in params + } @property def platform_automation_methods(self): @@ -101,8 +121,9 @@ class BasePlaybookManager: return host def host_callback(self, host, automation=None, **kwargs): - enabled_attr = '{}_enabled'.format(self.__class__.method_type()) - method_attr = '{}_method'.format(self.__class__.method_type()) + method_type = self.__class__.method_type() + enabled_attr = '{}_enabled'.format(method_type) + method_attr = '{}_method'.format(method_type) method_enabled = automation and \ getattr(automation, enabled_attr) and \ @@ -114,6 +135,7 @@ class BasePlaybookManager: return host host = self.convert_cert_to_file(host, kwargs.get('path_dir')) + host['params'] = self.get_params(automation, method_type) return host @staticmethod @@ -239,10 +261,12 @@ class BasePlaybookManager: jms_asset, jms_gateway = host['jms_asset'], host.get('gateway') if not jms_gateway: continue + server = SSHTunnelForwarder( (jms_gateway['address'], jms_gateway['port']), ssh_username=jms_gateway['username'], ssh_password=jms_gateway['secret'], + ssh_pkey=jms_gateway['private_key_path'], remote_bind_address=(jms_asset['address'], jms_asset['port']) ) try: @@ -252,8 +276,8 @@ class BasePlaybookManager: print('\033[31m %s \033[0m\n' % err_msg) not_valid.append(k) else: - jms_asset['address'] = '127.0.0.1' - jms_asset['port'] = server.local_bind_port + host['ansible_host'] = jms_asset['address'] = '127.0.0.1' + host['ansible_port'] = jms_asset['port'] = server.local_bind_port servers.append(server) # 网域不可连接的,就不继续执行此资源的后续任务了 diff --git a/apps/assets/automations/gather_facts/manager.py b/apps/assets/automations/gather_facts/manager.py index afd5ce523..e33d97617 100644 --- a/apps/assets/automations/gather_facts/manager.py +++ b/apps/assets/automations/gather_facts/manager.py @@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager): asset = self.host_asset_mapper.get(host) if asset and info: info = self.format_asset_info(asset.type, info) - asset.info = info - asset.save(update_fields=['info']) + asset.gathered_info = info + asset.save(update_fields=['gathered_info']) else: logger.error("Not found info: {}".format(host)) diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py index 86fcb775c..cad2b1b3c 100644 --- a/apps/assets/automations/methods.py +++ b/apps/assets/automations/methods.py @@ -1,8 +1,9 @@ -import os -import yaml import json +import os from functools import partial +import yaml + def check_platform_method(manifest, manifest_path): required_keys = ['category', 'method', 'name', 'id', 'type'] @@ -21,6 +22,15 @@ def check_platform_methods(methods): raise ValueError("Duplicate id: {}".format(_id)) +def generate_serializer(data): + from common.serializers import create_serializer_class + params = data.pop('params', None) + if not params: + return None + serializer_name = data['id'].title().replace('_', '') + 'Serializer' + return create_serializer_class(serializer_name, params) + + def get_platform_automation_methods(path): methods = [] for root, dirs, files in os.walk(path, topdown=False): @@ -33,6 +43,7 @@ def get_platform_automation_methods(path): manifest = yaml.safe_load(f) check_platform_method(manifest, path) manifest['dir'] = os.path.dirname(path) + manifest['params_serializer'] = generate_serializer(manifest) methods.append(manifest) check_platform_methods(methods) @@ -46,12 +57,12 @@ def filter_key(manifest, attr, value): return value in manifest_value or 'all' in manifest_value -def filter_platform_methods(category, tp, method=None, methods=None): +def filter_platform_methods(category, tp_name, method=None, methods=None): methods = platform_automation_methods if methods is None else methods if category: methods = filter(partial(filter_key, attr='category', value=category), methods) - if tp: - methods = filter(partial(filter_key, attr='type', value=tp), methods) + if tp_name: + methods = filter(partial(filter_key, attr='type', value=tp_name), methods) if method: methods = filter(lambda x: x['method'] == method, methods) return methods diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py index aa5070d5e..c90ab7320 100644 --- a/apps/assets/const/base.py +++ b/apps/assets/const/base.py @@ -4,10 +4,21 @@ from jumpserver.utils import has_valid_xpack_license from .protocol import Protocol +class Type: + def __init__(self, label, value): + self.label = label + self.value = value + + def __str__(self): + return self.value + + class BaseType(TextChoices): """ - 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持 + 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, + 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持 """ + @classmethod def get_constrains(cls): constrains = {} @@ -20,7 +31,7 @@ class BaseType(TextChoices): protocols_default = protocols.pop('*', {}) automation_default = automation.pop('*', {}) - for k, v in cls.choices: + for k, v in cls.get_choices(): tp_base = {**base_default, **base.get(k, {})} tp_auto = {**automation_default, **automation.get(k, {})} tp_protocols = {**protocols_default, **protocols.get(k, {})} @@ -35,8 +46,12 @@ class BaseType(TextChoices): choices = protocol.get('choices', []) if choices == '__self__': choices = [tp] - protocols = [{'name': name, **settings.get(name, {})} for name in choices] - protocols[0]['primary'] = True + protocols = [ + {'name': name, **settings.get(name, {})} + for name in choices + ] + if protocols: + protocols[0]['default'] = True return protocols @classmethod @@ -56,23 +71,21 @@ class BaseType(TextChoices): raise NotImplementedError @classmethod - def get_community_types(cls): - raise NotImplementedError + def _get_choices_to_types(cls): + choices = cls.get_choices() + return [Type(label, value) for value, label in choices] @classmethod def get_types(cls): - tps = [tp for tp in cls] + tps = cls._get_choices_to_types() if not has_valid_xpack_license(): tps = cls.get_community_types() return tps + @classmethod + def get_community_types(cls): + return cls._get_choices_to_types() + @classmethod def get_choices(cls): - tps = cls.get_types() - cls_choices = cls.choices - return [ - choice for choice in cls_choices - if choice[0] in tps - ] - - + return cls.choices diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index e14867d69..8c4d387d8 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _ from common.db.models import ChoicesMixin - __all__ = ['Category'] @@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices): DATABASE = 'database', _("Database") CLOUD = 'cloud', _("Cloud service") WEB = 'web', _("Web") + CUSTOM = 'custom', _("Custom type") @classmethod def filter_choices(cls, category): _category = getattr(cls, category.upper(), None) choices = [(_category.value, _category.label)] if _category else cls.choices return choices - - - - diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index 12c4f09dd..02410a6a9 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -15,6 +15,9 @@ class CloudTypes(BaseType): 'charset_enabled': False, 'domain_enabled': False, 'su_enabled': False, + }, + cls.K8S: { + 'domain_enabled': True, } } diff --git a/apps/assets/const/custom.py b/apps/assets/const/custom.py new file mode 100644 index 000000000..47d4e9229 --- /dev/null +++ b/apps/assets/const/custom.py @@ -0,0 +1,60 @@ +from .base import BaseType + + +class CustomTypes(BaseType): + @classmethod + def get_choices(cls): + try: + platforms = list(cls.get_custom_platforms()) + except Exception: + return [] + types = [p.type for p in platforms] + return [(t, t) for t in types] + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': False, + 'su_enabled': False, + }, + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'ansible_enabled': False, + 'ansible_config': {}, + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + constrains = {} + for platform in cls.get_custom_platforms(): + choices = list(platform.protocols.values_list('name', flat=True)) + if platform.type in constrains: + choices = constrains[platform.type]['choices'] + choices + constrains[platform.type] = {'choices': choices} + return constrains + + @classmethod + def internal_platforms(cls): + return { + # cls.PUBLIC: [], + # cls.PRIVATE: [{'name': 'Vmware-vSphere'}], + # cls.K8S: [{'name': 'Kubernetes'}], + } + + @classmethod + def get_custom_platforms(cls): + from assets.models import Platform + return Platform.objects.filter(category='custom') diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index d00996dd6..5e8f8f879 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -15,7 +15,8 @@ class DeviceTypes(BaseType): '*': { 'charset_enabled': False, 'domain_enabled': True, - 'su_enabled': False, + 'su_enabled': True, + 'su_methods': ['enable', 'super', 'super_level'] } } diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index eb7930b09..afb92a447 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -19,10 +19,7 @@ class HostTypes(BaseType): 'charset': 'utf-8', # default 'domain_enabled': True, 'su_enabled': True, - 'su_methods': [ - {'name': 'sudo su', 'id': 'sudo su'}, - {'name': 'su -', 'id': 'su -'} - ], + 'su_methods': ['sudo', 'su'], }, cls.WINDOWS: { 'su_enabled': False, @@ -39,7 +36,7 @@ class HostTypes(BaseType): 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] }, cls.WINDOWS: { - 'choices': ['rdp', 'ssh', 'vnc'] + 'choices': ['rdp', 'ssh', 'vnc', 'winrm'] } } @@ -61,7 +58,7 @@ class HostTypes(BaseType): cls.WINDOWS: { 'ansible_config': { 'ansible_shell_type': 'cmd', - 'ansible_connection': 'ssh', + 'ansible_connection': 'smart', }, }, cls.OTHER_HOST: { diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index 884a53785..a0b7b7ec7 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices): rdp = 'rdp', 'RDP' telnet = 'telnet', 'Telnet' vnc = 'vnc', 'VNC' + winrm = 'winrm', 'WinRM' mysql = 'mysql', 'MySQL' mariadb = 'mariadb', 'MariaDB' @@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 23, 'secret_types': ['password'], }, + cls.winrm: { + 'port': 5985, + 'secret_types': ['password'], + 'setting': { + 'use_ssl': False, + } + }, } @classmethod @@ -116,7 +124,7 @@ class Protocol(ChoicesMixin, models.TextChoices): 'setting': { 'username_selector': 'name=username', 'password_selector': 'name=password', - 'submit_selector': 'id=longin_button', + 'submit_selector': 'id=login_button', } }, } @@ -128,3 +136,11 @@ class Protocol(ChoicesMixin, models.TextChoices): **cls.database_protocols(), **cls.cloud_protocols() } + + @classmethod + def protocol_secret_types(cls): + settings = cls.settings() + return { + protocol: settings[protocol]['secret_types'] or ['password'] + for protocol in cls.settings() + } diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 00a496eee..a73b967b2 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext as _ from common.db.models import ChoicesMixin from .category import Category from .cloud import CloudTypes +from .custom import CustomTypes from .database import DatabaseTypes from .device import DeviceTypes from .host import HostTypes @@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin): choices: list includes = [ HostTypes, DeviceTypes, DatabaseTypes, - CloudTypes, WebTypes, + CloudTypes, WebTypes, CustomTypes ] _category_constrains = {} @@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin): def choices(cls): choices = [] for tp in cls.includes: - choices.extend(tp.choices) + choices.extend(tp.get_choices()) return choices + @classmethod + def get_choices(cls): + return cls.choices() + @classmethod def filter_choices(cls, category): - choices = dict(cls.category_types()).get(category, cls).choices + choices = dict(cls.category_types()).get(category, cls).get_choices() return choices() if callable(choices) else choices @classmethod - def get_constraints(cls, category, tp): + def get_constraints(cls, category, tp_name): + if not isinstance(tp_name, str): + tp_name = tp_name.value + types_cls = dict(cls.category_types()).get(category) if not types_cls: return {} type_constraints = types_cls.get_constrains() - constraints = type_constraints.get(tp, {}) - cls.set_automation_methods(category, tp, constraints) + constraints = type_constraints.get(tp_name, {}) + cls.set_automation_methods(category, tp_name, constraints) return constraints @classmethod @@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin): return asset_methods + account_methods @classmethod - def set_automation_methods(cls, category, tp, constraints): + def set_automation_methods(cls, category, tp_name, constraints): from assets.automations import filter_platform_methods automation = constraints.get('automation', {}) automation_methods = {} @@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin): continue item_name = item.replace('_enabled', '') methods = filter_platform_methods( - category, tp, item_name, methods=platform_automation_methods + category, tp_name, item_name, methods=platform_automation_methods ) methods = [{'name': m['name'], 'id': m['id']} for m in methods] automation_methods[item_name + '_methods'] = methods @@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin): @classmethod def grouped_choices(cls): - grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()] + grouped_types = [(str(ca), tp.get_choices()) for ca, tp in cls.category_types()] return grouped_types @classmethod @@ -136,16 +144,22 @@ class AllTypes(ChoicesMixin): (Category.HOST, HostTypes), (Category.DEVICE, DeviceTypes), (Category.DATABASE, DatabaseTypes), + (Category.CLOUD, CloudTypes), (Category.WEB, WebTypes), - (Category.CLOUD, CloudTypes) + (Category.CUSTOM, CustomTypes), ) @classmethod def get_types(cls): - tps = [] + choices = [] for i in dict(cls.category_types()).values(): - tps.extend(i.get_types()) - return tps + choices.extend(i.get_types()) + return choices + + @classmethod + def get_types_values(cls): + choices = cls.get_types() + return [c.value for c in choices] @staticmethod def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None): diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index cd5732630..87eed5fb0 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -49,7 +49,10 @@ def migrate_asset_accounts(apps, schema_editor): account_values.update(auth_book_auth) auth_infos = [] - username = account_values['username'] + username = account_values.get('username') + if not username: + continue + for attr in auth_attrs: secret = account_values.pop(attr, None) if not secret: diff --git a/apps/assets/migrations/0111_auto_20230321_1633.py b/apps/assets/migrations/0111_auto_20230321_1633.py new file mode 100644 index 000000000..52dbc1971 --- /dev/null +++ b/apps/assets/migrations/0111_auto_20230321_1633.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.17 on 2023-03-21 08:33 + +from django.db import migrations, models + +from assets.const import AllTypes + + +def migrate_platform_charset(apps, schema_editor): + platform_model = apps.get_model('assets', 'Platform') + platform_model.objects.filter(charset='utf8').update(charset='utf-8') + + +def migrate_platform_protocol_primary(apps, schema_editor): + platform_model = apps.get_model('assets', 'Platform') + platforms = platform_model.objects.all() + + for platform in platforms: + p = platform.protocols.first() + if not p: + continue + p.primary = True + p.save() + + +def migrate_internal_platforms(apps, schema_editor): + platform_cls = apps.get_model('assets', 'Platform') + AllTypes.create_or_update_internal_platforms(platform_cls) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0110_auto_20230315_1741'), + ] + + operations = [ + migrations.AddField( + model_name='platformprotocol', + name='primary', + field=models.BooleanField(default=False, verbose_name='Primary'), + ), + migrations.AddField( + model_name='platformprotocol', + name='public', + field=models.BooleanField(default=True, verbose_name='Public'), + ), + migrations.RunPython(migrate_platform_charset), + migrations.RunPython(migrate_platform_protocol_primary), + migrations.RunPython(migrate_internal_platforms), + ] diff --git a/apps/assets/migrations/0112_auto_20230404_1631.py b/apps/assets/migrations/0112_auto_20230404_1631.py new file mode 100644 index 000000000..72285123a --- /dev/null +++ b/apps/assets/migrations/0112_auto_20230404_1631.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.17 on 2023-04-04 08:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0111_auto_20230321_1633'), + ] + + operations = [ + migrations.CreateModel( + name='Custom', + fields=[ + ('asset_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.asset')), + ], + options={ + 'verbose_name': 'Custom asset', + }, + bases=('assets.asset',), + ), + migrations.AddField( + model_name='platform', + name='custom_fields', + field=models.JSONField(default=list, null=True, verbose_name='Custom fields'), + ), + migrations.AddField( + model_name='asset', + name='custom_info', + field=models.JSONField(default=dict, verbose_name='Custom info'), + ), + migrations.AddField( + model_name='asset', + name='gathered_info', + field=models.JSONField(blank=True, default=dict, verbose_name='Gathered info'), + ), + migrations.RemoveField( + model_name='asset', + name='info', + ), + ] diff --git a/apps/assets/migrations/0113_auto_20230411_1814.py b/apps/assets/migrations/0113_auto_20230411_1814.py new file mode 100644 index 000000000..58fd2fa92 --- /dev/null +++ b/apps/assets/migrations/0113_auto_20230411_1814.py @@ -0,0 +1,66 @@ +# Generated by Django 3.2.16 on 2023-04-11 10:14 + +from django.db import migrations, models + +from assets.const import AllTypes + + +def migrate_automation_push_account_params(apps, schema_editor): + platform_automation_model = apps.get_model('assets', 'PlatformAutomation') + platform_automation_methods = AllTypes.get_automation_methods() + methods_id_data_map = { + i['id']: None if i['params_serializer'] is None else i['params_serializer']({}).data + for i in platform_automation_methods + if i['method'] == 'push_account' + } + automation_objs = [] + for automation in platform_automation_model.objects.all(): + push_account_method = automation.push_account_method + if not push_account_method: + continue + value = methods_id_data_map.get(push_account_method) + if value is None: + continue + automation.push_account_params = value + automation_objs.append(automation) + platform_automation_model.objects.bulk_update(automation_objs, ['push_account_params']) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0112_auto_20230404_1631'), + ] + + operations = [ + migrations.AddField( + model_name='platformautomation', + name='change_secret_params', + field=models.JSONField(default=dict, verbose_name='Change secret params'), + ), + migrations.AddField( + model_name='platformautomation', + name='gather_accounts_params', + field=models.JSONField(default=dict, verbose_name='Gather facts params'), + ), + migrations.AddField( + model_name='platformautomation', + name='gather_facts_params', + field=models.JSONField(default=dict, verbose_name='Gather facts params'), + ), + migrations.AddField( + model_name='platformautomation', + name='ping_params', + field=models.JSONField(default=dict, verbose_name='Ping params'), + ), + migrations.AddField( + model_name='platformautomation', + name='push_account_params', + field=models.JSONField(default=dict, verbose_name='Push account params'), + ), + migrations.AddField( + model_name='platformautomation', + name='verify_account_params', + field=models.JSONField(default=dict, verbose_name='Verify account params'), + ), + migrations.RunPython(migrate_automation_push_account_params), + ] diff --git a/apps/assets/migrations/0113_auto_20230411_1917.py b/apps/assets/migrations/0113_auto_20230411_1917.py new file mode 100644 index 000000000..ca41a30ab --- /dev/null +++ b/apps/assets/migrations/0113_auto_20230411_1917.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.17 on 2023-04-11 11:17 + +from django.db import migrations + + +def migrate_device_platform_su_method(apps, schema_editor): + platform_model = apps.get_model('assets', 'Platform') + device_map = { + 'Huawei': 'super', + 'Cisco': 'enable', + 'H3C': 'super_level', + } + platforms = platform_model.objects.filter(name__in=device_map.keys()) + print() + for platform in platforms: + print("Migrate platform su method: {}".format(platform.name)) + if platform.name not in device_map: + continue + platform.su_method = device_map[platform.name] + platform.su_enabled = True + platform.save(update_fields=['su_method', 'su_enabled']) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0112_auto_20230404_1631'), + ] + + operations = [ + migrations.RunPython(migrate_device_platform_su_method) + ] diff --git a/apps/assets/migrations/0114_baseautomation_params.py b/apps/assets/migrations/0114_baseautomation_params.py new file mode 100644 index 000000000..2e6d87f8e --- /dev/null +++ b/apps/assets/migrations/0114_baseautomation_params.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-04-13 10:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0113_auto_20230411_1814'), + ] + + operations = [ + migrations.AddField( + model_name='baseautomation', + name='params', + field=models.JSONField(default=dict, verbose_name='Params'), + ), + ] diff --git a/apps/assets/models/asset/__init__.py b/apps/assets/models/asset/__init__.py index 793df7455..0004bfbb5 100644 --- a/apps/assets/models/asset/__init__.py +++ b/apps/assets/models/asset/__init__.py @@ -1,6 +1,7 @@ +from .cloud import * from .common import * -from .host import * +from .custom import * from .database import * from .device import * +from .host import * from .web import * -from .cloud import * diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 3a38699e8..9c81dd0e9 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -1,12 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # - import json import logging from collections import defaultdict from django.db import models +from django.forms import model_to_dict from django.utils.translation import ugettext_lazy as _ from assets import const @@ -94,6 +94,20 @@ class Protocol(models.Model): def __str__(self): return '{}/{}'.format(self.name, self.port) + @lazyproperty + def asset_platform_protocol(self): + protocols = self.asset.platform.protocols.values('name', 'public', 'setting') + protocols = list(filter(lambda p: p['name'] == self.name, protocols)) + return protocols[0] if len(protocols) > 0 else {} + + @property + def setting(self): + return self.asset_platform_protocol.get('setting', {}) + + @property + def public(self): + return self.asset_platform_protocol.get('public', True) + class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): Category = const.Category @@ -108,7 +122,8 @@ 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) # 资产的一些信息,如 硬件信息 + gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息 + custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict) objects = AssetManager.from_queryset(AssetQuerySet)() @@ -148,20 +163,27 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): return self.get_spec_values(instance, spec_fields) @lazyproperty - def auto_info(self): + def info(self): + info = {} + info.update(self.gathered_info or {}) + info.update(self.custom_info or {}) + info.update(self.spec_info or {}) + return info + + @lazyproperty + def auto_config(self): platform = self.platform automation = self.platform.automation - return { + auto_config = { 'su_enabled': platform.su_enabled, - 'ping_enabled': automation.ping_enabled, 'domain_enabled': platform.domain_enabled, - 'ansible_enabled': automation.ansible_enabled, - 'push_account_enabled': automation.push_account_enabled, - 'gather_facts_enabled': automation.gather_facts_enabled, - 'change_secret_enabled': automation.change_secret_enabled, - 'verify_account_enabled': automation.verify_account_enabled, - 'gather_accounts_enabled': automation.gather_accounts_enabled, + 'ansible_enabled': False } + if not automation: + return auto_config + + auto_config.update(model_to_dict(automation)) + return auto_config def get_target_ip(self): return self.address @@ -191,25 +213,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): names.append(n.name + ':' + n.value) return names - @lazyproperty - def primary_protocol(self): - from assets.const.types import AllTypes - primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type) - protocol = self.protocols.filter(name=primary_protocol_name).first() - return protocol - - @lazyproperty - def protocol(self): - if not self.primary_protocol: - return 'none' - return self.primary_protocol.name - - @lazyproperty - def port(self): - if not self.primary_protocol: - return 0 - return self.primary_protocol.port - @lazyproperty def type(self): return self.platform.type @@ -275,6 +278,22 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): tree_node = TreeNode(**data) return tree_node + @staticmethod + def get_secret_type_assets(asset_ids, secret_type): + assets = Asset.objects.filter(id__in=asset_ids) + asset_protocol = assets.prefetch_related('protocols').values_list('id', 'protocols__name') + protocol_secret_types_map = const.Protocol.protocol_secret_types() + asset_secret_types_mapp = defaultdict(set) + + for asset_id, protocol in asset_protocol: + secret_types = set(protocol_secret_types_map.get(protocol, [])) + asset_secret_types_mapp[asset_id].update(secret_types) + + return [ + asset for asset in assets + if secret_type in asset_secret_types_mapp.get(asset.id, []) + ] + class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset") diff --git a/apps/assets/models/asset/custom.py b/apps/assets/models/asset/custom.py new file mode 100644 index 000000000..c3e4263ba --- /dev/null +++ b/apps/assets/models/asset/custom.py @@ -0,0 +1,8 @@ +from django.utils.translation import gettext_lazy as _ + +from .common import Asset + + +class Custom(Asset): + class Meta: + verbose_name = _("Custom asset") diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 9ddeb5bf2..e41092fbf 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -19,6 +19,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) type = models.CharField(max_length=16, verbose_name=_('Type')) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + params = models.JSONField(default=dict, verbose_name=_("Params")) def __str__(self): return self.name + '@' + str(self.created_by) diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py index d9d4d891b..0fc6626df 100644 --- a/apps/assets/models/gateway.py +++ b/apps/assets/models/gateway.py @@ -2,11 +2,10 @@ # from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.models import OrgManager -from assets.models.platform import Platform from assets.const import GATEWAY_NAME +from assets.models.platform import Platform from common.utils import get_logger, lazyproperty - +from orgs.mixins.models import OrgManager from .asset.host import Host logger = get_logger(__file__) @@ -57,6 +56,14 @@ class Gateway(Host): account = self.select_account return account.password if account else None + @lazyproperty + def port(self): + protocol = self.protocols.filter(name='ssh').first() + if protocol: + return protocol.port + else: + return '22' + @lazyproperty def private_key(self): account = self.select_account diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index 488525ccd..4927ce793 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -10,56 +10,68 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] class PlatformProtocol(models.Model): - SETTING_ATTRS = { - 'console': False, - 'security': 'any,tls,rdp', - 'sftp_enabled': True, - 'sftp_home': '/tmp' - } - default = models.BooleanField(default=False, verbose_name=_('Default')) - required = models.BooleanField(default=False, verbose_name=_('Required')) name = models.CharField(max_length=32, verbose_name=_('Name')) port = models.IntegerField(verbose_name=_('Port')) + primary = models.BooleanField(default=False, verbose_name=_('Primary')) + required = models.BooleanField(default=False, verbose_name=_('Required')) + default = models.BooleanField(default=False, verbose_name=_('Default')) + public = models.BooleanField(default=True, verbose_name=_('Public')) setting = models.JSONField(verbose_name=_('Setting'), default=dict) platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols') def __str__(self): return '{}/{}'.format(self.name, self.port) - @property - def primary(self): - primary_protocol_name = AllTypes.get_primary_protocol_name( - self.platform.category, self.platform.type - ) - return self.name == primary_protocol_name - @property def secret_types(self): - return Protocol.settings().get(self.name, {}).get('secret_types') + return Protocol.settings().get(self.name, {}).get('secret_types', ['password']) + + def set_public(self): + private_protocol_set = ('winrm',) + self.public = self.name not in private_protocol_set + + def save(self, **kwargs): + self.set_public() + return super().save(**kwargs) class PlatformAutomation(models.Model): ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config")) + ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled")) ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method")) + ping_params = models.JSONField(default=dict, verbose_name=_("Ping params")) + gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) - gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")) + gather_facts_method = models.TextField( + max_length=32, blank=True, null=True, verbose_name=_("Gather facts method") + ) + gather_facts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params")) + change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled")) change_secret_method = models.TextField( max_length=32, blank=True, null=True, verbose_name=_("Change secret method") ) + change_secret_params = models.JSONField(default=dict, verbose_name=_("Change secret params")) + push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled")) push_account_method = models.TextField( max_length=32, blank=True, null=True, verbose_name=_("Push account method") ) + push_account_params = models.JSONField(default=dict, verbose_name=_("Push account params")) + verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled")) verify_account_method = models.TextField( - max_length=32, blank=True, null=True, verbose_name=_("Verify account method")) + max_length=32, blank=True, null=True, verbose_name=_("Verify account method") + ) + verify_account_params = models.JSONField(default=dict, verbose_name=_("Verify account params")) + gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) gather_accounts_method = models.TextField( max_length=32, blank=True, null=True, verbose_name=_("Gather facts method") ) + gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params")) class Platform(JMSBaseModel): @@ -80,14 +92,18 @@ class Platform(JMSBaseModel): internal = models.BooleanField(default=False, verbose_name=_("Internal")) # 资产有关的 charset = models.CharField( - default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset") + default=CharsetChoices.utf8, choices=CharsetChoices.choices, + max_length=8, verbose_name=_("Charset") ) domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled")) # 账号有关的 su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled")) su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method")) - automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform', - blank=True, null=True, verbose_name=_("Automation")) + automation = models.OneToOneField( + PlatformAutomation, on_delete=models.CASCADE, related_name='platform', + blank=True, null=True, verbose_name=_("Automation") + ) + custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields")) @property def type_constraints(self): @@ -100,11 +116,6 @@ class Platform(JMSBaseModel): ) return linux.id - @property - def primary_protocol(self): - primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type) - return self.protocols.filter(name=primary_protocol_name).first() - def __str__(self): return self.name diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py new file mode 100644 index 000000000..14e84768e --- /dev/null +++ b/apps/assets/notifications.py @@ -0,0 +1,25 @@ +from django.utils.translation import ugettext as _ + +from notifications.notifications import UserMessage + + +class BulkUpdatePlatformSkipAssetUserMsg(UserMessage): + def __init__(self, user, assets): + super().__init__(user) + self.assets = assets + + def get_html_msg(self) -> dict: + subject = _("Batch update platform in assets, skipping assets that do not meet platform type") + message = f'
    {"".join([f"
  1. {asset}
  2. " for asset in self.assets])}
' + return { + 'subject': subject, + 'message': message + } + + @classmethod + def gen_test_msg(cls): + from users.models import User + from assets.models import Asset + user = User.objects.first() + assets = Asset.objects.all()[:10] + return cls(user, assets) \ No newline at end of file diff --git a/apps/assets/serializers/asset/__init__.py b/apps/assets/serializers/asset/__init__.py index 12f1eb66c..8e3e14cf3 100644 --- a/apps/assets/serializers/asset/__init__.py +++ b/apps/assets/serializers/asset/__init__.py @@ -1,6 +1,8 @@ +# No pass +from .cloud import * from .common import * -from .host import * +from .custom import * from .database import * from .device import * -from .cloud import * +from .host import * from .web import * diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index d7dc7b175..2a50d3419 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- # +import re + from django.db.models import F from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from accounts.models import Account -from accounts.serializers import AccountSerializerCreateValidateMixin -from accounts.serializers import AuthValidateMixin -from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer +from accounts.serializers import AccountSerializer +from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \ + MethodSerializer +from common.serializers.dynamic import create_serializer_class from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ...const import Category, AllTypes @@ -19,9 +22,11 @@ __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', 'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', - 'AccountSecretSerializer', 'SpecSerializer' + 'AccountSecretSerializer', 'AssetProtocolsPermsSerializer' ] +uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') + class AssetProtocolsSerializer(serializers.ModelSerializer): port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1) @@ -38,6 +43,11 @@ class AssetProtocolsSerializer(serializers.ModelSerializer): fields = ['name', 'port'] +class AssetProtocolsPermsSerializer(AssetProtocolsSerializer): + class Meta(AssetProtocolsSerializer.Meta): + fields = AssetProtocolsSerializer.Meta.fields + ['public', 'setting'] + + class AssetLabelSerializer(serializers.ModelSerializer): class Meta: model = Label @@ -59,45 +69,39 @@ class AssetPlatformSerializer(serializers.ModelSerializer): } -class AssetAccountSerializer( - AuthValidateMixin, - AccountSerializerCreateValidateMixin, - CommonModelSerializer -): +class AssetAccountSerializer(AccountSerializer): add_org_fields = False - push_now = serializers.BooleanField( - default=False, label=_("Push now"), write_only=True - ) - template = serializers.BooleanField( - default=False, label=_("Template"), write_only=True - ) - name = serializers.CharField(max_length=128, required=True, label=_("Name")) + asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True) + clone_id: str - class Meta: - model = Account - fields_mini = [ - 'id', 'name', 'username', 'privileged', - 'is_active', 'version', 'secret_type', + def to_internal_value(self, data): + clone_id = data.pop('id', None) + ret = super().to_internal_value(data) + self.clone_id = clone_id + return ret + + def set_secret(self, attrs): + _id = self.clone_id + if not _id: + return attrs + + account = Account.objects.get(id=_id) + attrs['secret'] = account.secret + return attrs + + def validate(self, attrs): + attrs = super().validate(attrs) + return self.set_secret(attrs) + + class Meta(AccountSerializer.Meta): + fields = [ + f for f in AccountSerializer.Meta.fields + if f not in ['spec_info'] ] - fields_write_only = [ - 'secret', 'passphrase', 'push_now', 'template' - ] - fields = fields_mini + fields_write_only extra_kwargs = { - 'secret': {'write_only': True}, + **AccountSerializer.Meta.extra_kwargs, } - def validate_push_now(self, value): - request = self.context['request'] - if not request.user.has_perms('accounts.push_account'): - return False - return value - - def validate_name(self, value): - if not value: - value = self.initial_data.get('username') - return value - class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer): class Meta: @@ -110,44 +114,32 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer): } -class SpecSerializer(serializers.Serializer): - # 数据库 - db_name = serializers.CharField(label=_("Database"), max_length=128, required=False) - use_ssl = serializers.BooleanField(label=_("Use SSL"), required=False) - allow_invalid_cert = serializers.BooleanField(label=_("Allow invalid cert"), required=False) - # Web - autofill = serializers.CharField(label=_("Auto fill"), required=False) - username_selector = serializers.CharField(label=_("Username selector"), required=False) - password_selector = serializers.CharField(label=_("Password selector"), required=False) - submit_selector = serializers.CharField(label=_("Submit selector"), required=False) - script = serializers.JSONField(label=_("Script"), required=False) - - class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account')) - nodes_display = serializers.ListField(read_only=True, label=_("Node path")) + nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path")) + custom_info = MethodSerializer(label=_('Custom info')) class Meta: model = Asset fields_mini = ['id', 'name', 'address'] - fields_small = fields_mini + ['is_active', 'comment'] + fields_small = fields_mini + ['custom_info', 'is_active', 'comment'] fields_fk = ['domain', 'platform'] fields_m2m = [ 'nodes', 'labels', 'protocols', - 'nodes_display', 'accounts' + 'nodes_display', 'accounts', ] read_only_fields = [ - 'category', 'type', 'connectivity', 'auto_info', + 'category', 'type', 'connectivity', 'auto_config', 'date_verified', 'created_by', 'date_created', ] fields = fields_small + fields_fk + fields_m2m + read_only_fields - fields_unexport = ['auto_info'] + fields_unexport = ['auto_config'] extra_kwargs = { - 'auto_info': {'label': _('Auto info')}, + 'auto_config': {'label': _('Auto info')}, 'name': {'label': _("Name")}, 'address': {'label': _('Address')}, 'nodes_display': {'label': _('Node path')}, @@ -197,6 +189,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali .annotate(type=F("platform__type")) return queryset + def get_custom_info_serializer(self): + request = self.context.get('request') + default_field = serializers.DictField(required=False, label=_('Custom info')) + + if not request: + return default_field + + if self.instance and isinstance(self.instance, list): + return default_field + + if not self.instance and uuid_pattern.findall(request.path): + pk = uuid_pattern.findall(request.path)[0] + self.instance = Asset.objects.filter(id=pk).first() + + platform = None + if self.instance: + platform = self.instance.platform + elif request.query_params.get('platform'): + platform_id = request.query_params.get('platform') + platform_id = int(platform_id) if platform_id.isdigit() else 0 + platform = Platform.objects.filter(id=platform_id).first() + + if not platform: + return default_field + custom_fields = platform.custom_fields + if not custom_fields: + return default_field + name = platform.name.title() + 'CustomSerializer' + return create_serializer_class(name, custom_fields)() + @staticmethod def perform_nodes_display_create(instance, nodes_display): if not nodes_display: @@ -276,8 +298,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali if not accounts_data: return for data in accounts_data: - data['asset'] = asset - AssetAccountSerializer().create(data) + data['asset'] = asset.id + + s = AssetAccountSerializer(data=accounts_data, many=True) + s.is_valid(raise_exception=True) + s.save() @atomic def create(self, validated_data): @@ -300,16 +325,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali class DetailMixin(serializers.Serializer): accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) - spec_info = serializers.DictField(label=_('Spec info'), read_only=True) - auto_info = serializers.DictField(read_only=True, label=_('Auto info')) + spec_info = MethodSerializer(label=_('Spec info'), read_only=True) + gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True) + auto_config = serializers.DictField(read_only=True, label=_('Auto info')) + + def get_instance(self): + request = self.context.get('request') + if not self.instance and uuid_pattern.findall(request.path): + pk = uuid_pattern.findall(request.path)[0] + self.instance = Asset.objects.filter(id=pk).first() + return self.instance def get_field_names(self, declared_fields, info): names = super().get_field_names(declared_fields, info) names.extend([ - 'accounts', 'info', 'spec_info', 'auto_info' + 'accounts', 'gathered_info', 'spec_info', + 'auto_config', ]) return names + def get_category(self): + request = self.context.get('request') + if request.query_params.get('category'): + category = request.query_params.get('category') + else: + instance = self.get_instance() + category = instance.category + return category + + def get_gathered_info_serializer(self): + category = self.get_category() + from .info.gathered import category_gathered_serializer_map + serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField) + return serializer_cls() + + def get_spec_info_serializer(self): + category = self.get_category() + from .info.spec import category_spec_serializer_map + serializer_cls = category_spec_serializer_map.get(category, serializers.DictField) + return serializer_cls() + class AssetDetailSerializer(DetailMixin, AssetSerializer): pass diff --git a/apps/assets/serializers/asset/custom.py b/apps/assets/serializers/asset/custom.py new file mode 100644 index 000000000..d88024218 --- /dev/null +++ b/apps/assets/serializers/asset/custom.py @@ -0,0 +1,9 @@ +from assets.models import Custom +from .common import AssetSerializer + +__all__ = ['CustomSerializer'] + + +class CustomSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = Custom diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py index 9f5e97dba..da6c9574a 100644 --- a/apps/assets/serializers/asset/database.py +++ b/apps/assets/serializers/asset/database.py @@ -1,9 +1,9 @@ -from rest_framework.serializers import ValidationError from django.utils.translation import ugettext_lazy as _ +from rest_framework.serializers import ValidationError from assets.models import Database +from assets.serializers.gateway import GatewayWithAccountSecretSerializer from .common import AssetSerializer -from ..gateway import GatewayWithAccountSecretSerializer __all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer'] diff --git a/apps/assets/serializers/asset/host.py b/apps/assets/serializers/asset/host.py index 0f6bd8f5d..c3575f43b 100644 --- a/apps/assets/serializers/asset/host.py +++ b/apps/assets/serializers/asset/host.py @@ -1,34 +1,18 @@ from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers from assets.models import Host from .common import AssetSerializer +from .info.gathered import HostGatheredInfoSerializer -__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.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')) - arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch')) +__all__ = ['HostSerializer'] class HostSerializer(AssetSerializer): - info = HostInfoSerializer(required=False, label=_('Info')) + gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info")) class Meta(AssetSerializer.Meta): model = Host - fields = AssetSerializer.Meta.fields + ['info'] + fields = AssetSerializer.Meta.fields + ['gathered_info'] extra_kwargs = { **AssetSerializer.Meta.extra_kwargs, 'address': { diff --git a/apps/assets/serializers/asset/info/gathered.py b/apps/assets/serializers/asset/info/gathered.py new file mode 100644 index 000000000..db0f22e65 --- /dev/null +++ b/apps/assets/serializers/asset/info/gathered.py @@ -0,0 +1,23 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + + +class HostGatheredInfoSerializer(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.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')) + arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch')) + + +category_gathered_serializer_map = { + 'host': HostGatheredInfoSerializer, +} diff --git a/apps/assets/serializers/asset/info/spec.py b/apps/assets/serializers/asset/info/spec.py new file mode 100644 index 000000000..5d3de54c3 --- /dev/null +++ b/apps/assets/serializers/asset/info/spec.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + +from assets.models import Database, Web + + +class DatabaseSpecSerializer(serializers.ModelSerializer): + class Meta: + model = Database + fields = ['db_name', 'use_ssl', 'allow_invalid_cert'] + + +class WebSpecSerializer(serializers.ModelSerializer): + class Meta: + model = Web + fields = [ + 'autofill', 'username_selector', 'password_selector', + 'submit_selector', 'script' + ] + + +category_spec_serializer_map = { + 'database': DatabaseSpecSerializer, + 'web': WebSpecSerializer, +} diff --git a/apps/assets/serializers/asset/web.py b/apps/assets/serializers/asset/web.py index aa35e28e0..c6e05f6c0 100644 --- a/apps/assets/serializers/asset/web.py +++ b/apps/assets/serializers/asset/web.py @@ -24,6 +24,6 @@ class WebSerializer(AssetSerializer): 'default': 'name=password' }, 'submit_selector': { - 'default': 'id=longin_button', + 'default': 'id=login_button', }, } diff --git a/apps/assets/serializers/gateway.py b/apps/assets/serializers/gateway.py index 78d8afda5..259bc13f8 100644 --- a/apps/assets/serializers/gateway.py +++ b/apps/assets/serializers/gateway.py @@ -3,8 +3,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from .asset import HostSerializer from .asset.common import AccountSecretSerializer +from .asset.host import HostSerializer from ..models import Gateway, Asset __all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer'] diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 15b4e7cca..c542cc20a 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -4,6 +4,7 @@ from rest_framework import serializers from assets.const.web import FillType from common.serializers import WritableNestedModelSerializer from common.serializers.fields import LabeledChoiceField +from common.utils import lazyproperty from ..const import Category, AllTypes from ..models import Platform, PlatformProtocol, PlatformAutomation @@ -37,10 +38,12 @@ class ProtocolSettingSerializer(serializers.Serializer): default="", allow_blank=True, label=_("Submit selector") ) script = serializers.JSONField(default=list, label=_("Script")) - # Redis auth_username = serializers.BooleanField(default=False, label=_("Auth with username")) + # WinRM + use_ssl = serializers.BooleanField(default=False, label=_("Use SSL")) + class PlatformAutomationSerializer(serializers.ModelSerializer): class Meta: @@ -48,12 +51,12 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): fields = [ "id", "ansible_enabled", "ansible_config", - "ping_enabled", "ping_method", - "push_account_enabled", "push_account_method", - "gather_facts_enabled", "gather_facts_method", - "change_secret_enabled", "change_secret_method", - "verify_account_enabled", "verify_account_method", - "gather_accounts_enabled", "gather_accounts_method", + "ping_enabled", "ping_method", "ping_params", + "push_account_enabled", "push_account_method", "push_account_params", + "gather_facts_enabled", "gather_facts_method", "gather_facts_params", + "change_secret_enabled", "change_secret_method", "change_secret_params", + "verify_account_enabled", "verify_account_method", "verify_account_params", + "gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params", ] extra_kwargs = { # 启用资产探测 @@ -72,33 +75,53 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): } -class PlatformProtocolsSerializer(serializers.ModelSerializer): +class PlatformProtocolSerializer(serializers.ModelSerializer): setting = ProtocolSettingSerializer(required=False, allow_null=True) - primary = serializers.BooleanField(read_only=True, label=_("Primary")) class Meta: model = PlatformProtocol fields = [ "id", "name", "port", "primary", - "default", "required", "secret_types", - "setting", + "required", "default", + "secret_types", "setting", ] +class PlatformCustomField(serializers.Serializer): + TYPE_CHOICES = [ + ("str", "str"), + ("text", "text"), + ("int", "int"), + ("bool", "bool"), + ("choice", "choice"), + ("list", "list"), + ] + name = serializers.CharField(label=_("Name"), max_length=128) + label = serializers.CharField(label=_("Label"), max_length=128) + type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str') + default = serializers.CharField(default="", allow_blank=True, label=_("Default"), max_length=1024) + help_text = serializers.CharField(default="", allow_blank=True, label=_("Help text"), max_length=1024) + choices = serializers.ListField(default=list, label=_("Choices"), required=False) + + class PlatformSerializer(WritableNestedModelSerializer): - charset = LabeledChoiceField( - choices=Platform.CharsetChoices.choices, label=_("Charset") - ) + SU_METHOD_CHOICES = [ + ("sudo", "sudo su -"), + ("su", "su - "), + ("enable", "enable"), + ("super", "super 15"), + ("super_level", "super level 15") + ] + charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8') type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type")) category = LabeledChoiceField(choices=Category.choices, label=_("Category")) - protocols = PlatformProtocolsSerializer( - label=_("Protocols"), many=True, required=False - ) - automation = PlatformAutomationSerializer(label=_("Automation"), required=False) + protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False) + automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict) su_method = LabeledChoiceField( - choices=[("sudo", "sudo su -"), ("su", "su - ")], - label=_("Su method"), required=False, default="sudo", allow_null=True + choices=SU_METHOD_CHOICES, label=_("Su method"), + required=False, default="sudo", allow_null=True ) + custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False) class Meta: model = Platform @@ -106,19 +129,54 @@ class PlatformSerializer(WritableNestedModelSerializer): fields_small = fields_mini + [ "category", "type", "charset", ] - fields_other = [ - 'date_created', 'date_updated', 'created_by', 'updated_by', + read_only_fields = [ + 'internal', 'date_created', 'date_updated', + 'created_by', 'updated_by' ] fields = fields_small + [ "protocols", "domain_enabled", "su_enabled", - "su_method", "automation", "comment", - ] + fields_other + "su_method", "automation", "comment", "custom_fields", + ] + read_only_fields extra_kwargs = { "su_enabled": {"label": _('Su enabled')}, "domain_enabled": {"label": _('Domain enabled')}, "domain_default": {"label": _('Default Domain')}, } + @property + def platform_category_type(self): + if self.instance: + return self.instance.category, self.instance.type + if self.initial_data: + return self.initial_data.get('category'), self.initial_data.get('type') + raise serializers.ValidationError({'type': _("type is required")}) + + def add_type_choices(self, name, label): + tp = self.fields['type'] + tp.choices[name] = label + tp.choice_mapper[name] = label + tp.choice_strings_to_values[name] = label + + @lazyproperty + def constraints(self): + category, tp = self.platform_category_type + constraints = AllTypes.get_constraints(category, tp) + return constraints + + def validate(self, attrs): + domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False) + su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False) + automation = attrs.get('automation', {}) + automation['ansible_enabled'] = automation.get('ansible_enabled', False) \ + and self.constraints.get('ansible_enabled', False) + attrs.update({ + 'domain_enabled': domain_enabled, + 'su_enabled': su_enabled, + 'automation': automation, + }) + self.initial_data['automation'] = automation + return attrs + @classmethod def setup_eager_loading(cls, queryset): queryset = queryset.prefetch_related( @@ -126,6 +184,16 @@ class PlatformSerializer(WritableNestedModelSerializer): ) return queryset + def validate_protocols(self, protocols): + if not protocols: + raise serializers.ValidationError(_("Protocols is required")) + primary = [p for p in protocols if p.get('primary')] + if not primary: + protocols[0]['primary'] = True + # 这里不设置不行,write_nested 不使用 validated 中的 + self.initial_data['protocols'] = protocols + return protocols + class PlatformOpsMethodSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index 107be4422..9598bec5d 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs): ensure_asset_has_node(assets=(instance,)) # 获取资产硬件信息 - auto_info = instance.auto_info - if auto_info.get('ping_enabled'): + auto_config = instance.auto_config + if auto_config.get('ping_enabled'): logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name)) test_assets_connectivity_handler(assets=(instance,)) - if auto_info.get('gather_facts_enabled'): + if auto_config.get('gather_facts_enabled'): logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name)) gather_assets_facts_handler(assets=(instance,)) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index a9a385eb9..6b5f469d0 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device') router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'webs', api.WebViewSet, 'web') router.register(r'clouds', api.CloudViewSet, 'cloud') +router.register(r'customs', api.CustomViewSet, 'custom') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') @@ -45,6 +46,7 @@ urlpatterns = [ path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + path('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), name='platform-automation-methods'), ] urlpatterns += router.urls diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index dd225073e..5b5dcd700 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -162,7 +162,8 @@ class RDPFileClientProtocolURLMixin: def get_smart_endpoint(self, protocol, asset=None): target_ip = asset.get_target_ip() if asset else '' endpoint = EndpointRule.match_endpoint( - target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request + target_instance=asset, target_ip=target_ip, + protocol=protocol, request=self.request ) return endpoint diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 6a69a7e37..63049170b 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import PermissionDenied from assets.const import Protocol +from assets.const.host import GATEWAY_NAME from common.db.fields import EncryptTextField from common.exceptions import JMSException from common.utils import lazyproperty, pretty_string, bulk_get @@ -231,12 +232,14 @@ class ConnectionToken(JMSOrgBaseModel): def domain(self): if not self.asset.platform.domain_enabled: return - domain = self.asset.domain if self.asset else None + if self.asset.platform.name == GATEWAY_NAME: + return + domain = self.asset.domain if self.asset.domain else None return domain @lazyproperty def gateway(self): - if not self.asset: + if not self.asset or not self.domain: return return self.asset.gateway diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py index 003d8239e..b96a99171 100644 --- a/apps/authentication/serializers/connect_token_secret.py +++ b/apps/authentication/serializers/connect_token_secret.py @@ -5,7 +5,8 @@ from accounts.const import SecretType from accounts.models import Account from acls.models import CommandGroup, CommandFilterACL from assets.models import Asset, Platform, Gateway, Domain -from assets.serializers import PlatformSerializer, AssetProtocolsSerializer +from assets.serializers.asset import AssetProtocolsSerializer +from assets.serializers.platform import PlatformSerializer from common.serializers.fields import LabeledChoiceField from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin @@ -26,18 +27,17 @@ class _ConnectionTokenUserSerializer(serializers.ModelSerializer): class _ConnectionTokenAssetSerializer(serializers.ModelSerializer): protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) + info = serializers.DictField() class Meta: model = Asset fields = [ - 'id', 'name', 'address', 'protocols', - 'category', 'type', 'org_id', 'spec_info', - 'secret_info', + 'id', 'name', 'address', 'protocols', 'category', + 'type', 'org_id', 'info', 'secret_info', 'spec_info' ] class _SimpleAccountSerializer(serializers.ModelSerializer): - """ Account """ secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) class Meta: @@ -46,20 +46,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer): class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): - """ Account """ su_from = _SimpleAccountSerializer(required=False, label=_('Su from')) secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) class Meta: model = Account fields = [ - 'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged' + 'id', 'name', 'username', 'secret_type', + 'secret', 'su_from', 'privileged' ] class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): - """ Gateway """ - account = _SimpleAccountSerializer( required=False, source='select_account', read_only=True ) @@ -85,7 +83,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer): class Meta: model = CommandFilterACL fields = [ - 'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active' + 'id', 'name', 'command_groups', 'action', + 'reviewers', 'priority', 'is_active' ] @@ -136,8 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): 'id', 'value', 'user', 'asset', 'account', 'platform', 'command_filter_acls', 'protocol', 'domain', 'gateway', 'actions', 'expire_at', - 'from_ticket', - 'expire_now', 'connect_method', + 'from_ticket', 'expire_now', 'connect_method', ] extra_kwargs = { 'value': {'read_only': True}, diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index 7f3d4b055..9ba77ca72 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -111,7 +111,7 @@ class BaseFileParser(BaseParser): return {'pk': obj_id, 'name': obj_name} def parse_value(self, field, value): - if value is '-': + if value == '-' and field and field.allow_null: return None elif hasattr(field, 'to_file_internal_value'): value = field.to_file_internal_value(value) diff --git a/apps/common/serializers/__init__.py b/apps/common/serializers/__init__.py index 7ecadafc7..6d79241fc 100644 --- a/apps/common/serializers/__init__.py +++ b/apps/common/serializers/__init__.py @@ -1,2 +1,3 @@ from .common import * +from .dynamic import * from .mixin import * diff --git a/apps/common/serializers/dynamic.py b/apps/common/serializers/dynamic.py new file mode 100644 index 000000000..9ab26b9bb --- /dev/null +++ b/apps/common/serializers/dynamic.py @@ -0,0 +1,55 @@ +from rest_framework import serializers + +example_info = [ + {"name": "name", "label": "姓名", "required": False, "default": "广州老广", "type": "str"}, + {"name": "age", "label": "年龄", "required": False, "default": 18, "type": "int"}, +] + +type_field_map = { + "str": serializers.CharField, + "int": serializers.IntegerField, + "bool": serializers.BooleanField, + "text": serializers.CharField, + "choice": serializers.ChoiceField, + "list": serializers.ListField, +} + + +def set_default_if_need(data, i): + field_name = data.pop('name', 'Attr{}'.format(i + 1)) + data['name'] = field_name + + if not data.get('label'): + data['label'] = field_name + return data + + +def set_default_by_type(tp, data, field_info): + if tp == 'str': + data['max_length'] = 4096 + elif tp == 'choice': + choices = field_info.pop('choices', []) + if isinstance(choices, str): + choices = choices.split(',') + choices = [ + (c, c.title()) if not isinstance(c, (tuple, list)) else c + for c in choices + ] + data['choices'] = choices + return data + + +def create_serializer_class(serializer_name, fields_info): + serializer_fields = {} + fields_name = ['name', 'label', 'default', 'type', 'help_text'] + + for i, field_info in enumerate(fields_info): + data = {k: field_info.get(k) for k in fields_name} + field_type = data.pop('type', 'str') + data = set_default_by_type(field_type, data, field_info) + data = set_default_if_need(data, i) + field_name = data.pop('name') + field_class = type_field_map.get(field_type, serializers.CharField) + serializer_fields[field_name] = field_class(**data) + + return type(serializer_name, (serializers.Serializer,), serializer_fields) diff --git a/apps/common/serializers/fields.py b/apps/common/serializers/fields.py index 84d4d5927..514a96fd0 100644 --- a/apps/common/serializers/fields.py +++ b/apps/common/serializers/fields.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import phonenumbers from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -17,6 +18,7 @@ __all__ = [ "BitChoicesField", "TreeChoicesField", "LabeledMultipleChoiceField", + "PhoneField", ] @@ -201,3 +203,11 @@ class BitChoicesField(TreeChoicesField): value = self.to_internal_value(data) self.run_validators(value) return value + + +class PhoneField(serializers.CharField): + def to_representation(self, value): + if value: + phone = phonenumbers.parse(value, 'CN') + value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number} + return value diff --git a/apps/common/validators.py b/apps/common/validators.py index 4be90d855..5bb3e4bdb 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -2,12 +2,15 @@ # import re +import phonenumbers + from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ from rest_framework.validators import ( UniqueTogetherValidator, ValidationError ) from rest_framework import serializers +from phonenumbers.phonenumberutil import NumberParseException from common.utils.strings import no_special_chars @@ -42,9 +45,14 @@ class NoSpecialChars: class PhoneValidator: - pattern = re.compile(r"^1[3456789]\d{9}$") message = _('The mobile phone number format is incorrect') def __call__(self, value): - if not self.pattern.match(value): + try: + phone = phonenumbers.parse(value, 'CN') + valid = phonenumbers.is_valid_number(phone) + except NumberParseException: + valid = False + + if not valid: raise serializers.ValidationError(self.message) diff --git a/apps/common/views/mixins.py b/apps/common/views/mixins.py index cb2e402c8..562e9ca81 100644 --- a/apps/common/views/mixins.py +++ b/apps/common/views/mixins.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- # from django.utils import translation -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_noop from django.contrib.auth.mixins import UserPassesTestMixin from django.http.response import JsonResponse from rest_framework import permissions from rest_framework.request import Request from common.exceptions import UserConfirmRequired +from common.utils import i18n_fmt from audits.handler import create_or_update_operate_log -from audits.const import ActionChoices +from audits.const import ActionChoices, ActivityChoices +from audits.models import ActivityLog __all__ = [ "PermissionsMixin", @@ -49,38 +51,63 @@ class RecordViewLogMixin: ACTION = ActionChoices.view @staticmethod - def get_resource_display(request): + def _filter_params(params): + new_params = {} + need_pop_params = ('format', 'order') + for key, value in params.items(): + if key in need_pop_params: + continue + if isinstance(value, list): + value = list(filter(None, value)) + if value: + new_params[key] = value + return new_params + + def get_resource_display(self, request): query_params = dict(request.query_params) - if query_params.get("format"): - query_params.pop("format") - spm_filter = query_params.pop("spm") if query_params.get("spm") else None - if not query_params and not spm_filter: - display_message = _("Export all") + params = self._filter_params(query_params) + + spm_filter = params.pop("spm", None) + + if not params and not spm_filter: + display_message = gettext_noop("Export all") elif spm_filter: - display_message = _("Export only selected items") + display_message = gettext_noop("Export only selected items") else: query = ",".join( - ["%s=%s" % (key, value) for key, value in query_params.items()] + ["%s=%s" % (key, value) for key, value in params.items()] ) - display_message = _("Export filtered: %s") % query + display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query) return display_message + def record_logs(self, ids, **kwargs): + resource_type = self.model._meta.verbose_name + create_or_update_operate_log( + self.ACTION, resource_type, force=True, **kwargs + ) + detail = i18n_fmt( + gettext_noop('User %s view/export secret'), self.request.user + ) + activities = [ + ActivityLog( + resource_id=getattr(resource_id, 'pk', resource_id), + type=ActivityChoices.operate_log, detail=detail + ) + for resource_id in ids + ] + ActivityLog.objects.bulk_create(activities) + def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) with translation.override('en'): resource_display = self.get_resource_display(request) - resource_type = self.model._meta.verbose_name - create_or_update_operate_log( - self.ACTION, resource_type, force=True, - resource_display=resource_display - ) + ids = [q.id for q in self.get_queryset()] + self.record_logs(ids, resource_display=resource_display) return response def retrieve(self, request, *args, **kwargs): response = super().retrieve(request, *args, **kwargs) with translation.override('en'): - resource_type = self.model._meta.verbose_name - create_or_update_operate_log( - self.ACTION, resource_type, force=True, resource=self.get_object() - ) + resource = self.get_object() + self.record_logs([resource.id], resource=resource) return response diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 472b329b3..86fc7369d 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:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72 -size 137935 +oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d +size 138303 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index d4b4ecc20..772fb0b5c 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-03-14 17:34+0800\n" +"POT-Creation-Date: 2023-04-07 13:57+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -70,6 +70,22 @@ msgstr "ローカル" msgid "Collected" msgstr "" +#: accounts/const/account.py:24 ops/const.py:45 +msgid "Skip" +msgstr "スキップ" + +#: accounts/const/account.py:25 audits/const.py:23 rbac/tree.py:229 +#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 +msgid "Update" +msgstr "更新" + +#: accounts/const/account.py:26 +#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 +#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 +#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失敗しました" + #: accounts/const/automation.py:22 rbac/tree.py:50 msgid "Push account" msgstr "アカウントプッシュ" @@ -159,25 +175,26 @@ msgid "Only create" msgstr "作成のみ" #: accounts/models/account.py:47 -#: accounts/models/automations/gather_account.py:14 -#: accounts/serializers/account/account.py:95 +#: accounts/models/automations/gather_account.py:16 +#: accounts/serializers/account/account.py:173 +#: accounts/serializers/account/account.py:206 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:131 #: acls/models/base.py:100 acls/serializers/base.py:56 -#: assets/models/asset/common.py:92 assets/models/asset/common.py:280 +#: assets/models/asset/common.py:92 assets/models/asset/common.py:268 #: 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:35 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 #: 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 msgid "Asset" msgstr "資産" -#: accounts/models/account.py:51 accounts/serializers/account/account.py:99 -#: authentication/serializers/connect_token_secret.py:50 +#: accounts/models/account.py:51 accounts/serializers/account/account.py:178 +#: authentication/serializers/connect_token_secret.py:48 msgid "Su from" msgstr "から切り替え" @@ -186,52 +203,58 @@ msgstr "から切り替え" msgid "Version" msgstr "バージョン" -#: accounts/models/account.py:55 accounts/serializers/account/account.py:96 +#: accounts/models/account.py:55 accounts/serializers/account/account.py:174 #: users/models/user.py:768 msgid "Source" msgstr "ソース" -#: accounts/models/account.py:58 +#: accounts/models/account.py:56 +#, fuzzy +#| msgid "Source" +msgid "Source ID" +msgstr "ソース" + +#: accounts/models/account.py:59 #: 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:131 assets/serializers/gateway.py:28 +#: assets/serializers/asset/common.py:107 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40 +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 #: 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" msgstr "アカウント" -#: accounts/models/account.py:64 +#: accounts/models/account.py:65 msgid "Can view asset account secret" msgstr "資産アカウントの秘密を表示できます" -#: accounts/models/account.py:65 +#: accounts/models/account.py:66 msgid "Can view asset history account" msgstr "資産履歴アカウントを表示できます" -#: accounts/models/account.py:66 +#: accounts/models/account.py:67 msgid "Can view asset history account secret" msgstr "資産履歴アカウントパスワードを表示できます" -#: accounts/models/account.py:67 +#: accounts/models/account.py:68 msgid "Can verify account" msgstr "アカウントを確認できます" -#: accounts/models/account.py:68 +#: accounts/models/account.py:69 msgid "Can push account" msgstr "アカウントをプッシュできます" -#: accounts/models/account.py:109 +#: accounts/models/account.py:110 msgid "Account template" msgstr "アカウント テンプレート" -#: accounts/models/account.py:114 +#: accounts/models/account.py:115 msgid "Can view asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを表示できます" -#: accounts/models/account.py:115 +#: accounts/models/account.py:116 msgid "Can change asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを変更できます" @@ -276,7 +299,7 @@ msgid "Trigger mode" msgstr "トリガーモード" #: accounts/models/automations/backup_account.py:97 audits/models.py:172 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:168 +#: terminal/models/session/sharing.py:111 xpack/plugins/cloud/models.py:168 msgid "Reason" msgstr "理由" @@ -328,16 +351,17 @@ msgid "Can add push account execution" msgstr "プッシュ アカウントの作成の実行" #: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 -#: accounts/serializers/account/account.py:134 +#: accounts/serializers/account/account.py:383 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 -#: authentication/serializers/connect_token_secret.py:41 -#: authentication/serializers/connect_token_secret.py:51 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:49 msgid "Secret type" msgstr "鍵の種類" #: accounts/models/automations/change_secret.py:20 #: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 #: settings/serializers/auth/radius.py:19 @@ -376,7 +400,8 @@ msgstr "開始日" msgid "Date finished" msgstr "終了日" -#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8 +#: accounts/models/automations/change_secret.py:94 +#: accounts/serializers/account/account.py:208 assets/const/automation.py:8 #: common/const/choices.py:20 msgid "Error" msgstr "間違い" @@ -385,15 +410,15 @@ msgstr "間違い" msgid "Change secret record" msgstr "パスワード レコードの変更" -#: accounts/models/automations/gather_account.py:12 +#: accounts/models/automations/gather_account.py:14 msgid "Present" msgstr "存在する" -#: accounts/models/automations/gather_account.py:13 +#: accounts/models/automations/gather_account.py:15 msgid "Date last login" msgstr "最終ログイン日" -#: accounts/models/automations/gather_account.py:15 +#: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 #: acls/serializers/base.py:18 acls/serializers/base.py:49 #: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25 @@ -406,15 +431,21 @@ msgstr "最終ログイン日" msgid "Username" msgstr "ユーザー名" -#: accounts/models/automations/gather_account.py:16 +#: accounts/models/automations/gather_account.py:18 msgid "Address last login" msgstr "最終ログインアドレス" -#: accounts/models/automations/gather_account.py:23 +#: accounts/models/automations/gather_account.py:44 msgid "Gather account automation" msgstr "自動収集アカウント" -#: accounts/models/automations/gather_account.py:39 +#: accounts/models/automations/gather_account.py:56 +#, fuzzy +#| msgid "Is service account" +msgid "Is sync account" +msgstr "サービスアカウントです" + +#: accounts/models/automations/gather_account.py:71 #: accounts/tasks/gather_accounts.py:29 msgid "Gather asset accounts" msgstr "アカウントのコレクション" @@ -426,7 +457,7 @@ msgstr "トリガー方式" #: 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 +#: authentication/serializers/connect_token_secret.py:107 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" msgstr "アクション" @@ -445,10 +476,10 @@ msgstr "アカウントの確認" #: assets/models/asset/common.py:90 assets/models/asset/common.py:102 #: 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:74 assets/serializers/asset/common.py:151 -#: assets/serializers/platform.py:133 -#: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21 +#: assets/models/platform.py:13 assets/models/platform.py:65 +#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93 +#: assets/serializers/platform.py:184 +#: authentication/serializers/connect_token_secret.py:101 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 #: orgs/models.py:69 perms/models/asset_permission.py:56 rbac/models/role.py:29 @@ -456,7 +487,7 @@ msgstr "アカウントの確認" #: terminal/models/applet/applet.py:27 terminal/models/component/endpoint.py:12 #: terminal/models/component/endpoint.py:90 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: terminal/models/component/terminal.py:85 users/forms/profile.py:33 #: users/models/group.py:13 users/models/user.py:717 #: xpack/plugins/cloud/models.py:28 msgid "Name" @@ -469,7 +500,7 @@ msgstr "特権アカウント" #: accounts/models/base.py:40 assets/models/asset/common.py:109 #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:107 +#: authentication/serializers/connect_token_secret.py:105 #: terminal/models/applet/applet.py:32 users/serializers/user.py:162 msgid "Is active" msgstr "アクティブです。" @@ -515,38 +546,38 @@ msgstr "" "{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" "情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" -#: accounts/serializers/account/account.py:65 -#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75 +#: accounts/serializers/account/account.py:26 +#: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "テンプレート" -#: accounts/serializers/account/account.py:68 -#: assets/serializers/asset/common.py:69 +#: accounts/serializers/account/account.py:29 msgid "Push now" msgstr "今すぐプッシュ" -#: accounts/serializers/account/account.py:70 -#: accounts/serializers/account/base.py:64 -msgid "Has secret" -msgstr "エスクローされたパスワード" +#: accounts/serializers/account/account.py:33 +#, fuzzy +#| msgid "Runas policy" +msgid "Exist policy" +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:127 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:94 assets/serializers/platform.py:134 +#: accounts/serializers/account/account.py:153 applications/models.py:11 +#: assets/models/label.py:21 assets/models/platform.py:66 +#: assets/serializers/asset/common.py:103 assets/serializers/cagegory.py:8 +#: assets/serializers/platform.py:111 assets/serializers/platform.py:185 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "カテゴリ" -#: accounts/serializers/account/account.py:76 +#: accounts/serializers/account/account.py:154 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 -#: assets/models/cmd_filter.py:74 assets/models/platform.py:78 -#: 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 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:67 +#: assets/serializers/asset/common.py:104 assets/serializers/platform.py:95 +#: assets/serializers/platform.py:110 audits/serializers.py:48 +#: authentication/serializers/connect_token_secret.py:114 ops/models/job.py:102 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29 @@ -558,10 +589,79 @@ msgstr "カテゴリ" msgid "Type" msgstr "タイプ" -#: accounts/serializers/account/account.py:91 +#: accounts/serializers/account/account.py:169 msgid "Asset not found" msgstr "資産が存在しません" +#: accounts/serializers/account/account.py:175 +#: accounts/serializers/account/base.py:64 +msgid "Has secret" +msgstr "エスクローされたパスワード" + +#: accounts/serializers/account/account.py:207 ops/models/celery.py:60 +#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45 +#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14 +#: tickets/serializers/ticket/ticket.py:21 +msgid "State" +msgstr "状態" + +#: accounts/serializers/account/account.py:209 +#, fuzzy +#| msgid "Change by" +msgid "Changed" +msgstr "による変更" + +#: accounts/serializers/account/account.py:213 +#: accounts/serializers/automations/base.py:22 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:104 ops/serializers/job.py:20 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "資産" + +#: accounts/serializers/account/account.py:284 +#, fuzzy +#| msgid "Name already exists" +msgid "Account already exists" +msgstr "名前は既に存在します。" + +#: accounts/serializers/account/account.py:320 +#, fuzzy, python-format +#| msgid "Current user not support mfa type: {}" +msgid "Asset does not support this secret type: %s" +msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" + +#: accounts/serializers/account/account.py:351 +#, fuzzy +#| msgid "Account name" +msgid "Account has exist" +msgstr "アカウント名" + +#: accounts/serializers/account/account.py:393 acls/models/base.py:98 +#: acls/models/login_acl.py:13 acls/serializers/base.py:55 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24 +#: assets/models/label.py:16 audits/models.py:44 audits/models.py:63 +#: audits/models.py:141 authentication/models/connection_token.py:29 +#: 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:30 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 +#: terminal/serializers/command.py:16 tickets/models/comment.py:21 +#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 +#: users/serializers/group.py:18 +msgid "User" +msgstr "ユーザー" + +#: accounts/serializers/account/account.py:394 +#: authentication/templates/authentication/_access_key_modal.html:33 +#: terminal/notifications.py:98 terminal/notifications.py:146 +msgid "Date" +msgstr "日付" + #: accounts/serializers/account/backup.py:31 #: accounts/serializers/automations/base.py:36 #: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103 @@ -583,27 +683,15 @@ msgstr "現在、メール送信のみがサポートされています" msgid "Asset type" msgstr "資産タイプ" -#: accounts/serializers/account/base.py:19 -msgid "Secret/Password" -msgstr "キー/パスワード" - #: accounts/serializers/account/base.py:24 msgid "Key password" msgstr "キーパスワード" -#: accounts/serializers/account/base.py:81 -#: assets/serializers/asset/common.py:301 +#: accounts/serializers/account/base.py:80 +#: assets/serializers/asset/common.py:298 msgid "Spec info" msgstr "特別情報" -#: accounts/serializers/automations/base.py:22 -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:104 ops/serializers/job.py:20 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "資産" - #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:108 assets/models/automations/base.py:18 #: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 @@ -641,17 +729,11 @@ msgstr "自動タスク実行履歴" #: accounts/serializers/automations/change_secret.py:154 audits/const.py:52 #: audits/models.py:54 audits/signal_handlers/activity_log.py:33 #: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39 -#: terminal/const.py:59 terminal/models/session/sharing.py:103 +#: terminal/const.py:59 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" -#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 -#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失敗しました" - #: accounts/tasks/automation.py:24 msgid "Account execute automation" msgstr "アカウント実行の自動化" @@ -721,7 +803,7 @@ msgstr "1-100、低い値は最初に一致します" #: acls/models/base.py:82 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 -#: authentication/serializers/connect_token_secret.py:82 +#: authentication/serializers/connect_token_secret.py:79 msgid "Reviewers" msgstr "レビュー担当者" @@ -733,24 +815,6 @@ msgstr "レビュー担当者" msgid "Active" msgstr "アクティブ" -#: acls/models/base.py:98 acls/models/login_acl.py:13 -#: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 -#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:44 -#: audits/models.py:63 audits/models.py:141 -#: authentication/models/connection_token.py:29 -#: 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: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 -#: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 -#: users/serializers/group.py:18 -msgid "User" -msgstr "ユーザー" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 #: ops/serializers/job.py:65 terminal/const.py:67 #: terminal/models/session/session.py:43 terminal/serializers/command.py:18 @@ -778,7 +842,7 @@ msgstr "家を無視する" #: acls/models/command_acl.py:33 acls/models/command_acl.py:96 #: acls/serializers/command_acl.py:28 -#: authentication/serializers/connect_token_secret.py:79 +#: authentication/serializers/connect_token_secret.py:76 msgid "Command group" msgstr "コマンドグループ" @@ -883,7 +947,7 @@ msgstr "" #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:62 msgid "IP" msgstr "IP" @@ -908,7 +972,7 @@ msgstr "アプリケーション" msgid "Can match application" msgstr "アプリケーションを一致させることができます" -#: assets/api/asset/asset.py:143 +#: assets/api/asset/asset.py:147 msgid "Cannot create asset directly, you should create a host or other" msgstr "" "資産を直接作成することはできません。ホストまたはその他を作成する必要がありま" @@ -984,31 +1048,37 @@ msgstr "テストゲートウェイ" msgid "Gather facts" msgstr "資産情報の収集" -#: assets/const/category.py:11 assets/models/asset/host.py:8 +#: assets/const/category.py:10 assets/models/asset/host.py:8 #: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 #: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" -#: assets/const/category.py:12 assets/models/asset/device.py:8 +#: assets/const/category.py:11 assets/models/asset/device.py:8 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:115 +#: assets/const/category.py:12 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:91 msgid "Database" msgstr "データベース" -#: assets/const/category.py:14 +#: assets/const/category.py:13 msgid "Cloud service" msgstr "クラウド サービス" -#: assets/const/category.py:15 assets/models/asset/web.py:16 audits/const.py:33 +#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:33 #: terminal/models/applet/applet.py:25 msgid "Web" msgstr "Web" +#: assets/const/category.py:15 +#, fuzzy +#| msgid "Custom user" +msgid "Custom type" +msgstr "カスタムユーザー" + #: assets/const/cloud.py:7 msgid "Public cloud" msgstr "パブリック クラウド" @@ -1042,7 +1112,7 @@ msgstr "ファイアウォール" msgid "Other" msgstr "その他" -#: assets/const/types.py:200 +#: assets/const/types.py:214 msgid "All types" msgstr "いろんなタイプ" @@ -1060,7 +1130,7 @@ msgid "Basic" msgstr "基本" #: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40 +#: assets/serializers/asset/common.py:99 assets/serializers/platform.py:40 msgid "Script" msgstr "脚本" @@ -1080,7 +1150,7 @@ msgstr "SSHパブリックキー" #: assets/models/cmd_filter.py:88 assets/models/group.py:23 #: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:110 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:158 +#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:160 #: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:47 tickets/models/comment.py:32 @@ -1129,7 +1199,7 @@ msgid "Username same with user" msgstr "ユーザーと同じユーザー名" #: assets/models/_user.py:52 authentication/models/connection_token.py:38 -#: authentication/serializers/connect_token_secret.py:104 +#: authentication/serializers/connect_token_secret.py:102 #: terminal/models/applet/applet.py:34 terminal/serializers/session.py:20 #: terminal/serializers/session.py:41 terminal/serializers/storage.py:68 msgid "Protocol" @@ -1183,25 +1253,25 @@ msgstr "システムユーザーに一致できます" msgid "Cloud" msgstr "クラウド サービス" -#: assets/models/asset/common.py:91 assets/models/platform.py:22 +#: assets/models/asset/common.py:91 assets/models/platform.py:14 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" -#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152 +#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:129 msgid "Address" msgstr "アドレス" -#: assets/models/asset/common.py:104 assets/models/platform.py:112 -#: authentication/serializers/connect_token_secret.py:108 +#: assets/models/asset/common.py:104 assets/models/platform.py:100 +#: authentication/serializers/connect_token_secret.py:106 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:187 msgid "Platform" msgstr "プラットフォーム" #: assets/models/asset/common.py:106 assets/models/domain.py:21 -#: authentication/serializers/connect_token_secret.py:126 +#: authentication/serializers/connect_token_secret.py:124 #: perms/serializers/user_permission.py:28 msgid "Domain" msgstr "ドメイン" @@ -1210,23 +1280,42 @@ msgstr "ドメイン" msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:283 +#: assets/models/asset/common.py:111 +#, fuzzy +#| msgid "Gather asset hardware info" +msgid "Gathered info" +msgstr "資産ハードウェア情報の収集" + +#: assets/models/asset/common.py:112 assets/serializers/asset/common.py:109 +#: assets/serializers/asset/common.py:179 +#, fuzzy +#| msgid "Auto info" +msgid "Custom info" +msgstr "自動情報" + +#: assets/models/asset/common.py:271 msgid "Can refresh asset hardware info" msgstr "資産ハードウェア情報を更新できます" -#: assets/models/asset/common.py:284 +#: assets/models/asset/common.py:272 msgid "Can test asset connectivity" msgstr "資産接続をテストできます" -#: assets/models/asset/common.py:285 +#: assets/models/asset/common.py:273 msgid "Can match asset" msgstr "アセットを一致させることができます" -#: assets/models/asset/common.py:286 +#: assets/models/asset/common.py:274 msgid "Can change asset nodes" msgstr "資産ノードを変更できます" -#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116 +#: assets/models/asset/custom.py:8 +#, fuzzy +#| msgid "Custom user" +msgid "Custom asset" +msgstr "カスタムユーザー" + +#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:92 #: settings/serializers/email.py:37 msgid "Use SSL" msgstr "SSLの使用" @@ -1243,7 +1332,7 @@ msgstr "クライアント証明書" msgid "Client key" msgstr "クライアントキー" -#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117 +#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:93 msgid "Allow invalid cert" msgstr "証明書チェックを無視" @@ -1251,23 +1340,23 @@ msgstr "証明書チェックを無視" msgid "Autofill" msgstr "自動充填" -#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120 +#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:96 #: assets/serializers/platform.py:32 msgid "Username selector" msgstr "ユーザー名ピッカー" -#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121 +#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:97 #: assets/serializers/platform.py:35 msgid "Password selector" msgstr "パスワードセレクター" -#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122 +#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:98 #: 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:300 rbac/tree.py:35 +#: assets/serializers/asset/common.py:297 rbac/tree.py:35 msgid "Accounts" msgstr "アカウント" @@ -1281,8 +1370,8 @@ msgstr "アセットの自動化タスク" #: assets/models/automations/base.py:112 audits/models.py:177 #: 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/models/applet/applet.py:159 terminal/models/applet/host.py:108 +#: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 #: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164 @@ -1307,7 +1396,7 @@ msgid "Date verified" msgstr "確認済みの日付" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 -#: perms/serializers/permission.py:33 users/models/group.py:25 +#: perms/serializers/permission.py:32 users/models/group.py:25 #: users/models/user.py:723 msgid "User group" msgstr "ユーザーグループ" @@ -1340,7 +1429,7 @@ msgstr "コマンドフィルタルール" msgid "Favorite asset" msgstr "お気に入りのアセット" -#: assets/models/gateway.py:35 assets/serializers/domain.py:16 +#: assets/models/gateway.py:34 assets/serializers/domain.py:16 msgid "Gateway" msgstr "ゲートウェイ" @@ -1348,7 +1437,8 @@ msgstr "ゲートウェイ" msgid "Asset group" msgstr "資産グループ" -#: assets/models/group.py:34 assets/models/platform.py:19 +#: assets/models/group.py:34 assets/models/platform.py:17 +#: assets/serializers/platform.py:97 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "デフォルト" @@ -1364,14 +1454,15 @@ msgstr "システム" #: assets/models/label.py:19 assets/models/node.py:544 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:26 -#: authentication/serializers/connect_token_secret.py:115 +#: authentication/serializers/connect_token_secret.py:113 #: common/serializers/common.py:80 settings/models.py:34 msgid "Value" msgstr "値" -#: assets/models/label.py:40 assets/serializers/asset/common.py:129 +#: assets/models/label.py:40 assets/serializers/asset/common.py:105 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 -#: authentication/serializers/connect_token_secret.py:114 +#: assets/serializers/platform.py:94 +#: authentication/serializers/connect_token_secret.py:112 #: common/serializers/common.py:79 settings/serializers/sms.py:7 msgid "Label" msgstr "ラベル" @@ -1396,7 +1487,7 @@ msgstr "フルバリュー" msgid "Parent key" msgstr "親キー" -#: assets/models/node.py:558 perms/serializers/permission.py:36 +#: assets/models/node.py:558 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "ノード" @@ -1405,129 +1496,153 @@ msgstr "ノード" msgid "Can match node" msgstr "ノードを一致させることができます" -#: assets/models/platform.py:20 +#: assets/models/platform.py:15 +msgid "Primary" +msgstr "主要" + +#: assets/models/platform.py:16 msgid "Required" msgstr "必要" -#: assets/models/platform.py:23 settings/serializers/settings.py:65 +#: assets/models/platform.py:18 +#, fuzzy +#| msgid "Public IP" +msgid "Public" +msgstr "パブリックIP" + +#: assets/models/platform.py:19 settings/serializers/settings.py:65 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "設定" -#: assets/models/platform.py:42 audits/const.py:47 settings/models.py:37 +#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37 #: terminal/serializers/applet_host.py:29 msgid "Enabled" msgstr "有効化" -#: assets/models/platform.py:43 +#: assets/models/platform.py:32 msgid "Ansible config" msgstr "Ansible 構成" -#: assets/models/platform.py:44 assets/serializers/platform.py:61 +#: assets/models/platform.py:33 assets/serializers/platform.py:60 msgid "Ping enabled" msgstr "アセット ディスカバリを有効にする" -#: assets/models/platform.py:45 assets/serializers/platform.py:62 +#: assets/models/platform.py:34 assets/serializers/platform.py:61 msgid "Ping method" msgstr "資産検出方法" -#: assets/models/platform.py:46 assets/models/platform.py:59 -#: assets/serializers/platform.py:63 +#: assets/models/platform.py:35 assets/models/platform.py:48 +#: assets/serializers/platform.py:62 msgid "Gather facts enabled" msgstr "資産情報の収集を有効にする" -#: assets/models/platform.py:47 assets/models/platform.py:61 -#: assets/serializers/platform.py:64 +#: assets/models/platform.py:36 assets/models/platform.py:50 +#: assets/serializers/platform.py:63 msgid "Gather facts method" msgstr "情報収集の方法" -#: assets/models/platform.py:48 assets/serializers/platform.py:67 +#: assets/models/platform.py:37 assets/serializers/platform.py:66 msgid "Change secret enabled" msgstr "パスワードの変更が有効" -#: assets/models/platform.py:50 assets/serializers/platform.py:68 +#: assets/models/platform.py:39 assets/serializers/platform.py:67 msgid "Change secret method" msgstr "パスワード変更モード" -#: assets/models/platform.py:52 assets/serializers/platform.py:69 +#: assets/models/platform.py:41 assets/serializers/platform.py:68 msgid "Push account enabled" msgstr "アカウントのプッシュを有効にする" -#: assets/models/platform.py:54 assets/serializers/platform.py:70 +#: assets/models/platform.py:43 assets/serializers/platform.py:69 msgid "Push account method" msgstr "アカウントプッシュ方式" -#: assets/models/platform.py:56 assets/serializers/platform.py:65 +#: assets/models/platform.py:45 assets/serializers/platform.py:64 msgid "Verify account enabled" msgstr "アカウントの確認をオンにする" -#: assets/models/platform.py:58 assets/serializers/platform.py:66 +#: assets/models/platform.py:47 assets/serializers/platform.py:65 msgid "Verify account method" msgstr "アカウント認証方法" -#: assets/models/platform.py:79 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:68 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:80 +#: assets/models/platform.py:69 msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:83 assets/serializers/platform.py:91 +#: assets/models/platform.py:73 assets/serializers/platform.py:109 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:85 assets/serializers/platform.py:119 +#: assets/models/platform.py:75 assets/serializers/platform.py:133 msgid "Domain enabled" msgstr "ドメインを有効にする" -#: assets/models/platform.py:87 assets/serializers/platform.py:118 +#: assets/models/platform.py:77 assets/serializers/platform.py:132 msgid "Su enabled" msgstr "アカウントの切り替えを有効にする" -#: assets/models/platform.py:88 assets/serializers/platform.py:101 +#: assets/models/platform.py:78 assets/serializers/platform.py:115 msgid "Su method" msgstr "アカウントの切り替え方法" -#: assets/models/platform.py:90 assets/serializers/platform.py:98 +#: assets/models/platform.py:81 assets/serializers/platform.py:113 msgid "Automation" msgstr "オートメーション" +#: assets/models/platform.py:83 assets/serializers/platform.py:118 +#, fuzzy +#| msgid "Custom user" +msgid "Custom fields" +msgstr "カスタムユーザー" + #: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s は偶数ではありません" -#: assets/serializers/asset/common.py:119 +#: assets/notifications.py:12 +msgid "" +"Batch update platform in assets, skipping assets that do not meet platform " +"type" +msgstr "" +"プラットフォームタイプがスキップされた資産に合致しない、資産内の一括更新プ" +"ラットフォーム" + +#: assets/serializers/asset/common.py:95 msgid "Auto fill" msgstr "自動充填" -#: 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 +#: assets/serializers/asset/common.py:106 assets/serializers/platform.py:112 +#: authentication/serializers/connect_token_secret.py:29 +#: authentication/serializers/connect_token_secret.py:63 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 msgid "Protocols" msgstr "プロトコル" -#: assets/serializers/asset/common.py:132 -#: assets/serializers/asset/common.py:153 +#: assets/serializers/asset/common.py:108 +#: assets/serializers/asset/common.py:130 msgid "Node path" msgstr "ノードパスです" -#: assets/serializers/asset/common.py:150 -#: assets/serializers/asset/common.py:302 +#: assets/serializers/asset/common.py:127 +#: assets/serializers/asset/common.py:299 msgid "Auto info" msgstr "自動情報" -#: assets/serializers/asset/common.py:226 +#: assets/serializers/asset/common.py:220 msgid "Platform not exist" msgstr "プラットフォームが存在しません" -#: assets/serializers/asset/common.py:261 +#: assets/serializers/asset/common.py:255 msgid "port out of range (1-65535)" msgstr "ポート番号が範囲外です (1-65535)" -#: assets/serializers/asset/common.py:268 +#: assets/serializers/asset/common.py:262 msgid "Protocol is required: {}" msgstr "プロトコルが必要です: {}" @@ -1576,7 +1691,7 @@ msgid "Disk total" msgstr "ディスクの合計" #: assets/serializers/asset/host.py:21 -#: authentication/serializers/connect_token_secret.py:105 +#: authentication/serializers/connect_token_secret.py:103 msgid "OS" msgstr "OS" @@ -1628,26 +1743,34 @@ msgstr "SFTP が有効" msgid "SFTP home" msgstr "SFTP ルート パス" -#: assets/serializers/platform.py:43 +#: assets/serializers/platform.py:42 msgid "Auth with username" msgstr "ユーザー名で認証する" -#: assets/serializers/platform.py:71 +#: assets/serializers/platform.py:70 msgid "Gather accounts enabled" msgstr "アカウント収集を有効にする" -#: assets/serializers/platform.py:72 +#: assets/serializers/platform.py:71 msgid "Gather accounts method" msgstr "アカウントの収集方法" -#: assets/serializers/platform.py:78 -msgid "Primary" -msgstr "主要" - -#: assets/serializers/platform.py:120 +#: assets/serializers/platform.py:134 msgid "Default Domain" msgstr "デフォルト ドメイン" +#: assets/serializers/platform.py:143 +#, fuzzy +#| msgid "test_phone is required" +msgid "type is required" +msgstr "携帯番号をテストこのフィールドは必須です" + +#: assets/serializers/platform.py:173 +#, fuzzy +#| msgid "Protocol is required: {}" +msgid "Protocols is required" +msgstr "プロトコルが必要です: {}" + #: assets/signal_handlers/asset.py:26 assets/tasks/ping.py:35 msgid "Test assets connectivity " msgstr "アセット接続のテスト。" @@ -1762,11 +1885,6 @@ msgstr "ダウンロード" msgid "View" msgstr "表示" -#: audits/const.py:23 rbac/tree.py:229 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" - #: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:227 @@ -1788,7 +1906,7 @@ msgid "Change password" msgstr "パスワードを変更する" #: audits/const.py:34 settings/serializers/terminal.py:6 -#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:156 +#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163 #: terminal/serializers/session.py:48 msgid "Terminal" msgstr "ターミナル" @@ -1827,7 +1945,7 @@ msgid "Job audit log" msgstr "ジョブ監査ログ" #: audits/models.py:46 audits/models.py:73 audits/models.py:144 -#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:95 +#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99 msgid "Remote addr" msgstr "リモートaddr" @@ -2463,23 +2581,23 @@ msgstr "異なる都市ログインのリマインダー" msgid "binding reminder" msgstr "バインディングリマインダー" -#: authentication/serializers/connect_token_secret.py:106 +#: authentication/serializers/connect_token_secret.py:104 msgid "Is builtin" msgstr "ビルトイン" -#: authentication/serializers/connect_token_secret.py:110 +#: authentication/serializers/connect_token_secret.py:108 msgid "Options" msgstr "オプション" -#: authentication/serializers/connect_token_secret.py:117 +#: authentication/serializers/connect_token_secret.py:115 msgid "Component" msgstr "コンポーネント" -#: authentication/serializers/connect_token_secret.py:128 +#: authentication/serializers/connect_token_secret.py:126 msgid "Expired now" msgstr "すぐに期限切れ" -#: authentication/serializers/connect_token_secret.py:148 +#: authentication/serializers/connect_token_secret.py:145 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:33 msgid "ID" @@ -2494,15 +2612,15 @@ msgid "Ticket info" msgstr "作業指示情報" #: authentication/serializers/connection_token.py:20 -#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37 -#: perms/serializers/permission.py:70 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 +#: perms/serializers/permission.py:69 #: 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:39 perms/serializers/permission.py:71 +#: perms/serializers/permission.py:38 perms/serializers/permission.py:70 #: users/serializers/user.py:93 users/serializers/user.py:165 msgid "Is expired" msgstr "期限切れです" @@ -2522,8 +2640,8 @@ msgstr "メール" msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:38 -#: perms/serializers/permission.py:72 users/serializers/user.py:94 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: perms/serializers/permission.py:71 users/serializers/user.py:94 #: users/serializers/user.py:163 msgid "Is valid" msgstr "有効です" @@ -2544,11 +2662,6 @@ msgstr "APIキー記号APIヘッダーを使用すると、すべてのリクエ msgid "docs" msgstr "ドキュメント" -#: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:98 terminal/notifications.py:146 -msgid "Date" -msgstr "日付" - #: authentication/templates/authentication/_access_key_modal.html:48 msgid "Show" msgstr "表示" @@ -2664,7 +2777,7 @@ msgid "request new one" msgstr "新しいものを要求する" #: authentication/templates/authentication/_msg_reset_password_code.html:12 -#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:79 +#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:83 #: users/forms/profile.py:104 users/templates/users/forgot_password.html:65 msgid "Verify code" msgstr "コードの確認" @@ -2991,7 +3104,7 @@ msgstr "組織 ID" msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "ファイルの内容がオーバーフローしました (最大長 '{}' バイト)" -#: common/drf/parsers/base.py:189 +#: common/drf/parsers/base.py:193 msgid "Parse file error: {}" msgstr "解析ファイルエラー: {}" @@ -3153,19 +3266,24 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" -#: common/views/mixins.py:58 +#: common/views/mixins.py:73 msgid "Export all" msgstr "すべてエクスポート" -#: common/views/mixins.py:60 +#: common/views/mixins.py:75 msgid "Export only selected items" msgstr "選択項目のみエクスポート" -#: common/views/mixins.py:65 +#: common/views/mixins.py:80 #, python-format msgid "Export filtered: %s" msgstr "検索のエクスポート: %s" +#: common/views/mixins.py:89 +#, python-format +msgid "User %s view/export secret" +msgstr "ユーザー %s がパスワードを閲覧/導き出しました" + #: jumpserver/conf.py:416 msgid "Create account successfully" msgstr "アカウントを正常に作成" @@ -3242,11 +3360,11 @@ msgstr "投稿サイトニュース" msgid "No account available" msgstr "利用可能なアカウントがありません" -#: ops/ansible/inventory.py:189 +#: ops/ansible/inventory.py:209 msgid "Ansible disabled" msgstr "Ansible 無効" -#: ops/ansible/inventory.py:205 +#: ops/ansible/inventory.py:225 msgid "Skip hosts below:" msgstr "次のホストをスキップします: " @@ -3322,10 +3440,6 @@ msgstr "特権アカウントのみ" msgid "Privileged First" msgstr "特権アカウント優先" -#: ops/const.py:45 -msgid "Skip" -msgstr "スキップ" - #: ops/const.py:50 msgid "Powershell" msgstr "PowerShell" @@ -3423,14 +3537,7 @@ msgstr "タスクモニターを表示できます" msgid "Kwargs" msgstr "クワーグ" -#: ops/models/celery.py:60 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 -#: tickets/serializers/super_ticket.py:14 -#: tickets/serializers/ticket/ticket.py:21 -msgid "State" -msgstr "状態" - -#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:114 #: tickets/const.py:25 msgid "Finished" msgstr "終了" @@ -3943,7 +4050,7 @@ msgid "My assets" msgstr "私の資産" #: rbac/tree.py:56 terminal/models/applet/applet.py:43 -#: terminal/models/applet/applet.py:154 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "リモートアプリケーション" @@ -4919,43 +5026,39 @@ msgid "Remember manual auth" msgstr "手動入力パスワードの保存" #: settings/serializers/security.py:172 -msgid "Enable change auth secure mode" -msgstr "安全モードの変更を有効にする" - -#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "安全でないコマンドアラート" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:175 msgid "Email recipient" msgstr "メール受信者" -#: settings/serializers/security.py:179 +#: settings/serializers/security.py:176 msgid "Multiple user using , split" msgstr "複数のユーザーを使用して、分割" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:179 msgid "Operation center" msgstr "職業センター" -#: settings/serializers/security.py:183 +#: settings/serializers/security.py:180 msgid "Allow user run batch command or not using ansible" msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" -#: settings/serializers/security.py:186 +#: settings/serializers/security.py:183 msgid "Session share" msgstr "セッション共有" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:184 msgid "Enabled, Allows user active session to be shared with other users" msgstr "" "ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。" -#: settings/serializers/security.py:190 +#: settings/serializers/security.py:187 msgid "Remote Login Protection" msgstr "リモートログイン保護" -#: settings/serializers/security.py:192 +#: settings/serializers/security.py:189 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -5037,11 +5140,11 @@ msgstr "" "ヒント: Luna ページでグラフィック アセットを接続するときに使用するデフォルト" "の解像度" -#: settings/tasks/ldap.py:24 +#: settings/tasks/ldap.py:25 msgid "Import ldap user" msgstr "LDAP ユーザーのインポート" -#: settings/tasks/ldap.py:45 +#: settings/tasks/ldap.py:47 msgid "Periodic import ldap user" msgstr "LDAP ユーザーを定期的にインポートする" @@ -5424,7 +5527,7 @@ msgid "Output" msgstr "出力" #: terminal/backends/command/models.py:24 terminal/models/session/replay.py:9 -#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77 +#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:81 #: terminal/templates/terminal/_msg_command_alert.html:10 #: tickets/models/ticket/command_confirm.py:15 msgid "Session" @@ -5465,6 +5568,14 @@ msgstr "一致しない" msgid "Tunnel" msgstr "" +#: terminal/const.py:71 +msgid "Read Only" +msgstr "読み取り専用" + +#: terminal/const.py:72 +msgid "Writable" +msgstr "書き込み可能" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "一括作成非サポート" @@ -5489,7 +5600,7 @@ msgstr "ホスト" msgid "Applet pkg not valid, Missing file {}" msgstr "無効なアプレット パッケージ、ファイル {} がありません" -#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:158 terminal/models/applet/host.py:34 #: terminal/models/applet/host.py:106 msgid "Hosting" msgstr "ホスト マシン" @@ -5547,7 +5658,7 @@ msgid "Redis port" msgstr "Redis ポート" #: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:66 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 @@ -5562,31 +5673,31 @@ msgstr "IP グループ" msgid "Endpoint rule" msgstr "エンドポイントルール" -#: terminal/models/component/status.py:14 +#: terminal/models/component/status.py:15 msgid "Session Online" msgstr "セッションオンライン" -#: terminal/models/component/status.py:15 +#: terminal/models/component/status.py:16 msgid "CPU Load" msgstr "CPUロード" -#: terminal/models/component/status.py:16 +#: terminal/models/component/status.py:17 msgid "Memory Used" msgstr "使用メモリ" -#: terminal/models/component/status.py:17 +#: terminal/models/component/status.py:18 msgid "Disk Used" msgstr "使用済みディスク" -#: terminal/models/component/status.py:18 +#: terminal/models/component/status.py:19 msgid "Connections" msgstr "接続" -#: terminal/models/component/status.py:19 +#: terminal/models/component/status.py:20 msgid "Threads" msgstr "スレッド" -#: terminal/models/component/status.py:20 +#: terminal/models/component/status.py:21 msgid "Boot Time" msgstr "ブート時間" @@ -5595,28 +5706,28 @@ msgid "Default storage" msgstr "デフォルトのストレージ" #: terminal/models/component/storage.py:140 -#: terminal/models/component/terminal.py:85 +#: terminal/models/component/terminal.py:91 msgid "Command storage" msgstr "コマンドストレージ" #: terminal/models/component/storage.py:200 -#: terminal/models/component/terminal.py:86 +#: terminal/models/component/terminal.py:92 msgid "Replay storage" msgstr "再生ストレージ" -#: terminal/models/component/terminal.py:82 +#: terminal/models/component/terminal.py:88 msgid "type" msgstr "タイプ" -#: terminal/models/component/terminal.py:84 terminal/serializers/command.py:51 +#: terminal/models/component/terminal.py:90 terminal/serializers/command.py:51 msgid "Remote Address" msgstr "リモートアドレス" -#: terminal/models/component/terminal.py:87 +#: terminal/models/component/terminal.py:93 msgid "Application User" msgstr "ユーザーの適用" -#: terminal/models/component/terminal.py:158 +#: terminal/models/component/terminal.py:165 msgid "Can view terminal config" msgstr "ターミナル構成を表示できます" @@ -5640,7 +5751,7 @@ msgstr "セッション再生をダウンロードできます" msgid "Account id" msgstr "アカウント ID" -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:100 +#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104 msgid "Login from" msgstr "ログイン元" @@ -5676,43 +5787,48 @@ msgstr "セッションアクションのパーマを検証できます" msgid "Expired time (min)" msgstr "期限切れ時間 (分)" -#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82 +#: terminal/models/session/sharing.py:35 terminal/serializers/sharing.py:20 +#: terminal/serializers/sharing.py:52 +msgid "Action permission" +msgstr "アクションパーミッション" + +#: terminal/models/session/sharing.py:40 terminal/models/session/sharing.py:86 msgid "Session sharing" msgstr "セッション共有" -#: terminal/models/session/sharing.py:38 +#: terminal/models/session/sharing.py:42 msgid "Can add super session sharing" msgstr "スーパーセッション共有を追加できます" -#: terminal/models/session/sharing.py:65 +#: terminal/models/session/sharing.py:69 msgid "Link not active" msgstr "リンクがアクティブでない" -#: terminal/models/session/sharing.py:67 +#: terminal/models/session/sharing.py:71 msgid "Link expired" msgstr "リンク期限切れ" -#: terminal/models/session/sharing.py:69 +#: terminal/models/session/sharing.py:73 msgid "User not allowed to join" msgstr "ユーザーはセッションに参加できません" -#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59 +#: terminal/models/session/sharing.py:90 terminal/serializers/sharing.py:71 msgid "Joiner" msgstr "ジョイナー" -#: terminal/models/session/sharing.py:89 +#: terminal/models/session/sharing.py:93 msgid "Date joined" msgstr "参加日" -#: terminal/models/session/sharing.py:92 +#: terminal/models/session/sharing.py:96 msgid "Date left" msgstr "日付が残っています" -#: terminal/models/session/sharing.py:115 +#: terminal/models/session/sharing.py:119 msgid "Session join record" msgstr "セッション参加記録" -#: terminal/models/session/sharing.py:131 +#: terminal/models/session/sharing.py:135 msgid "Invalid verification code" msgstr "検証コードが無効" @@ -5811,15 +5927,15 @@ msgstr "アカウント" msgid "Timestamp" msgstr "タイムスタンプ" -#: terminal/serializers/endpoint.py:14 +#: terminal/serializers/endpoint.py:15 msgid "Oracle port" msgstr "Oracle ポート" -#: terminal/serializers/endpoint.py:17 +#: terminal/serializers/endpoint.py:18 msgid "Oracle port range" msgstr "Oracle がリッスンするポート範囲" -#: terminal/serializers/endpoint.py:19 +#: terminal/serializers/endpoint.py:20 msgid "" "Oracle proxy server listen port is dynamic, Each additional Oracle database " "instance adds a port listener" @@ -5827,13 +5943,13 @@ msgstr "" "Oracle プロキシサーバーがリッスンするポートは動的です。追加の Oracle データ" "ベースインスタンスはポートリスナーを追加します" -#: terminal/serializers/endpoint.py:35 +#: terminal/serializers/endpoint.py:36 msgid "Visit IP/Host, if empty, use the current request instead" msgstr "" "IP/ホストにアクセスします。空の場合は、代わりに現在のリクエストのアドレスを使" "用します" -#: terminal/serializers/endpoint.py:58 +#: terminal/serializers/endpoint.py:59 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "" @@ -6573,7 +6689,7 @@ msgstr "MFAフォース有効化" #: users/serializers/user.py:92 msgid "Login blocked" -msgstr "ログインブロック" +msgstr "ログインがロックされました" #: users/serializers/user.py:95 users/serializers/user.py:169 msgid "Is OTP bound" @@ -7449,3 +7565,8 @@ msgstr "究極のエディション" #: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "コミュニティ版" + +#, fuzzy +#~| msgid "Custom user" +#~ msgid "Custom" +#~ msgstr "カスタムユーザー" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 360ae7c4b..3dbd987da 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:9819889a6d8b2934b06c5b242e3f63f404997f30851919247a405f542e8a03bc -size 113244 +oid sha256:035f9429613b541f229855a7d36c98e5f4736efce54dcd21119660dd6d89d94e +size 114269 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index ba0a34c84..29286bd92 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-03-14 17:34+0800\n" +"POT-Creation-Date: 2023-04-07 13:57+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -69,6 +69,22 @@ msgstr "数据库" msgid "Collected" msgstr "收集" +#: accounts/const/account.py:24 ops/const.py:45 +msgid "Skip" +msgstr "跳过" + +#: accounts/const/account.py:25 audits/const.py:23 rbac/tree.py:229 +#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 +msgid "Update" +msgstr "更新" + +#: accounts/const/account.py:26 +#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 +#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 +#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失败" + #: accounts/const/automation.py:22 rbac/tree.py:50 msgid "Push account" msgstr "账号推送" @@ -158,25 +174,26 @@ msgid "Only create" msgstr "仅创建" #: accounts/models/account.py:47 -#: accounts/models/automations/gather_account.py:14 -#: accounts/serializers/account/account.py:95 +#: accounts/models/automations/gather_account.py:16 +#: accounts/serializers/account/account.py:173 +#: accounts/serializers/account/account.py:206 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:131 #: acls/models/base.py:100 acls/serializers/base.py:56 -#: assets/models/asset/common.py:92 assets/models/asset/common.py:280 +#: assets/models/asset/common.py:92 assets/models/asset/common.py:268 #: 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:35 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 #: 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 msgid "Asset" msgstr "资产" -#: accounts/models/account.py:51 accounts/serializers/account/account.py:99 -#: authentication/serializers/connect_token_secret.py:50 +#: accounts/models/account.py:51 accounts/serializers/account/account.py:178 +#: authentication/serializers/connect_token_secret.py:48 msgid "Su from" msgstr "切换自" @@ -185,52 +202,56 @@ msgstr "切换自" msgid "Version" msgstr "版本" -#: accounts/models/account.py:55 accounts/serializers/account/account.py:96 +#: accounts/models/account.py:55 accounts/serializers/account/account.py:174 #: users/models/user.py:768 msgid "Source" msgstr "来源" -#: accounts/models/account.py:58 +#: accounts/models/account.py:56 +msgid "Source ID" +msgstr "来源 ID" + +#: accounts/models/account.py:59 #: 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:131 assets/serializers/gateway.py:28 +#: assets/serializers/asset/common.py:107 assets/serializers/gateway.py:28 #: audits/models.py:49 ops/models/base.py:18 -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40 +#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 #: 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" msgstr "账号" -#: accounts/models/account.py:64 +#: accounts/models/account.py:65 msgid "Can view asset account secret" msgstr "可以查看资产账号密码" -#: accounts/models/account.py:65 +#: accounts/models/account.py:66 msgid "Can view asset history account" msgstr "可以查看资产历史账号" -#: accounts/models/account.py:66 +#: accounts/models/account.py:67 msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" -#: accounts/models/account.py:67 +#: accounts/models/account.py:68 msgid "Can verify account" msgstr "可以验证账号" -#: accounts/models/account.py:68 +#: accounts/models/account.py:69 msgid "Can push account" msgstr "可以推送账号" -#: accounts/models/account.py:109 +#: accounts/models/account.py:110 msgid "Account template" msgstr "账号模版" -#: accounts/models/account.py:114 +#: accounts/models/account.py:115 msgid "Can view asset account template secret" msgstr "可以查看资产账号模版密码" -#: accounts/models/account.py:115 +#: accounts/models/account.py:116 msgid "Can change asset account template secret" msgstr "可以更改资产账号模版密码" @@ -275,7 +296,7 @@ msgid "Trigger mode" msgstr "触发模式" #: accounts/models/automations/backup_account.py:97 audits/models.py:172 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:168 +#: terminal/models/session/sharing.py:111 xpack/plugins/cloud/models.py:168 msgid "Reason" msgstr "原因" @@ -327,16 +348,17 @@ msgid "Can add push account execution" msgstr "创建推送账号执行" #: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36 -#: accounts/serializers/account/account.py:134 +#: accounts/serializers/account/account.py:383 #: accounts/serializers/account/base.py:16 #: accounts/serializers/automations/change_secret.py:46 -#: authentication/serializers/connect_token_secret.py:41 -#: authentication/serializers/connect_token_secret.py:51 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:49 msgid "Secret type" msgstr "密文类型" #: accounts/models/automations/change_secret.py:20 #: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 #: settings/serializers/auth/radius.py:19 @@ -375,7 +397,8 @@ msgstr "开始日期" msgid "Date finished" msgstr "结束日期" -#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8 +#: accounts/models/automations/change_secret.py:94 +#: accounts/serializers/account/account.py:208 assets/const/automation.py:8 #: common/const/choices.py:20 msgid "Error" msgstr "错误" @@ -384,15 +407,15 @@ msgstr "错误" msgid "Change secret record" msgstr "改密记录" -#: accounts/models/automations/gather_account.py:12 +#: accounts/models/automations/gather_account.py:14 msgid "Present" msgstr "存在" -#: accounts/models/automations/gather_account.py:13 +#: accounts/models/automations/gather_account.py:15 msgid "Date last login" msgstr "最后登录日期" -#: accounts/models/automations/gather_account.py:15 +#: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 #: acls/serializers/base.py:18 acls/serializers/base.py:49 #: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25 @@ -405,15 +428,19 @@ msgstr "最后登录日期" msgid "Username" msgstr "用户名" -#: accounts/models/automations/gather_account.py:16 +#: accounts/models/automations/gather_account.py:18 msgid "Address last login" msgstr "最后登录地址" -#: accounts/models/automations/gather_account.py:23 +#: accounts/models/automations/gather_account.py:44 msgid "Gather account automation" msgstr "自动化收集账号" -#: accounts/models/automations/gather_account.py:39 +#: accounts/models/automations/gather_account.py:56 +msgid "Is sync account" +msgstr "是否同步账号" + +#: accounts/models/automations/gather_account.py:71 #: accounts/tasks/gather_accounts.py:29 msgid "Gather asset accounts" msgstr "收集账号" @@ -425,7 +452,7 @@ msgstr "触发方式" #: 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 +#: authentication/serializers/connect_token_secret.py:107 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" msgstr "动作" @@ -444,10 +471,10 @@ msgstr "账号验证" #: assets/models/asset/common.py:90 assets/models/asset/common.py:102 #: 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:74 assets/serializers/asset/common.py:151 -#: assets/serializers/platform.py:133 -#: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21 +#: assets/models/platform.py:13 assets/models/platform.py:65 +#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93 +#: assets/serializers/platform.py:184 +#: authentication/serializers/connect_token_secret.py:101 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 #: orgs/models.py:69 perms/models/asset_permission.py:56 rbac/models/role.py:29 @@ -455,7 +482,7 @@ msgstr "账号验证" #: terminal/models/applet/applet.py:27 terminal/models/component/endpoint.py:12 #: terminal/models/component/endpoint.py:90 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: terminal/models/component/terminal.py:85 users/forms/profile.py:33 #: users/models/group.py:13 users/models/user.py:717 #: xpack/plugins/cloud/models.py:28 msgid "Name" @@ -468,7 +495,7 @@ msgstr "特权账号" #: accounts/models/base.py:40 assets/models/asset/common.py:109 #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:107 +#: authentication/serializers/connect_token_secret.py:105 #: terminal/models/applet/applet.py:32 users/serializers/user.py:162 msgid "Is active" msgstr "激活" @@ -511,38 +538,36 @@ msgstr "" "{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" "密密码" -#: accounts/serializers/account/account.py:65 -#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75 +#: accounts/serializers/account/account.py:26 +#: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "模板" -#: accounts/serializers/account/account.py:68 -#: assets/serializers/asset/common.py:69 +#: accounts/serializers/account/account.py:29 msgid "Push now" msgstr "立即推送" -#: accounts/serializers/account/account.py:70 -#: accounts/serializers/account/base.py:64 -msgid "Has secret" -msgstr "已托管密码" +#: accounts/serializers/account/account.py:33 +msgid "Exist policy" +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:127 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:94 assets/serializers/platform.py:134 +#: accounts/serializers/account/account.py:153 applications/models.py:11 +#: assets/models/label.py:21 assets/models/platform.py:66 +#: assets/serializers/asset/common.py:103 assets/serializers/cagegory.py:8 +#: assets/serializers/platform.py:111 assets/serializers/platform.py:185 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "类别" -#: accounts/serializers/account/account.py:76 +#: accounts/serializers/account/account.py:154 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 -#: assets/models/cmd_filter.py:74 assets/models/platform.py:78 -#: 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 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:67 +#: assets/serializers/asset/common.py:104 assets/serializers/platform.py:95 +#: assets/serializers/platform.py:110 audits/serializers.py:48 +#: authentication/serializers/connect_token_secret.py:114 ops/models/job.py:102 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29 @@ -554,10 +579,72 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: accounts/serializers/account/account.py:91 +#: accounts/serializers/account/account.py:169 msgid "Asset not found" msgstr "资产不存在" +#: accounts/serializers/account/account.py:175 +#: accounts/serializers/account/base.py:64 +msgid "Has secret" +msgstr "已托管密码" + +#: accounts/serializers/account/account.py:207 ops/models/celery.py:60 +#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45 +#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14 +#: tickets/serializers/ticket/ticket.py:21 +msgid "State" +msgstr "状态" + +#: accounts/serializers/account/account.py:209 +msgid "Changed" +msgstr "已修改" + +#: accounts/serializers/account/account.py:213 +#: accounts/serializers/automations/base.py:22 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:104 ops/serializers/job.py:20 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "资产" + +#: accounts/serializers/account/account.py:284 +msgid "Account already exists" +msgstr "账号已存在" + +#: accounts/serializers/account/account.py:320 +#, python-format +msgid "Asset does not support this secret type: %s" +msgstr "资产不支持账号类型: %s" + +#: accounts/serializers/account/account.py:351 +msgid "Account has exist" +msgstr "账号已存在" + +#: accounts/serializers/account/account.py:393 acls/models/base.py:98 +#: acls/models/login_acl.py:13 acls/serializers/base.py:55 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24 +#: assets/models/label.py:16 audits/models.py:44 audits/models.py:63 +#: audits/models.py:141 authentication/models/connection_token.py:29 +#: 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:30 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 +#: terminal/serializers/command.py:16 tickets/models/comment.py:21 +#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 +#: users/serializers/group.py:18 +msgid "User" +msgstr "用户" + +#: accounts/serializers/account/account.py:394 +#: authentication/templates/authentication/_access_key_modal.html:33 +#: terminal/notifications.py:98 terminal/notifications.py:146 +msgid "Date" +msgstr "日期" + #: accounts/serializers/account/backup.py:31 #: accounts/serializers/automations/base.py:36 #: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103 @@ -579,27 +666,15 @@ msgstr "当前只支持邮件发送" msgid "Asset type" msgstr "资产类型" -#: accounts/serializers/account/base.py:19 -msgid "Secret/Password" -msgstr "密钥/密码" - #: accounts/serializers/account/base.py:24 msgid "Key password" msgstr "密钥密码" -#: accounts/serializers/account/base.py:81 -#: assets/serializers/asset/common.py:301 +#: accounts/serializers/account/base.py:80 +#: assets/serializers/asset/common.py:298 msgid "Spec info" msgstr "特殊信息" -#: accounts/serializers/automations/base.py:22 -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:104 ops/serializers/job.py:20 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "资产" - #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:108 assets/models/automations/base.py:18 #: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 @@ -637,17 +712,11 @@ msgstr "自动化任务执行历史" #: accounts/serializers/automations/change_secret.py:154 audits/const.py:52 #: audits/models.py:54 audits/signal_handlers/activity_log.py:33 #: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39 -#: terminal/const.py:59 terminal/models/session/sharing.py:103 +#: terminal/const.py:59 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" -#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 -#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失败" - #: accounts/tasks/automation.py:24 msgid "Account execute automation" msgstr "账号执行自动化" @@ -717,7 +786,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" #: acls/models/base.py:82 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 -#: authentication/serializers/connect_token_secret.py:82 +#: authentication/serializers/connect_token_secret.py:79 msgid "Reviewers" msgstr "审批人" @@ -729,24 +798,6 @@ msgstr "审批人" msgid "Active" msgstr "激活中" -#: acls/models/base.py:98 acls/models/login_acl.py:13 -#: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 -#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:44 -#: audits/models.py:63 audits/models.py:141 -#: authentication/models/connection_token.py:29 -#: 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: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 -#: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:911 users/models/user.py:942 -#: users/serializers/group.py:18 -msgid "User" -msgstr "用户" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 #: ops/serializers/job.py:65 terminal/const.py:67 #: terminal/models/session/session.py:43 terminal/serializers/command.py:18 @@ -774,7 +825,7 @@ msgstr "忽略大小写" #: acls/models/command_acl.py:33 acls/models/command_acl.py:96 #: acls/serializers/command_acl.py:28 -#: authentication/serializers/connect_token_secret.py:79 +#: authentication/serializers/connect_token_secret.py:76 msgid "Command group" msgstr "命令组" @@ -877,7 +928,7 @@ msgstr "" #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:62 msgid "IP" msgstr "IP" @@ -902,7 +953,7 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" -#: assets/api/asset/asset.py:143 +#: assets/api/asset/asset.py:147 msgid "Cannot create asset directly, you should create a host or other" msgstr "不能直接创建资产, 你应该创建主机或其他资产" @@ -976,31 +1027,35 @@ msgstr "测试网关" msgid "Gather facts" msgstr "收集资产信息" -#: assets/const/category.py:11 assets/models/asset/host.py:8 +#: assets/const/category.py:10 assets/models/asset/host.py:8 #: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 #: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" -#: assets/const/category.py:12 assets/models/asset/device.py:8 +#: assets/const/category.py:11 assets/models/asset/device.py:8 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:115 +#: assets/const/category.py:12 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:91 msgid "Database" msgstr "数据库" -#: assets/const/category.py:14 +#: assets/const/category.py:13 msgid "Cloud service" msgstr "云服务" -#: assets/const/category.py:15 assets/models/asset/web.py:16 audits/const.py:33 +#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:33 #: terminal/models/applet/applet.py:25 msgid "Web" msgstr "Web" +#: assets/const/category.py:15 +msgid "Custom type" +msgstr "自定义类型" + #: assets/const/cloud.py:7 msgid "Public cloud" msgstr "公有云" @@ -1034,7 +1089,7 @@ msgstr "防火墙" msgid "Other" msgstr "其它" -#: assets/const/types.py:200 +#: assets/const/types.py:214 msgid "All types" msgstr "所有类型" @@ -1052,7 +1107,7 @@ msgid "Basic" msgstr "基本" #: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40 +#: assets/serializers/asset/common.py:99 assets/serializers/platform.py:40 msgid "Script" msgstr "脚本" @@ -1072,7 +1127,7 @@ msgstr "SSH公钥" #: assets/models/cmd_filter.py:88 assets/models/group.py:23 #: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:110 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:158 +#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:160 #: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:47 tickets/models/comment.py:32 @@ -1121,7 +1176,7 @@ msgid "Username same with user" msgstr "用户名与用户相同" #: assets/models/_user.py:52 authentication/models/connection_token.py:38 -#: authentication/serializers/connect_token_secret.py:104 +#: authentication/serializers/connect_token_secret.py:102 #: terminal/models/applet/applet.py:34 terminal/serializers/session.py:20 #: terminal/serializers/session.py:41 terminal/serializers/storage.py:68 msgid "Protocol" @@ -1175,25 +1230,25 @@ msgstr "可以匹配系统用户" msgid "Cloud" msgstr "云服务" -#: assets/models/asset/common.py:91 assets/models/platform.py:22 +#: assets/models/asset/common.py:91 assets/models/platform.py:14 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" -#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152 +#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:129 msgid "Address" msgstr "地址" -#: assets/models/asset/common.py:104 assets/models/platform.py:112 -#: authentication/serializers/connect_token_secret.py:108 +#: assets/models/asset/common.py:104 assets/models/platform.py:100 +#: authentication/serializers/connect_token_secret.py:106 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:187 msgid "Platform" msgstr "系统平台" #: assets/models/asset/common.py:106 assets/models/domain.py:21 -#: authentication/serializers/connect_token_secret.py:126 +#: authentication/serializers/connect_token_secret.py:124 #: perms/serializers/user_permission.py:28 msgid "Domain" msgstr "网域" @@ -1202,23 +1257,40 @@ msgstr "网域" msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:283 +#: assets/models/asset/common.py:111 +#, fuzzy +#| msgid "Gather asset hardware info" +msgid "Gathered info" +msgstr "收集资产硬件信息" + +#: assets/models/asset/common.py:112 assets/serializers/asset/common.py:109 +#: assets/serializers/asset/common.py:179 +#, fuzzy +#| msgid "Auto info" +msgid "Custom info" +msgstr "自动化信息" + +#: assets/models/asset/common.py:271 msgid "Can refresh asset hardware info" msgstr "可以更新资产硬件信息" -#: assets/models/asset/common.py:284 +#: assets/models/asset/common.py:272 msgid "Can test asset connectivity" msgstr "可以测试资产连接性" -#: assets/models/asset/common.py:285 +#: assets/models/asset/common.py:273 msgid "Can match asset" msgstr "可以匹配资产" -#: assets/models/asset/common.py:286 +#: assets/models/asset/common.py:274 msgid "Can change asset nodes" msgstr "可以修改资产节点" -#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116 +#: assets/models/asset/custom.py:8 +msgid "Custom asset" +msgstr "自定义资产" + +#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:92 #: settings/serializers/email.py:37 msgid "Use SSL" msgstr "使用 SSL" @@ -1235,7 +1307,7 @@ msgstr "客户端证书" msgid "Client key" msgstr "客户端密钥" -#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117 +#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:93 msgid "Allow invalid cert" msgstr "忽略证书校验" @@ -1243,23 +1315,23 @@ msgstr "忽略证书校验" msgid "Autofill" msgstr "自动代填" -#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120 +#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:96 #: assets/serializers/platform.py:32 msgid "Username selector" msgstr "用户名选择器" -#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121 +#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:97 #: assets/serializers/platform.py:35 msgid "Password selector" msgstr "密码选择器" -#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122 +#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:98 #: 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:300 rbac/tree.py:35 +#: assets/serializers/asset/common.py:297 rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" @@ -1273,8 +1345,8 @@ msgstr "资产自动化任务" #: assets/models/automations/base.py:112 audits/models.py:177 #: 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/models/applet/applet.py:159 terminal/models/applet/host.py:108 +#: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 #: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164 @@ -1299,7 +1371,7 @@ msgid "Date verified" msgstr "校验日期" #: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 -#: perms/serializers/permission.py:33 users/models/group.py:25 +#: perms/serializers/permission.py:32 users/models/group.py:25 #: users/models/user.py:723 msgid "User group" msgstr "用户组" @@ -1332,7 +1404,7 @@ msgstr "命令过滤规则" msgid "Favorite asset" msgstr "收藏的资产" -#: assets/models/gateway.py:35 assets/serializers/domain.py:16 +#: assets/models/gateway.py:34 assets/serializers/domain.py:16 msgid "Gateway" msgstr "网关" @@ -1340,7 +1412,8 @@ msgstr "网关" msgid "Asset group" msgstr "资产组" -#: assets/models/group.py:34 assets/models/platform.py:19 +#: assets/models/group.py:34 assets/models/platform.py:17 +#: assets/serializers/platform.py:97 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "默认" @@ -1356,14 +1429,15 @@ msgstr "系统" #: assets/models/label.py:19 assets/models/node.py:544 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:26 -#: authentication/serializers/connect_token_secret.py:115 +#: authentication/serializers/connect_token_secret.py:113 #: common/serializers/common.py:80 settings/models.py:34 msgid "Value" msgstr "值" -#: assets/models/label.py:40 assets/serializers/asset/common.py:129 +#: assets/models/label.py:40 assets/serializers/asset/common.py:105 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 -#: authentication/serializers/connect_token_secret.py:114 +#: assets/serializers/platform.py:94 +#: authentication/serializers/connect_token_secret.py:112 #: common/serializers/common.py:79 settings/serializers/sms.py:7 msgid "Label" msgstr "标签" @@ -1388,7 +1462,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:558 perms/serializers/permission.py:36 +#: assets/models/node.py:558 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "节点" @@ -1397,129 +1471,147 @@ msgstr "节点" msgid "Can match node" msgstr "可以匹配节点" -#: assets/models/platform.py:20 +#: assets/models/platform.py:15 +msgid "Primary" +msgstr "主要的" + +#: assets/models/platform.py:16 msgid "Required" msgstr "必须的" -#: assets/models/platform.py:23 settings/serializers/settings.py:65 +#: assets/models/platform.py:18 +msgid "Public" +msgstr "开放的" + +#: assets/models/platform.py:19 settings/serializers/settings.py:65 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" -#: assets/models/platform.py:42 audits/const.py:47 settings/models.py:37 +#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37 #: terminal/serializers/applet_host.py:29 msgid "Enabled" msgstr "启用" -#: assets/models/platform.py:43 +#: assets/models/platform.py:32 msgid "Ansible config" msgstr "Ansible 配置" -#: assets/models/platform.py:44 assets/serializers/platform.py:61 +#: assets/models/platform.py:33 assets/serializers/platform.py:60 msgid "Ping enabled" msgstr "启用资产探活" -#: assets/models/platform.py:45 assets/serializers/platform.py:62 +#: assets/models/platform.py:34 assets/serializers/platform.py:61 msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:46 assets/models/platform.py:59 -#: assets/serializers/platform.py:63 +#: assets/models/platform.py:35 assets/models/platform.py:48 +#: assets/serializers/platform.py:62 msgid "Gather facts enabled" msgstr "启用收集资产信息" -#: assets/models/platform.py:47 assets/models/platform.py:61 -#: assets/serializers/platform.py:64 +#: assets/models/platform.py:36 assets/models/platform.py:50 +#: assets/serializers/platform.py:63 msgid "Gather facts method" msgstr "收集信息方式" -#: assets/models/platform.py:48 assets/serializers/platform.py:67 +#: assets/models/platform.py:37 assets/serializers/platform.py:66 msgid "Change secret enabled" msgstr "启用改密" -#: assets/models/platform.py:50 assets/serializers/platform.py:68 +#: assets/models/platform.py:39 assets/serializers/platform.py:67 msgid "Change secret method" msgstr "改密方式" -#: assets/models/platform.py:52 assets/serializers/platform.py:69 +#: assets/models/platform.py:41 assets/serializers/platform.py:68 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:54 assets/serializers/platform.py:70 +#: assets/models/platform.py:43 assets/serializers/platform.py:69 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:56 assets/serializers/platform.py:65 +#: assets/models/platform.py:45 assets/serializers/platform.py:64 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:58 assets/serializers/platform.py:66 +#: assets/models/platform.py:47 assets/serializers/platform.py:65 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:79 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:68 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:80 +#: assets/models/platform.py:69 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:83 assets/serializers/platform.py:91 +#: assets/models/platform.py:73 assets/serializers/platform.py:109 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:85 assets/serializers/platform.py:119 +#: assets/models/platform.py:75 assets/serializers/platform.py:133 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:87 assets/serializers/platform.py:118 +#: assets/models/platform.py:77 assets/serializers/platform.py:132 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:88 assets/serializers/platform.py:101 +#: assets/models/platform.py:78 assets/serializers/platform.py:115 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:90 assets/serializers/platform.py:98 +#: assets/models/platform.py:81 assets/serializers/platform.py:113 msgid "Automation" msgstr "自动化" +#: assets/models/platform.py:83 assets/serializers/platform.py:118 +msgid "Custom fields" +msgstr "自定义属性" + #: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/serializers/asset/common.py:119 +#: assets/notifications.py:12 +msgid "" +"Batch update platform in assets, skipping assets that do not meet platform " +"type" +msgstr "资产中批量更新平台,不符合平台类型跳过的资产" + +#: assets/serializers/asset/common.py:95 msgid "Auto fill" msgstr "自动代填" -#: 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 +#: assets/serializers/asset/common.py:106 assets/serializers/platform.py:112 +#: authentication/serializers/connect_token_secret.py:29 +#: authentication/serializers/connect_token_secret.py:63 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 msgid "Protocols" msgstr "协议组" -#: assets/serializers/asset/common.py:132 -#: assets/serializers/asset/common.py:153 +#: assets/serializers/asset/common.py:108 +#: assets/serializers/asset/common.py:130 msgid "Node path" msgstr "节点路径" -#: assets/serializers/asset/common.py:150 -#: assets/serializers/asset/common.py:302 +#: assets/serializers/asset/common.py:127 +#: assets/serializers/asset/common.py:299 msgid "Auto info" msgstr "自动化信息" -#: assets/serializers/asset/common.py:226 +#: assets/serializers/asset/common.py:220 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:261 +#: assets/serializers/asset/common.py:255 msgid "port out of range (1-65535)" msgstr "端口超出范围 (1-65535)" -#: assets/serializers/asset/common.py:268 +#: assets/serializers/asset/common.py:262 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" @@ -1568,7 +1660,7 @@ msgid "Disk total" msgstr "硬盘大小" #: assets/serializers/asset/host.py:21 -#: authentication/serializers/connect_token_secret.py:105 +#: authentication/serializers/connect_token_secret.py:103 msgid "OS" msgstr "操作系统" @@ -1620,26 +1712,30 @@ msgstr "SFTP 已启用" msgid "SFTP home" msgstr "SFTP 根路径" -#: assets/serializers/platform.py:43 +#: assets/serializers/platform.py:42 msgid "Auth with username" msgstr "使用用户名认证" -#: assets/serializers/platform.py:71 +#: assets/serializers/platform.py:70 msgid "Gather accounts enabled" msgstr "启用账号收集" -#: assets/serializers/platform.py:72 +#: assets/serializers/platform.py:71 msgid "Gather accounts method" msgstr "收集账号方式" -#: assets/serializers/platform.py:78 -msgid "Primary" -msgstr "主要的" - -#: assets/serializers/platform.py:120 +#: assets/serializers/platform.py:134 msgid "Default Domain" msgstr "默认网域" +#: assets/serializers/platform.py:143 +msgid "type is required" +msgstr "类型 该字段是必填项。" + +#: assets/serializers/platform.py:173 +msgid "Protocols is required" +msgstr "协议是必填的" + #: assets/signal_handlers/asset.py:26 assets/tasks/ping.py:35 msgid "Test assets connectivity " msgstr "测试资产可连接性" @@ -1752,11 +1848,6 @@ msgstr "下载文件" msgid "View" msgstr "查看" -#: audits/const.py:23 rbac/tree.py:229 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" - #: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:227 @@ -1778,7 +1869,7 @@ msgid "Change password" msgstr "改密" #: audits/const.py:34 settings/serializers/terminal.py:6 -#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:156 +#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163 #: terminal/serializers/session.py:48 msgid "Terminal" msgstr "终端" @@ -1817,7 +1908,7 @@ msgid "Job audit log" msgstr "作业审计日志" #: audits/models.py:46 audits/models.py:73 audits/models.py:144 -#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:95 +#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99 msgid "Remote addr" msgstr "远端地址" @@ -2441,23 +2532,23 @@ msgstr "异地登录提醒" msgid "binding reminder" msgstr "绑定提醒" -#: authentication/serializers/connect_token_secret.py:106 +#: authentication/serializers/connect_token_secret.py:104 msgid "Is builtin" msgstr "内置的" -#: authentication/serializers/connect_token_secret.py:110 +#: authentication/serializers/connect_token_secret.py:108 msgid "Options" msgstr "选项" -#: authentication/serializers/connect_token_secret.py:117 +#: authentication/serializers/connect_token_secret.py:115 msgid "Component" msgstr "组件" -#: authentication/serializers/connect_token_secret.py:128 +#: authentication/serializers/connect_token_secret.py:126 msgid "Expired now" msgstr "立刻过期" -#: authentication/serializers/connect_token_secret.py:148 +#: authentication/serializers/connect_token_secret.py:145 #: authentication/templates/authentication/_access_key_modal.html:30 #: perms/models/perm_node.py:21 users/serializers/group.py:33 msgid "ID" @@ -2472,15 +2563,15 @@ msgid "Ticket info" msgstr "工单信息" #: authentication/serializers/connection_token.py:20 -#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37 -#: perms/serializers/permission.py:70 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 +#: perms/serializers/permission.py:69 #: 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:39 perms/serializers/permission.py:71 +#: perms/serializers/permission.py:38 perms/serializers/permission.py:70 #: users/serializers/user.py:93 users/serializers/user.py:165 msgid "Is expired" msgstr "已过期" @@ -2500,8 +2591,8 @@ msgstr "邮箱" msgid "The {} cannot be empty" msgstr "{} 不能为空" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:38 -#: perms/serializers/permission.py:72 users/serializers/user.py:94 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: perms/serializers/permission.py:71 users/serializers/user.py:94 #: users/serializers/user.py:163 msgid "Is valid" msgstr "是否有效" @@ -2522,11 +2613,6 @@ msgstr "使用api key签名请求头,每个请求的头部是不一样的" msgid "docs" msgstr "文档" -#: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:98 terminal/notifications.py:146 -msgid "Date" -msgstr "日期" - #: authentication/templates/authentication/_access_key_modal.html:48 msgid "Show" msgstr "显示" @@ -2638,7 +2724,7 @@ msgid "request new one" msgstr "重新申请" #: authentication/templates/authentication/_msg_reset_password_code.html:12 -#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:79 +#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:83 #: users/forms/profile.py:104 users/templates/users/forgot_password.html:65 msgid "Verify code" msgstr "验证码" @@ -2961,15 +3047,13 @@ msgstr "组织 ID" msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容太大 (最大长度 `{}` 字节)" -#: common/drf/parsers/base.py:189 +#: common/drf/parsers/base.py:193 msgid "Parse file error: {}" msgstr "解析文件错误: {}" #: common/drf/parsers/excel.py:14 -#, fuzzy -#| msgid "Invalid zip file" msgid "Invalid excel file" -msgstr "无效的 zip 文件" +msgstr "无效的 excel 文件" #: common/exceptions.py:15 #, python-format @@ -3123,19 +3207,24 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: common/views/mixins.py:58 +#: common/views/mixins.py:73 msgid "Export all" msgstr "导出所有" -#: common/views/mixins.py:60 +#: common/views/mixins.py:75 msgid "Export only selected items" msgstr "仅导出选择项" -#: common/views/mixins.py:65 +#: common/views/mixins.py:80 #, python-format msgid "Export filtered: %s" msgstr "导出搜素: %s" +#: common/views/mixins.py:89 +#, python-format +msgid "User %s view/export secret" +msgstr "用户 %s 查看/导出 了密码" + #: jumpserver/conf.py:416 msgid "Create account successfully" msgstr "创建账号成功" @@ -3207,11 +3296,11 @@ msgstr "发布站内消息" msgid "No account available" msgstr "无可用账号" -#: ops/ansible/inventory.py:189 +#: ops/ansible/inventory.py:209 msgid "Ansible disabled" msgstr "Ansible 已禁用" -#: ops/ansible/inventory.py:205 +#: ops/ansible/inventory.py:225 msgid "Skip hosts below:" msgstr "跳过以下主机: " @@ -3287,10 +3376,6 @@ msgstr "仅限特权账号" msgid "Privileged First" msgstr "特权账号优先" -#: ops/const.py:45 -msgid "Skip" -msgstr "跳过" - #: ops/const.py:50 msgid "Powershell" msgstr "PowerShell" @@ -3388,14 +3473,7 @@ msgstr "可以查看任务监控" msgid "Kwargs" msgstr "其它参数" -#: ops/models/celery.py:60 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 -#: tickets/serializers/super_ticket.py:14 -#: tickets/serializers/ticket/ticket.py:21 -msgid "State" -msgstr "状态" - -#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:114 #: tickets/const.py:25 msgid "Finished" msgstr "结束" @@ -3906,7 +3984,7 @@ msgid "My assets" msgstr "我的资产" #: rbac/tree.py:56 terminal/models/applet/applet.py:43 -#: terminal/models/applet/applet.py:154 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "远程应用" @@ -4862,42 +4940,38 @@ msgid "Remember manual auth" msgstr "保存手动输入密码" #: settings/serializers/security.py:172 -msgid "Enable change auth secure mode" -msgstr "启用改密安全模式" - -#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:175 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:179 +#: settings/serializers/security.py:176 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:179 msgid "Operation center" msgstr "作业中心" -#: settings/serializers/security.py:183 +#: settings/serializers/security.py:180 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:186 +#: settings/serializers/security.py:183 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:184 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" -#: settings/serializers/security.py:190 +#: settings/serializers/security.py:187 msgid "Remote Login Protection" msgstr "异地登录保护" -#: settings/serializers/security.py:192 +#: settings/serializers/security.py:189 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -4975,11 +5049,11 @@ msgid "" "Tip: Default resolution to use when connecting graphical assets in Luna pages" msgstr "提示:在Luna 页面中连接图形化资产时默认使用的分辨率" -#: settings/tasks/ldap.py:24 +#: settings/tasks/ldap.py:25 msgid "Import ldap user" msgstr "导入 LDAP 用户" -#: settings/tasks/ldap.py:45 +#: settings/tasks/ldap.py:47 msgid "Periodic import ldap user" msgstr "周期导入 LDAP 用户" @@ -5352,7 +5426,7 @@ msgid "Output" msgstr "输出" #: terminal/backends/command/models.py:24 terminal/models/session/replay.py:9 -#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77 +#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:81 #: terminal/templates/terminal/_msg_command_alert.html:10 #: tickets/models/ticket/command_confirm.py:15 msgid "Session" @@ -5393,6 +5467,14 @@ msgstr "未匹配" msgid "Tunnel" msgstr "隧道" +#: terminal/const.py:71 +msgid "Read Only" +msgstr "只读" + +#: terminal/const.py:72 +msgid "Writable" +msgstr "读写" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" @@ -5417,7 +5499,7 @@ msgstr "主机" msgid "Applet pkg not valid, Missing file {}" msgstr "Applet pkg 无效,缺少文件 {}" -#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:158 terminal/models/applet/host.py:34 #: terminal/models/applet/host.py:106 msgid "Hosting" msgstr "宿主机" @@ -5475,7 +5557,7 @@ msgid "Redis port" msgstr "Redis 端口" #: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:66 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 @@ -5490,31 +5572,31 @@ msgstr "IP 组" msgid "Endpoint rule" msgstr "端点规则" -#: terminal/models/component/status.py:14 +#: terminal/models/component/status.py:15 msgid "Session Online" msgstr "在线会话" -#: terminal/models/component/status.py:15 +#: terminal/models/component/status.py:16 msgid "CPU Load" msgstr "CPU负载" -#: terminal/models/component/status.py:16 +#: terminal/models/component/status.py:17 msgid "Memory Used" msgstr "内存使用" -#: terminal/models/component/status.py:17 +#: terminal/models/component/status.py:18 msgid "Disk Used" msgstr "磁盘使用" -#: terminal/models/component/status.py:18 +#: terminal/models/component/status.py:19 msgid "Connections" msgstr "连接数" -#: terminal/models/component/status.py:19 +#: terminal/models/component/status.py:20 msgid "Threads" msgstr "线程数" -#: terminal/models/component/status.py:20 +#: terminal/models/component/status.py:21 msgid "Boot Time" msgstr "运行时间" @@ -5523,28 +5605,28 @@ msgid "Default storage" msgstr "默认存储" #: terminal/models/component/storage.py:140 -#: terminal/models/component/terminal.py:85 +#: terminal/models/component/terminal.py:91 msgid "Command storage" msgstr "命令存储" #: terminal/models/component/storage.py:200 -#: terminal/models/component/terminal.py:86 +#: terminal/models/component/terminal.py:92 msgid "Replay storage" msgstr "录像存储" -#: terminal/models/component/terminal.py:82 +#: terminal/models/component/terminal.py:88 msgid "type" msgstr "类型" -#: terminal/models/component/terminal.py:84 terminal/serializers/command.py:51 +#: terminal/models/component/terminal.py:90 terminal/serializers/command.py:51 msgid "Remote Address" msgstr "远端地址" -#: terminal/models/component/terminal.py:87 +#: terminal/models/component/terminal.py:93 msgid "Application User" msgstr "应用用户" -#: terminal/models/component/terminal.py:158 +#: terminal/models/component/terminal.py:165 msgid "Can view terminal config" msgstr "可以查看终端配置" @@ -5568,7 +5650,7 @@ msgstr "可以下载会话录像" msgid "Account id" msgstr "账号 ID" -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:100 +#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104 msgid "Login from" msgstr "登录来源" @@ -5604,43 +5686,48 @@ msgstr "可以验证会话动作权限" msgid "Expired time (min)" msgstr "过期时间 (分)" -#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82 +#: terminal/models/session/sharing.py:35 terminal/serializers/sharing.py:20 +#: terminal/serializers/sharing.py:52 +msgid "Action permission" +msgstr "操作权限" + +#: terminal/models/session/sharing.py:40 terminal/models/session/sharing.py:86 msgid "Session sharing" msgstr "会话分享" -#: terminal/models/session/sharing.py:38 +#: terminal/models/session/sharing.py:42 msgid "Can add super session sharing" msgstr "可以创建超级会话分享" -#: terminal/models/session/sharing.py:65 +#: terminal/models/session/sharing.py:69 msgid "Link not active" msgstr "链接失效" -#: terminal/models/session/sharing.py:67 +#: terminal/models/session/sharing.py:71 msgid "Link expired" msgstr "链接过期" -#: terminal/models/session/sharing.py:69 +#: terminal/models/session/sharing.py:73 msgid "User not allowed to join" msgstr "该用户无权加入会话" -#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59 +#: terminal/models/session/sharing.py:90 terminal/serializers/sharing.py:71 msgid "Joiner" msgstr "加入者" -#: terminal/models/session/sharing.py:89 +#: terminal/models/session/sharing.py:93 msgid "Date joined" msgstr "加入日期" -#: terminal/models/session/sharing.py:92 +#: terminal/models/session/sharing.py:96 msgid "Date left" msgstr "结束日期" -#: terminal/models/session/sharing.py:115 +#: terminal/models/session/sharing.py:119 msgid "Session join record" msgstr "会话加入记录" -#: terminal/models/session/sharing.py:131 +#: terminal/models/session/sharing.py:135 msgid "Invalid verification code" msgstr "验证码不正确" @@ -5737,15 +5824,15 @@ msgstr "账号" msgid "Timestamp" msgstr "时间戳" -#: terminal/serializers/endpoint.py:14 +#: terminal/serializers/endpoint.py:15 msgid "Oracle port" msgstr "Oracle 端口" -#: terminal/serializers/endpoint.py:17 +#: terminal/serializers/endpoint.py:18 msgid "Oracle port range" msgstr "Oracle 端口范围" -#: terminal/serializers/endpoint.py:19 +#: terminal/serializers/endpoint.py:20 msgid "" "Oracle proxy server listen port is dynamic, Each additional Oracle database " "instance adds a port listener" @@ -5753,11 +5840,11 @@ msgstr "" "Oracle 代理服务器监听端口是动态的,每增加一个 Oracle 数据库实例,就会增加一个" "端口监听" -#: terminal/serializers/endpoint.py:35 +#: terminal/serializers/endpoint.py:36 msgid "Visit IP/Host, if empty, use the current request instead" msgstr "访问IP/Host,如果为空,则使用当前请求的地址代替" -#: terminal/serializers/endpoint.py:58 +#: terminal/serializers/endpoint.py:59 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" @@ -6489,7 +6576,7 @@ msgstr "强制 MFA" #: users/serializers/user.py:92 msgid "Login blocked" -msgstr "登录被阻塞" +msgstr "登录被锁定" #: users/serializers/user.py:95 users/serializers/user.py:169 msgid "Is OTP bound" @@ -6504,10 +6591,8 @@ msgid "Avatar url" msgstr "头像路径" #: users/serializers/user.py:171 -#, fuzzy -#| msgid "One level" msgid "MFA level" -msgstr "1 级" +msgstr "MFA 级别" #: users/serializers/user.py:277 msgid "Select users" @@ -7351,6 +7436,11 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#, fuzzy +#~| msgid "Custom user" +#~ msgid "Custom" +#~ msgstr "自定义用户" + #~ msgid "API Server" #~ msgstr "API 服务" diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 2dbfc44ef..2695851bb 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -49,12 +49,12 @@ class JMSInventory: if gateway.password: proxy_command_list.insert( - 0, "sshpass -p '{}'".format(gateway.password) + 0, "sshpass -p {}".format(gateway.password) ) if gateway.private_key: proxy_command_list.append("-i {}".format(gateway.private_key_path)) - proxy_command = '-o ProxyCommand=\"{}\"'.format( + proxy_command = "-o ProxyCommand='{}'".format( " ".join(proxy_command_list) ) return {"ansible_ssh_common_args": proxy_command} @@ -72,15 +72,14 @@ class JMSInventory: var['ansible_ssh_private_key_file'] = account.private_key_path return var - def make_ssh_account_vars(self, host, asset, account, automation, protocols, platform, gateway): + def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway): if not account: host['error'] = _("No account available") return host - ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols)) - ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None + port = protocol.port if protocol else 22 host['ansible_host'] = asset.address - host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 + host['ansible_port'] = port su_from = account.su_from if platform.su_enabled and su_from: @@ -97,15 +96,55 @@ class JMSInventory: host.update(self.make_account_ansible_vars(account)) if gateway: - host.update(self.make_proxy_command(gateway)) + ansible_connection = host.get('ansible_connection', 'ssh') + if ansible_connection in ('local', 'winrm'): + host['gateway'] = { + 'address': gateway.address, 'port': gateway.port, + 'username': gateway.username, 'secret': gateway.password, + 'private_key_path': gateway.private_key_path + } + host['jms_asset']['port'] = port + else: + host.update(self.make_proxy_command(gateway)) + + @staticmethod + def get_primary_protocol(ansible_config, protocols): + invalid_protocol = type('protocol', (), {'name': 'null', 'port': 0}) + ansible_connection = ansible_config.get('ansible_connection') + # 数值越小,优先级越高,若用户在 ansible_config 中配置了,则提高用户配置方式的优先级 + protocol_priority = {'ssh': 10, 'winrm': 9, ansible_connection: 1} + protocol_sorted = sorted(protocols, key=lambda x: protocol_priority.get(x.name, 999)) + protocol = protocol_sorted[0] if protocol_sorted else invalid_protocol + return protocol + + @staticmethod + def fill_ansible_config(ansible_config, protocol): + if protocol.name in ('ssh', 'winrm'): + ansible_config['ansible_connection'] = protocol.name + if protocol.name == 'winrm': + if protocol.setting.get('use_ssl', False): + ansible_config['ansible_winrm_scheme'] = 'https' + ansible_config['ansible_winrm_transport'] = 'ssl' + ansible_config['ansible_winrm_server_cert_validation'] = 'ignore' + else: + ansible_config['ansible_winrm_scheme'] = 'http' + ansible_config['ansible_winrm_transport'] = 'plaintext' + return ansible_config def asset_to_host(self, asset, account, automation, protocols, platform): + try: + ansible_config = dict(automation.ansible_config) + except (AttributeError, TypeError): + ansible_config = {} + + protocol = self.get_primary_protocol(ansible_config, protocols) + host = { 'name': '{}'.format(asset.name.replace(' ', '_')), 'jms_asset': { 'id': str(asset.id), 'name': asset.name, 'address': asset.address, 'type': asset.type, 'category': asset.category, - 'protocol': asset.protocol, 'port': asset.port, + 'protocol': protocol.name, 'port': protocol.port, 'spec_info': asset.spec_info, 'secret_info': asset.secret_info, 'protocols': [{'name': p.name, 'port': p.port} for p in protocols], }, @@ -118,33 +157,24 @@ class JMSInventory: if host['jms_account'] and asset.platform.type == 'oracle': host['jms_account']['mode'] = 'sysdba' if account.privileged else None - try: - ansible_config = dict(automation.ansible_config) - except Exception as e: - ansible_config = {} - ansible_connection = ansible_config.get('ansible_connection', 'ssh') + ansible_config = self.fill_ansible_config(ansible_config, protocol) host.update(ansible_config) gateway = None if not asset.is_gateway and asset.domain: gateway = asset.domain.select_gateway() - if ansible_connection == 'local': - if gateway: - host['gateway'] = { - 'address': gateway.address, 'port': gateway.port, - 'username': gateway.username, 'secret': gateway.password - } - else: - self.make_ssh_account_vars(host, asset, account, automation, protocols, platform, gateway) + self.make_account_vars( + host, asset, account, automation, protocol, platform, gateway + ) return host - def get_asset_accounts(self, asset): - 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 + def get_asset_sorted_accounts(self, asset): + accounts = list(asset.accounts.filter(is_active=True)) + connectivity_score = {'ok': 2, '-': 1, 'err': 0} + sort_key = lambda x: (x.privileged, connectivity_score.get(x.connectivity, 0), x.date_updated) + accounts_sorted = sorted(accounts, key=sort_key, reverse=True) + return accounts_sorted @staticmethod def get_account_prefer(account_prefer): @@ -163,37 +193,41 @@ class JMSInventory: return account def select_account(self, asset): - accounts = self.get_asset_accounts(asset) - if not accounts or self.account_policy == 'skip': + accounts = self.get_asset_sorted_accounts(asset) + if not accounts: return None - account_selected = None - # 首先找到特权账号 - privileged_accounts = list(filter(lambda account: account.privileged, accounts)) + refer_account = self.get_refer_account(accounts) + if refer_account: + return refer_account - # 不同类型的账号选择,优先使用提供的名称 - refer_privileged_account = self.get_refer_account(privileged_accounts) - if self.account_policy in ['privileged_only', 'privileged_first']: - first_privileged = privileged_accounts[0] if privileged_accounts else None - account_selected = refer_privileged_account or first_privileged - - # 此策略不管是否匹配到账号都需强制返回 - if self.account_policy == 'privileged_only': + account_selected = accounts[0] + if self.account_policy == 'skip': + return None + elif self.account_policy == 'privileged_first': return account_selected + elif self.account_policy == 'privileged_only' and account_selected.privileged: + return account_selected + else: + return None - if not account_selected: - account_selected = self.get_refer_account(accounts) - - return account_selected or accounts[0] + @staticmethod + def set_platform_protocol_setting_to_asset(asset, platform_protocols): + asset_protocols = asset.protocols.all() + for p in asset_protocols: + setattr(p, 'setting', platform_protocols.get(p.name, {})) + return asset_protocols def generate(self, path_dir): hosts = [] platform_assets = self.group_by_platform(self.assets) for platform, assets in platform_assets.items(): automation = platform.automation - + platform_protocols = { + p['name']: p['setting'] for p in platform.protocols.values('name', 'setting') + } for asset in assets: - protocols = asset.protocols.all() + protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols) account = self.select_account(asset) host = self.asset_to_host(asset, account, automation, protocols, platform) diff --git a/apps/ops/migrations/0025_auto_20230413_1531.py b/apps/ops/migrations/0025_auto_20230413_1531.py new file mode 100644 index 000000000..0b77f1281 --- /dev/null +++ b/apps/ops/migrations/0025_auto_20230413_1531.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.17 on 2023-04-13 07:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0024_alter_celerytask_date_last_publish'), + ] + + operations = [ + migrations.AlterField( + model_name='adhoc', + name='args', + field=models.CharField(default='', max_length=8192, verbose_name='Args'), + ), + migrations.AlterField( + model_name='historicaljob', + name='args', + field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Args'), + ), + migrations.AlterField( + model_name='job', + name='args', + field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Args'), + ), + migrations.AlterField( + model_name='jobexecution', + name='material', + field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Material'), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 8313be48b..254453a90 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -22,7 +22,7 @@ class AdHoc(JMSOrgBaseModel): pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all') module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, verbose_name=_('Module')) - args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) + args = models.CharField(max_length=8192, default='', verbose_name=_('Args')) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index ed936ddba..a49885dff 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -46,8 +46,9 @@ class JMSPermedInventory(JMSInventory): self.user = user self.assets_accounts_mapper = self.get_assets_accounts_mapper() - def get_asset_accounts(self, asset): - return self.assets_accounts_mapper.get(asset.id, []) + def get_asset_sorted_accounts(self, asset): + accounts = self.assets_accounts_mapper.get(asset.id, []) + return list(accounts) def get_assets_accounts_mapper(self): mapper = defaultdict(set) @@ -91,7 +92,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): name = models.CharField(max_length=128, null=True, verbose_name=_('Name')) instant = models.BooleanField(default=False) - args = models.CharField(max_length=1024, default='', verbose_name=_('Args'), null=True, blank=True) + args = models.CharField(max_length=8192, default='', verbose_name=_('Args'), null=True, blank=True) module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, verbose_name=_('Module'), null=True) chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True) @@ -191,7 +192,7 @@ class JobExecution(JMSOrgBaseModel): date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) - material = models.CharField(max_length=1024, default='', verbose_name=_('Material'), null=True, blank=True) + material = models.CharField(max_length=8192, default='', verbose_name=_('Material'), null=True, blank=True) job_type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Material Type")) diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py index 86c7ab188..b0c4906e4 100644 --- a/apps/ops/notifications.py +++ b/apps/ops/notifications.py @@ -6,7 +6,8 @@ from notifications.notifications import SystemMessage from notifications.models import SystemMsgSubscription from users.models import User from notifications.backends import BACKEND -from terminal.models import Status, Terminal +from terminal.models.component.status import Status +from terminal.models import Terminal __all__ = ('ServerPerformanceMessage', 'ServerPerformanceCheckUtil') diff --git a/apps/perms/api/user_permission/assets.py b/apps/perms/api/user_permission/assets.py index f94854164..1ec27803f 100644 --- a/apps/perms/api/user_permission/assets.py +++ b/apps/perms/api/user_permission/assets.py @@ -4,7 +4,7 @@ from rest_framework.generics import ListAPIView from assets.api.asset.asset import AssetFilterSet from assets.models import Asset, Node -from common.utils import get_logger, lazyproperty +from common.utils import get_logger, lazyproperty, is_uuid from perms import serializers from perms.pagination import AllPermedAssetPagination from perms.pagination import NodePermedAssetPagination @@ -58,7 +58,12 @@ class UserAllPermedAssetsApi(BaseUserPermedAssetsApi): pagination_class = AllPermedAssetPagination def get_assets(self): - return self.query_asset_util.get_all_assets() + node_id = self.request.query_params.get('node_id') + if is_uuid(node_id): + __, assets = self.query_asset_util.get_node_all_assets(node_id) + else: + assets = self.query_asset_util.get_all_assets() + return assets class UserDirectPermedAssetsApi(BaseUserPermedAssetsApi): diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 27bc90d89..5fc19be29 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # - -from django.db.models import Q, QuerySet +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -110,7 +109,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): if condition in username_secret_type_dict: continue account_data = {key: getattr(template, key) for key in account_attribute} - account_data['name'] = f"{account_data['name']}-clone" + account_data['name'] = f"{account_data['name']}-{_('Account template')}" need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) return Account.objects.bulk_create(need_create_accounts) @@ -119,7 +118,10 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): 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]) + account_ids = [str(account.id) for account in accounts] + slice_count = 20 + for i in range(0, len(account_ids), slice_count): + push_accounts_to_assets_task.delay(account_ids[i:i + slice_count]) def validate_accounts(self, usernames: list[str]): template_ids = [] diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index c9582cc11..4c175cba5 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -8,7 +8,7 @@ from rest_framework import serializers from accounts.models import Account from assets.const import Category, AllTypes from assets.models import Node, Asset, Platform -from assets.serializers.asset.common import AssetProtocolsSerializer +from assets.serializers.asset.common import AssetProtocolsPermsSerializer from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField @@ -22,7 +22,7 @@ __all__ = [ class AssetPermedSerializer(OrgResourceModelSerializerMixin): """ 被授权资产的数据结构 """ platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) - protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) + protocols = AssetProtocolsPermsSerializer(many=True, required=False, label=_('Protocols')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) domain = ObjectRelatedField(required=False, queryset=Node.objects, label=_('Domain')) diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py index d3d1562c0..b9b5b01be 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -92,7 +92,7 @@ class AssetPermissionUtil(object): @staticmethod def convert_to_queryset_if_need(objs_or_ids, model): if not objs_or_ids: - return objs_or_ids + return model.objects.none() if isinstance(objs_or_ids, QuerySet) and isinstance(objs_or_ids.first(), model): return objs_or_ids ids = [ diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 8f3511936..1dc41b037 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -168,9 +168,6 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField( label=_("Remember manual auth") ) - CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = serializers.BooleanField( - label=_("Enable change auth secure mode") - ) SECURITY_INSECURE_COMMAND = serializers.BooleanField( required=False, label=_('Insecure command alert') ) diff --git a/apps/settings/tasks/ldap.py b/apps/settings/tasks/ldap.py index 226a8139c..8a4658f2b 100644 --- a/apps/settings/tasks/ldap.py +++ b/apps/settings/tasks/ldap.py @@ -1,8 +1,9 @@ # coding: utf-8 -# +# from celery import shared_task from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from django.db import transaction from common.utils import get_logger from ops.celery.decorator import after_app_ready_start @@ -22,6 +23,7 @@ def sync_ldap_user(): @shared_task(verbose_name=_('Import ldap user')) +@transaction.atomic def import_ldap_user(): logger.info("Start import ldap user task") util_server = LDAPServerUtil() diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index d207335d8..a477df5ba 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -21,8 +21,8 @@ p {

diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index 10fb55091..116aec94e 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -64,13 +64,7 @@ class DownloadUploadMixin: if instance and not update: return Response({'error': 'Applet already exists: {}'.format(name)}, status=400) - serializer = serializers.AppletSerializer(data=manifest, instance=instance) - serializer.is_valid(raise_exception=True) - save_to = default_storage.path('applets/{}'.format(name)) - if os.path.exists(save_to): - shutil.rmtree(save_to) - shutil.move(tmp_dir, save_to) - serializer.save() + applet, serializer = Applet.install_from_dir(tmp_dir) return Response(serializer.data, status=201) @action(detail=True, methods=['get']) diff --git a/apps/terminal/applets/chrome/app.py b/apps/terminal/applets/chrome/app.py index 6a2f678b5..ced2456c8 100644 --- a/apps/terminal/applets/chrome/app.py +++ b/apps/terminal/applets/chrome/app.py @@ -18,6 +18,7 @@ class Command(Enum): CLICK = 'click' OPEN = 'open' CODE = 'code' + SELECT_FRAME = 'select_frame' def _execute_type(ele: WebElement, value: str): @@ -53,6 +54,9 @@ class StepAction: def execute(self, driver: webdriver.Chrome) -> bool: if not self.target: return True + if self.command == 'select_frame': + self._switch_iframe(driver, self.target) + return True target_name, target_value = self.target.split("=", 1) by_name = self.methods_map.get(target_name.upper(), By.NAME) ele = driver.find_element(by=by_name, value=target_value) @@ -74,6 +78,28 @@ class StepAction: def _execute_command_type(self, ele, value): ele.send_keys(value) + def _switch_iframe(self, driver: webdriver.Chrome, target: str): + """ + driver: webdriver.Chrome + target: str + target support three format str below: + index=1: switch to frame by index, if index < 0, switch to default frame + id=xxx: switch to frame by id + name=xxx: switch to frame by name + """ + target_name, target_value = target.split("=", 1) + if target_name == 'id': + driver.switch_to.frame(target_value) + elif target_name == 'index': + index = int(target_value) + if index < 0: + driver.switch_to.default_content() + else: + driver.switch_to.frame(index) + elif target_name == 'name': + driver.switch_to.frame(target_value) + else: + driver.switch_to.frame(target) def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool: try: diff --git a/apps/terminal/applets/chrome/manifest.yml b/apps/terminal/applets/chrome/manifest.yml index f2681a0c2..850ec3f08 100644 --- a/apps/terminal/applets/chrome/manifest.yml +++ b/apps/terminal/applets/chrome/manifest.yml @@ -1,6 +1,6 @@ name: chrome display_name: Chrome Browser -version: 0.1 +version: 0.2 comment: Chrome Browser Open URL Page Address author: JumpServer Team exec_type: python diff --git a/apps/terminal/applets/chrome/test_data_example.json b/apps/terminal/applets/chrome/test_data_example.json index fc8e00991..417b0163d 100644 --- a/apps/terminal/applets/chrome/test_data_example.json +++ b/apps/terminal/applets/chrome/test_data_example.json @@ -24,7 +24,7 @@ "autofill": "basic", "username_selector": "name=username", "password_selector": "name=password", - "submit_selector": "id=longin_button", + "submit_selector": "id=login_button", "script": [] }, "org_id": "2925D985-A435-411D-9BC4-FEA630F105D9" diff --git a/apps/terminal/applets/dbeaver/app.py b/apps/terminal/applets/dbeaver/app.py index f99fdd54c..39d953926 100644 --- a/apps/terminal/applets/dbeaver/app.py +++ b/apps/terminal/applets/dbeaver/app.py @@ -4,6 +4,9 @@ import win32api import shutil import subprocess +from xml.etree import ElementTree +from xml.sax import SAXException + from common import wait_pid, BaseApplication @@ -21,16 +24,88 @@ class AppletApplication(BaseApplication): self.port = self.asset.get_protocol_port(self.protocol) self.db = self.asset.spec_info.db_name self.name = '%s-%s-%s' % (self.host, self.db, int(time.time())) + self.app_work_path = self.get_app_work_path() self.pid = None self.app = None - def launch(self): + @staticmethod + def get_app_work_path(): win_user_name = win32api.GetUserName() + return r'C:\Users\%s\AppData\Roaming\DBeaverData' % win_user_name + + @staticmethod + def _read_config(config_file): + default_config = {} + if not os.path.exists(config_file): + return default_config + + with open(config_file, 'r') as f: + for line in f.readlines(): + try: + config_key, config_value = line.split('=') + except ValueError: + continue + default_config[config_key] = config_value + return default_config + + @staticmethod + def _write_config(config_file, config): + with open(config_file, 'w')as f: + for key, value in config.items(): + f.write(f'{key}={value}\n') + + @staticmethod + def _merge_driver_xml(src_path, dest_path): + tree1 = ElementTree.parse(dest_path) + tree2 = ElementTree.parse(src_path) + + for child2 in tree2.getroot(): + found = False + for child1 in tree1.getroot(): + if child1.tag == child2.tag and child1.attrib == child2.attrib: + found = True + break + if not found: + tree1.getroot().append(child2) + tree1.write(dest_path) + + def init_driver(self): src_driver = os.path.join(os.path.dirname(self.path), 'drivers') - dest_driver = r'C:\Users\%s\AppData\Roaming\DBeaverData\drivers' % win_user_name + dest_driver = os.path.join(self.app_work_path, 'drivers') if not os.path.exists(dest_driver): shutil.copytree(src_driver, dest_driver, dirs_exist_ok=True) + def init_driver_config(self): + driver_yml_path = os.path.join( + self.app_work_path, 'workspace6', '.metadata', '.config', + ) + driver_yml_file = os.path.join(driver_yml_path, 'drivers.xml') + try: + self._merge_driver_xml('./config/drivers.xml', driver_yml_file) + except (SAXException, FileNotFoundError): + os.makedirs(driver_yml_path, exist_ok=True) + shutil.copy('./config/drivers.xml', driver_yml_file) + + def init_other_config(self): + config_path = os.path.join( + self.app_work_path, 'workspace6', '.metadata', + '.plugins', 'org.eclipse.core.runtime', '.settings', + ) + os.makedirs(config_path, exist_ok=True) + config_file = os.path.join(config_path, 'org.jkiss.dbeaver.core.prefs') + + config = self._read_config(config_file) + config['ui.auto.update.check'] = 'false' + config['sample.database.canceled'] = 'true' + config['tipOfTheDayInitializer.notFirstRun'] = 'true' + config['ui.show.tip.of.the.day.on.startup'] = 'false' + self._write_config(config_file, config) + + def launch(self): + self.init_driver() + self.init_driver_config() + self.init_other_config() + def _get_exec_params(self): driver = getattr(self, 'driver', self.protocol) params_string = f'name={self.name}|' \ diff --git a/apps/terminal/applets/dbeaver/config/drivers.xml b/apps/terminal/applets/dbeaver/config/drivers.xml new file mode 100644 index 000000000..2ce625a5b --- /dev/null +++ b/apps/terminal/applets/dbeaver/config/drivers.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/terminal/applets/navicat/README.md b/apps/terminal/applets/navicat/README.md deleted file mode 100644 index ee18cd491..000000000 --- a/apps/terminal/applets/navicat/README.md +++ /dev/null @@ -1,4 +0,0 @@ -## Navicat Premium - -- 需要先手动导入License激活 - diff --git a/apps/terminal/applets/navicat/app.py b/apps/terminal/applets/navicat/app.py deleted file mode 100644 index 7c2dfd3bc..000000000 --- a/apps/terminal/applets/navicat/app.py +++ /dev/null @@ -1,411 +0,0 @@ -import os -import shutil -import time - -import winreg -import win32api -import win32con - -import const as c - -from pywinauto import Application -from pywinauto.keyboard import send_keys -from pywinauto.controls.uia_controls import ( - EditWrapper, ComboBoxWrapper, ButtonWrapper -) - -from common import wait_pid, BaseApplication, _messageBox - -_default_path = r'C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe' - - -class AppletApplication(BaseApplication): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.path = _default_path - self.username = self.account.username - self.password = self.account.secret - self.privileged = self.account.privileged - self.host = self.asset.address - self.port = self.asset.get_protocol_port(self.protocol) - self.db = self.asset.spec_info.db_name - self.name = '%s-%s-%s' % (self.host, self.db, int(time.time())) - self.use_ssl = self.asset.spec_info.use_ssl - self.client_key = self.asset.secret_info.client_key - self.client_key_path = None - self.pid = None - self.app = None - - @staticmethod - def get_cert_path(): - win_user_name = win32api.GetUserName() - cert_path = r'C:\Users\%s\AppData\Roaming\Navicat\certs' % win_user_name - return cert_path - - def clean_up(self): - protocols = ( - 'NavicatMARIADB', 'NavicatMONGODB', 'Navicat', - 'NavicatORA', 'NavicatMSSQL', 'NavicatPG' - ) - for p in protocols: - sub_key = r'Software\PremiumSoft\%s\Servers' % p - try: - win32api.RegDeleteTree(winreg.HKEY_CURRENT_USER, sub_key) - except Exception: - pass - cert_path = self.get_cert_path() - shutil.rmtree(cert_path, ignore_errors=True) - - def gen_asset_file(self): - if self.use_ssl and self.client_key: - cert_path = self.get_cert_path() - if not os.path.exists(cert_path): - os.makedirs(cert_path, exist_ok=True) - filepath = os.path.join(cert_path, str(int(time.time()))) - with open(filepath, 'w') as f: - f.write(self.client_key) - self.client_key_path = filepath - - @staticmethod - def edit_regedit(): - sub_key = r'Software\PremiumSoft\NavicatPremium' - try: - key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, sub_key) - # 禁止弹出欢迎页面 - winreg.SetValueEx(key, 'AlreadyShowNavicatV16WelcomeScreen', 0, winreg.REG_DWORD, 1) - # 禁止开启自动检查更新 - winreg.SetValueEx(key, 'AutoCheckUpdate', 0, winreg.REG_DWORD, 0) - # 禁止弹出初始化界面 - winreg.SetValueEx(key, 'ShareUsageData', 0, winreg.REG_DWORD, 0) - except Exception as err: - print('Launch error: %s' % err) - - def launch(self): - # 清理因为异常未关闭的会话历史记录 - self.clean_up() - # 生成资产依赖的相关文件 - self.gen_asset_file() - # 修改注册表,达到一些配置目的 - self.edit_regedit() - - @staticmethod - def _exec_commands(commands): - for command in commands: - pre_check = command.get('pre_check', lambda: True) - if not pre_check(): - _messageBox('程序启动异常,请重新连接!!', 'Error', win32con.MB_DEFAULT_DESKTOP_ONLY) - return - - time.sleep(0.5) - if command['type'] == 'key': - send_keys(' '.join(command['commands'])) - elif command['type'] == 'action': - for f in command['commands']: - f() - - def _pre_check_is_password_input(self): - try: - self.app.window(best_match='Connection Password') - except Exception: - return False - return True - - def _action_ele_click(self, ele_name, conn_win=None): - if not conn_win: - conn_win = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - conn_win.child_window(best_match=ele_name).click() - - def _fill_mysql_auth_info(self): - conn_window = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - - name_ele = conn_window.child_window(best_match='Edit5') - EditWrapper(name_ele.element_info).set_edit_text(self.name) - - host_ele = conn_window.child_window(best_match='Edit4') - EditWrapper(host_ele.element_info).set_edit_text(self.host) - - port_ele = conn_window.child_window(best_match='Edit2') - EditWrapper(port_ele.element_info).set_edit_text(self.port) - - username_ele = conn_window.child_window(best_match='Edit1') - EditWrapper(username_ele.element_info).set_edit_text(self.username) - - def _get_mysql_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.ENTER - ], - }, - { - 'type': 'action', - 'commands': [ - self._fill_mysql_auth_info, lambda: self._action_ele_click('Save password') - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - return commands - - def _get_mariadb_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.DOWN * 5, c.ENTER, - ], - }, - { - 'type': 'action', - 'commands': [ - self._fill_mysql_auth_info, lambda: self._action_ele_click('Save password') - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - return commands - - def _fill_mongodb_auth_info(self): - conn_window = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - - auth_type_ele = conn_window.child_window(best_match='ComboBox2') - ComboBoxWrapper(auth_type_ele.element_info).select('Password') - - name_ele = conn_window.child_window(best_match='Edit5') - EditWrapper(name_ele.element_info).set_edit_text(self.name) - - host_ele = conn_window.child_window(best_match='Edit4') - EditWrapper(host_ele.element_info).set_edit_text(self.host) - - port_ele = conn_window.child_window(best_match='Edit2') - EditWrapper(port_ele.element_info).set_edit_text(self.port) - - db_ele = conn_window.child_window(best_match='Edit6') - EditWrapper(db_ele.element_info).set_edit_text(self.db) - - username_ele = conn_window.child_window(best_match='Edit1') - EditWrapper(username_ele.element_info).set_edit_text(self.username) - - def _get_mongodb_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.DOWN * 6, c.ENTER, - ], - }, - { - 'type': 'action', - 'commands': [ - self._fill_mongodb_auth_info, lambda: self._action_ele_click('Save password') - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - if self.use_ssl: - ssl_commands = [ - { - 'type': 'key', - 'commands': [c.TAB * 5, c.RIGHT * 3, c.TAB] - }, - { - 'type': 'action', - 'commands': [ - lambda: self._action_ele_click('Use SSL'), - lambda: self._action_ele_click('Use authentication'), - ] - }, - { - 'type': 'key', - 'commands': [c.TAB, self.client_key_path] - }, - { - 'type': 'action', - 'commands': [lambda: self._action_ele_click('Allow invalid host names')] - } - ] - commands = commands[:2] + ssl_commands + commands[2:] - return commands - - def _fill_postgresql_auth_info(self): - conn_window = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - - name_ele = conn_window.child_window(best_match='Edit6') - EditWrapper(name_ele.element_info).set_edit_text(self.name) - - host_ele = conn_window.child_window(best_match='Edit5') - EditWrapper(host_ele.element_info).set_edit_text(self.host) - - port_ele = conn_window.child_window(best_match='Edit2') - EditWrapper(port_ele.element_info).set_edit_text(self.port) - - db_ele = conn_window.child_window(best_match='Edit4') - EditWrapper(db_ele.element_info).set_edit_text(self.db) - - username_ele = conn_window.child_window(best_match='Edit1') - EditWrapper(username_ele.element_info).set_edit_text(self.username) - - def _get_postgresql_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.DOWN, c.ENTER, - ], - }, - { - 'type': 'action', - 'commands': [ - self._fill_postgresql_auth_info, lambda: self._action_ele_click('Save password') - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - return commands - - def _fill_sqlserver_auth_info(self): - conn_window = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - - name_ele = conn_window.child_window(best_match='Edit5') - EditWrapper(name_ele.element_info).set_edit_text(self.name) - - host_ele = conn_window.child_window(best_match='Edit4') - EditWrapper(host_ele.element_info).set_edit_text('%s,%s' % (self.host, self.port)) - - db_ele = conn_window.child_window(best_match='Edit3') - EditWrapper(db_ele.element_info).set_edit_text(self.db) - - username_ele = conn_window.child_window(best_match='Edit6') - EditWrapper(username_ele.element_info).set_edit_text(self.username) - - def _get_sqlserver_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.DOWN * 4, c.ENTER, - ], - }, - { - 'type': 'action', - 'commands': [ - self._fill_sqlserver_auth_info, lambda: self._action_ele_click('Save password') - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - return commands - - def _fill_oracle_auth_info(self): - conn_window = self.app.window(best_match='Dialog'). \ - child_window(title_re='New Connection') - - name_ele = conn_window.child_window(best_match='Edit6') - EditWrapper(name_ele.element_info).set_edit_text(self.name) - - host_ele = conn_window.child_window(best_match='Edit5') - EditWrapper(host_ele.element_info).set_edit_text(self.host) - - port_ele = conn_window.child_window(best_match='Edit3') - EditWrapper(port_ele.element_info).set_edit_text(self.port) - - db_ele = conn_window.child_window(best_match='Edit2') - EditWrapper(db_ele.element_info).set_edit_text(self.db) - - username_ele = conn_window.child_window(best_match='Edit') - EditWrapper(username_ele.element_info).set_edit_text(self.username) - - if self.privileged: - conn_window.child_window(best_match='Advanced', control_type='TabItem').click_input() - role_ele = conn_window.child_window(best_match='ComboBox2') - ComboBoxWrapper(role_ele.element_info).select('SYSDBA') - - def _get_oracle_commands(self): - commands = [ - { - 'type': 'key', - 'commands': [ - '%f', c.DOWN, c.RIGHT, c.DOWN * 2, c.ENTER, - ], - }, - { - 'type': 'action', - 'commands': [ - lambda: self._action_ele_click('Save password'), self._fill_oracle_auth_info - ] - }, - { - 'type': 'key', - 'commands': [c.ENTER] - } - ] - return commands - - def run(self): - self.launch() - self.app = Application(backend='uia') - work_dir = os.path.dirname(self.path) - self.app.start(self.path, work_dir=work_dir) - self.pid = self.app.process - - # 检测是否为试用版本 - try: - trial_btn = self.app.top_window().child_window( - best_match='Trial', control_type='Button' - ) - ButtonWrapper(trial_btn.element_info).click() - time.sleep(0.5) - except Exception: - pass - - # 根据协议获取相应操作命令 - action = getattr(self, '_get_%s_commands' % self.protocol, None) - if action is None: - raise ValueError('This protocol is not supported: %s' % self.protocol) - commands = action() - # 关闭掉桌面许可弹框 - commands.insert(0, {'type': 'key', 'commands': (c.ESC,)}) - # 登录 - commands.extend([ - { - 'type': 'key', - 'commands': ( - '%f', c.DOWN * 5, c.ENTER - ) - }, - { - 'type': 'key', - 'commands': (self.password, c.ENTER), - 'pre_check': self._pre_check_is_password_input - } - ]) - self._exec_commands(commands) - - def wait(self): - try: - wait_pid(self.pid) - except Exception: - pass - finally: - self.clean_up() diff --git a/apps/terminal/applets/navicat/common.py b/apps/terminal/applets/navicat/common.py deleted file mode 100644 index 62becd792..000000000 --- a/apps/terminal/applets/navicat/common.py +++ /dev/null @@ -1,215 +0,0 @@ -import abc -import base64 -import json -import locale -import os -import subprocess -import sys -import time -from subprocess import CREATE_NO_WINDOW - -_blockInput = None -_messageBox = None -if sys.platform == 'win32': - import ctypes - from ctypes import wintypes - import win32ui - - # import win32con - - _messageBox = win32ui.MessageBox - - _blockInput = ctypes.windll.user32.BlockInput - _blockInput.argtypes = [wintypes.BOOL] - _blockInput.restype = wintypes.BOOL - - -def block_input(): - if _blockInput: - _blockInput(True) - - -def unblock_input(): - if _blockInput: - _blockInput(False) - - -def decode_content(content: bytes) -> str: - for encoding_name in ['utf-8', 'gbk', 'gb2312']: - try: - return content.decode(encoding_name) - except Exception as e: - print(e) - encoding_name = locale.getpreferredencoding() - return content.decode(encoding_name) - - -def notify_err_message(msg): - if _messageBox: - _messageBox(msg, 'Error') - - -def check_pid_alive(pid) -> bool: - # tasklist /fi "PID eq 508" /fo csv - # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' - try: - - csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], - creationflags=CREATE_NO_WINDOW) - content = decode_content(csv_ret) - content_list = content.strip().split("\r\n") - if len(content_list) != 2: - print("check pid {} ret invalid: {}".format(pid, content)) - return False - ret_pid = content_list[1].split(",")[1].strip('"') - return str(pid) == ret_pid - except Exception as e: - print("check pid {} err: {}".format(pid, e)) - return False - - -def wait_pid(pid): - while 1: - time.sleep(5) - ok = check_pid_alive(pid) - if not ok: - print("pid {} is not alive".format(pid)) - break - - -class DictObj: - def __init__(self, in_dict: dict): - assert isinstance(in_dict, dict) - for key, val in in_dict.items(): - if isinstance(val, (list, tuple)): - setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val]) - else: - setattr(self, key, DictObj(val) if isinstance(val, dict) else val) - - -class User(DictObj): - id: str - name: str - username: str - - -class Specific(DictObj): - # web - autofill: str - username_selector: str - password_selector: str - submit_selector: str - script: list - - # database - db_name: str - use_ssl: str - - -class Secret(DictObj): - client_key: str - - -class Category(DictObj): - value: str - label: str - - -class Protocol(DictObj): - id: str - name: str - port: int - - -class Asset(DictObj): - id: str - name: str - address: str - protocols: list[Protocol] - category: Category - spec_info: Specific - secret_info: Secret - - def get_protocol_port(self, protocol): - for item in self.protocols: - if item.name == protocol: - return item.port - return None - - -class LabelValue(DictObj): - label: str - value: str - - -class Account(DictObj): - id: str - name: str - username: str - secret: str - privileged: bool - secret_type: LabelValue - - -class Platform(DictObj): - charset: str - name: str - charset: LabelValue - type: LabelValue - - -class Manifest(DictObj): - name: str - version: str - path: str - exec_type: str - connect_type: str - protocols: list[str] - - -def get_manifest_data() -> dict: - current_dir = os.path.dirname(__file__) - manifest_file = os.path.join(current_dir, 'manifest.json') - try: - with open(manifest_file, "r", encoding='utf8') as f: - return json.load(f) - except Exception as e: - print(e) - return {} - - -def read_app_manifest(app_dir) -> dict: - main_json_file = os.path.join(app_dir, "manifest.json") - if not os.path.exists(main_json_file): - return {} - with open(main_json_file, 'r', encoding='utf8') as f: - return json.load(f) - - -def convert_base64_to_dict(base64_str: str) -> dict: - try: - data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8') - return json.loads(data_json) - except Exception as e: - print(e) - return {} - - -class BaseApplication(abc.ABC): - - def __init__(self, *args, **kwargs): - self.app_name = kwargs.get('app_name', '') - self.protocol = kwargs.get('protocol', '') - self.manifest = Manifest(kwargs.get('manifest', {})) - self.user = User(kwargs.get('user', {})) - self.asset = Asset(kwargs.get('asset', {})) - self.account = Account(kwargs.get('account', {})) - self.platform = Platform(kwargs.get('platform', {})) - - @abc.abstractmethod - def run(self): - raise NotImplementedError('run') - - @abc.abstractmethod - def wait(self): - raise NotImplementedError('wait') diff --git a/apps/terminal/applets/navicat/const.py b/apps/terminal/applets/navicat/const.py deleted file mode 100644 index 7c186b8cc..000000000 --- a/apps/terminal/applets/navicat/const.py +++ /dev/null @@ -1,8 +0,0 @@ - -UP = '{UP}' -LEFT = '{LEFT}' -DOWN = '{DOWN}' -RIGHT = '{RIGHT}' -TAB = '{VK_TAB}' -ESC = '{ESC}' -ENTER = '{VK_RETURN}' diff --git a/apps/terminal/applets/navicat/i18n.yml b/apps/terminal/applets/navicat/i18n.yml deleted file mode 100644 index ec6427048..000000000 --- a/apps/terminal/applets/navicat/i18n.yml +++ /dev/null @@ -1,3 +0,0 @@ -- zh: - display_name: Navicat premium 16 - comment: 数据库管理软件 diff --git a/apps/terminal/applets/navicat/icon.png b/apps/terminal/applets/navicat/icon.png deleted file mode 100644 index 10b343bf0..000000000 Binary files a/apps/terminal/applets/navicat/icon.png and /dev/null differ diff --git a/apps/terminal/applets/navicat/main.py b/apps/terminal/applets/navicat/main.py deleted file mode 100644 index be0ff3585..000000000 --- a/apps/terminal/applets/navicat/main.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys - -from common import (block_input, unblock_input) -from common import convert_base64_to_dict -from app import AppletApplication - - -def main(): - base64_str = sys.argv[1] - data = convert_base64_to_dict(base64_str) - applet_app = AppletApplication(**data) - block_input() - applet_app.run() - unblock_input() - applet_app.wait() - - -if __name__ == '__main__': - try: - main() - except Exception as e: - print(e) diff --git a/apps/terminal/applets/navicat/manifest.yml b/apps/terminal/applets/navicat/manifest.yml deleted file mode 100644 index 02db9576a..000000000 --- a/apps/terminal/applets/navicat/manifest.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: navicat -display_name: Navicat premium 16 -comment: Database management software -version: 0.1 -exec_type: python -author: JumpServer Team -type: general -update_policy: always -tags: - - database -protocols: - - mysql - - mariadb - - postgresql - - sqlserver - - oracle - - mongodb diff --git a/apps/terminal/applets/navicat/setup.yml b/apps/terminal/applets/navicat/setup.yml deleted file mode 100644 index 60e8d6c43..000000000 --- a/apps/terminal/applets/navicat/setup.yml +++ /dev/null @@ -1,6 +0,0 @@ -type: manual # exe, zip, manual -# 从这里下载的: https://www.navicat.com.cn/download/direct-download?product=navicat_premium_en_x64.exe -source: -destination: C:\Program Files\PremiumSoft\Navicat Premium 16 -program: C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe -md5: 6c2c25fa56c75254c6bbcba043000063 diff --git a/apps/terminal/automations/deploy_applet_host/install_all.yml b/apps/terminal/automations/deploy_applet_host/install_all.yml index bf3da06b4..befa8f5a2 100644 --- a/apps/terminal/automations/deploy_applet_host/install_all.yml +++ b/apps/terminal/automations/deploy_applet_host/install_all.yml @@ -4,5 +4,6 @@ tasks: - name: Install all applets - ansible.windows.win_shell: - "tinkerd install all" + ansible.windows.win_powershell: + script: | + tinkerd install all diff --git a/apps/terminal/automations/deploy_applet_host/install_applet.yml b/apps/terminal/automations/deploy_applet_host/install_applet.yml index 5c216773f..de3c0fa49 100644 --- a/apps/terminal/automations/deploy_applet_host/install_applet.yml +++ b/apps/terminal/automations/deploy_applet_host/install_applet.yml @@ -6,6 +6,7 @@ tasks: - name: Install applet - ansible.windows.win_shell: - "tinkerd install --name {{ applet_name }}" + ansible.windows.win_powershell: + script: | + tinkerd install --name {{ applet_name }} when: applet_name != 'all' diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index bd819a93f..b68f6e06c 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -143,9 +143,9 @@ dest: "{{ ansible_env.TEMP }}\\pip_packages" - name: Install python requirements offline - ansible.windows.win_shell: > - pip install -r '{{ ansible_env.TEMP }}\pip_packages\pip_packages\requirements.txt' - --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages\pip_packages' + ansible.windows.win_powershell: + script: | + pip install -r '{{ ansible_env.TEMP }}\pip_packages\pip_packages\requirements.txt' --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages\pip_packages' - name: Download chromedriver (Chromium) ansible.windows.win_get_url: @@ -183,31 +183,20 @@ GOOGLE_DEFAULT_CLIENT_ID: '' GOOGLE_DEFAULT_CLIENT_SECRET: '' - - name: Download navicat161_premium_en package (navicat) - ansible.windows.win_get_url: - url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/navicat161_premium_en_x64.exe" - dest: "{{ ansible_env.TEMP }}\\navicat161_premium_en_x64.exe" - validate_certs: "{{ not IGNORE_VERIFY_CERTS }}" - - - name: Install navicat (navicat) - ansible.windows.win_package: - path: "{{ ansible_env.TEMP }}\\navicat161_premium_en_x64.exe" - state: present - arguments: - - /SILENT - - name: Generate tinkerd component config - ansible.windows.win_shell: - "tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} - --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }} --ignore-verify-certs {{ IGNORE_VERIFY_CERTS }}" + ansible.windows.win_powershell: + script: | + tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }} --ignore-verify-certs {{ IGNORE_VERIFY_CERTS }} - name: Install tinkerd service - ansible.windows.win_shell: - "tinkerd service install" + ansible.windows.win_powershell: + script: | + tinkerd service install - name: Start tinkerd service - ansible.windows.win_shell: - "tinkerd service start" + ansible.windows.win_powershell: + script: | + tinkerd service start - name: Wait Tinker api health ansible.windows.win_uri: @@ -220,5 +209,6 @@ delay: 5 - name: Sync all remote applets - ansible.windows.win_shell: - "tinkerd install all" + ansible.windows.win_powershell: + script: | + tinkerd install all \ No newline at end of file diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 40adb4e82..fd0421427 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -65,3 +65,8 @@ class SessionType(TextChoices): normal = 'normal', _('Normal') tunnel = 'tunnel', _('Tunnel') command = 'command', _('Command') + + +class ActionPermission(TextChoices): + readonly = "readonly", _('Read Only') + writable = "writable", _('Writable') diff --git a/apps/terminal/migrations/0060_sessionsharing_action_permission.py b/apps/terminal/migrations/0060_sessionsharing_action_permission.py new file mode 100644 index 000000000..1e0bcab94 --- /dev/null +++ b/apps/terminal/migrations/0060_sessionsharing_action_permission.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-04-03 06:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0059_session_account_id'), + ] + + operations = [ + migrations.AddField( + model_name='sessionsharing', + name='action_permission', + field=models.CharField(default='writable', max_length=16, verbose_name='Action permission'), + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index 22afe39e8..c6e938028 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -12,7 +12,6 @@ from rest_framework.serializers import ValidationError from common.db.models import JMSBaseModel from common.utils import lazyproperty, get_logger -from jumpserver.utils import has_valid_xpack_license logger = get_logger(__name__) @@ -91,29 +90,55 @@ class Applet(JMSBaseModel): return manifest @classmethod - def install_from_dir(cls, path): + def load_platform_if_need(cls, d): + from assets.serializers import PlatformSerializer + + if not os.path.exists(os.path.join(d, 'platform.yml')): + return + try: + with open(os.path.join(d, 'platform.yml')) as f: + data = yaml.safe_load(f) + except Exception as e: + raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)}) + + if data['category'] != 'custom': + raise ValidationError({'error': _('Only support custom platform')}) + + try: + tp = data['type'] + except KeyError: + raise ValidationError({'error': _('Missing type in platform.yml')}) + + s = PlatformSerializer(data=data) + s.add_type_choices(tp, tp) + s.is_valid(raise_exception=True) + s.save() + + @classmethod + def install_from_dir(cls, path, builtin=True): from terminal.serializers import AppletSerializer manifest = cls.validate_pkg(path) name = manifest['name'] - if not has_valid_xpack_license() and name.lower() in ('navicat',): - return - instance = cls.objects.filter(name=name).first() serializer = AppletSerializer(instance=instance, data=manifest) serializer.is_valid() - serializer.save(builtin=True) - pkg_path = default_storage.path('applets/{}'.format(name)) + serializer.save(builtin=builtin) + cls.load_platform_if_need(path) + + pkg_path = default_storage.path('applets/{}'.format(name)) if os.path.exists(pkg_path): shutil.rmtree(pkg_path) shutil.copytree(path, pkg_path) - return instance + return instance, serializer def select_host_account(self): # 选择激活的发布机 - hosts = [item for item in self.hosts.filter(is_active=True).all() - if item.load != 'offline'] + hosts = [ + host for host in self.hosts.filter(is_active=True) + if host.load != 'offline' + ] if not hosts: return None diff --git a/apps/terminal/models/component/status.py b/apps/terminal/models/component/status.py index 3da13d9f0..d273b9dac 100644 --- a/apps/terminal/models/component/status.py +++ b/apps/terminal/models/component/status.py @@ -1,11 +1,12 @@ import uuid +from django.core.cache import cache from django.db import models +from django.forms.models import model_to_dict from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger - logger = get_logger(__name__) @@ -21,9 +22,44 @@ class Status(models.Model): terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.CASCADE) date_created = models.DateTimeField(auto_now_add=True) + CACHE_KEY = 'TERMINAL_STATUS_{}' + class Meta: db_table = 'terminal_status' get_latest_by = 'date_created' verbose_name = _("Status") + @classmethod + def get_terminal_latest_stat(cls, terminal): + key = cls.CACHE_KEY.format(terminal.id) + data = cache.get(key) + if not data: + return None + data.pop('terminal', None) + stat = cls(**data) + stat.terminal = terminal + stat.is_alive = terminal.is_alive + stat.keep_one_decimal_place() + return stat + def keep_one_decimal_place(self): + keys = ['cpu_load', 'memory_used', 'disk_used'] + for key in keys: + value = getattr(self, key, 0) + if not isinstance(value, (int, float)): + continue + value = '%.1f' % value + setattr(self, key, float(value)) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + self.terminal.set_alive(ttl=60 * 3) + return self.save_to_cache() + + def save_to_cache(self): + if not self.terminal: + return + key = self.CACHE_KEY.format(self.terminal.id) + data = model_to_dict(self) + cache.set(key, data, 60 * 3) + return data diff --git a/apps/terminal/models/component/terminal.py b/apps/terminal/models/component/terminal.py index 406d2192a..74364e06a 100644 --- a/apps/terminal/models/component/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -1,6 +1,7 @@ import time - +import uuid from django.conf import settings +from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -10,6 +11,7 @@ from common.utils import get_logger, lazyproperty from orgs.utils import tmp_to_root_org from terminal.const import TerminalType as TypeChoices from users.models import User +from .status import Status from ..session import Session logger = get_logger(__file__) @@ -22,7 +24,7 @@ class TerminalStatusMixin: @lazyproperty def last_stat(self): - return self.status_set.order_by('date_created').last() + return Status.get_terminal_latest_stat(self) @lazyproperty def load(self): @@ -31,9 +33,12 @@ class TerminalStatusMixin: @property def is_alive(self): - if not self.last_stat: - return False - return time.time() - self.last_stat.date_created.timestamp() < 150 + key = self.ALIVE_KEY.format(self.id) + return cache.get(key, False) + + def set_alive(self, ttl=60 * 3): + key = self.ALIVE_KEY.format(self.id) + cache.set(key, True, ttl) class StorageMixin: @@ -136,6 +141,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel): if self.user: setattr(self.user, SKIP_SIGNAL, True) self.user.delete() + self.name = self.name + '_' + uuid.uuid4().hex[:8] self.user = None self.is_deleted = True self.save() diff --git a/apps/terminal/models/session/sharing.py b/apps/terminal/models/session/sharing.py index b71cad900..a62e23a85 100644 --- a/apps/terminal/models/session/sharing.py +++ b/apps/terminal/models/session/sharing.py @@ -31,6 +31,10 @@ class SessionSharing(JMSBaseModel, OrgModelMixin): ) users = models.TextField(blank=True, verbose_name=_("User")) + action_permission = models.CharField( + max_length=16, verbose_name=_('Action permission'), default='writable' + ) + class Meta: ordering = ('-date_created',) verbose_name = _('Session sharing') @@ -142,3 +146,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin): self.date_left = timezone.now() self.is_finished = True self.save() + + @property + def action_permission(self): + return self.sharing.action_permission diff --git a/apps/terminal/serializers/sharing.py b/apps/terminal/serializers/sharing.py index faf4515ad..c7b8f2e4e 100644 --- a/apps/terminal/serializers/sharing.py +++ b/apps/terminal/serializers/sharing.py @@ -1,8 +1,11 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.serializers import OrgResourceModelSerializerMixin -from common.utils.random import random_string +from rest_framework import serializers + +from common.serializers.fields import LabeledChoiceField from common.utils.common import pretty_string +from common.utils.random import random_string +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from ..const import ActionPermission from ..models import SessionSharing, SessionJoinRecord __all__ = ['SessionSharingSerializer', 'SessionJoinRecordSerializer'] @@ -12,13 +15,18 @@ class SessionSharingSerializer(OrgResourceModelSerializerMixin): users = serializers.ListSerializer( child=serializers.CharField(max_length=36), allow_null=True, write_only=True ) + action_permission = LabeledChoiceField( + default=ActionPermission.writable, choices=ActionPermission.choices, + write_only=True, label=_('Action permission') + ) class Meta: model = SessionSharing fields_mini = ['id'] fields_small = fields_mini + [ 'verify_code', 'is_active', 'expired_time', 'created_by', - 'date_created', 'date_updated', 'users', 'users_display' + 'date_created', 'date_updated', 'users', 'users_display', + 'action_permission' ] fields_fk = ['session', 'creator'] fields = fields_small + fields_fk @@ -40,13 +48,17 @@ class SessionSharingSerializer(OrgResourceModelSerializerMixin): class SessionJoinRecordSerializer(OrgResourceModelSerializerMixin): + action_permission = LabeledChoiceField( + choices=ActionPermission.choices, read_only=True, label=_('Action permission') + ) + class Meta: model = SessionJoinRecord fields_mini = ['id'] fields_small = fields_mini + [ 'joiner_display', 'verify_code', 'date_joined', 'date_left', 'remote_addr', 'login_from', 'is_success', 'reason', 'is_finished', - 'created_by', 'date_created', 'date_updated' + 'created_by', 'date_created', 'date_updated', 'action_permission' ] fields_fk = ['session', 'sharing', 'joiner'] fields = fields_small + fields_fk diff --git a/apps/terminal/utils/components.py b/apps/terminal/utils/components.py index 59d021186..a3de26322 100644 --- a/apps/terminal/utils/components.py +++ b/apps/terminal/utils/components.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # -import time from itertools import groupby from common.utils import get_logger @@ -40,7 +39,7 @@ class ComputeLoadUtil: @classmethod def compute_load(cls, stat): - if not stat or time.time() - stat.date_created.timestamp() > 150: + if not stat: return ComponentLoad.offline system_status_values = cls._compute_system_stat_status(stat).values() if ComponentLoad.critical in system_status_values: diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 85cca8c2a..7502a7525 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -58,10 +58,11 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer): class UserUpdateSecretKeySerializer(serializers.ModelSerializer): new_secret_key = EncryptedField(required=True, max_length=128) new_secret_key_again = EncryptedField(required=True, max_length=128) + has_secret_key = serializers.BooleanField(read_only=True, source='secret_key') class Meta: model = User - fields = ['new_secret_key', 'new_secret_key_again'] + fields = ['has_secret_key', 'new_secret_key', 'new_secret_key_again'] def validate(self, values): new_secret_key = values.get('new_secret_key', '') @@ -114,6 +115,7 @@ class UserProfileSerializer(UserSerializer): MFA_LEVEL_CHOICES = ( (0, _('Disable')), (1, _('Enable')), + (2, _("Force enable")), ) public_key_comment = serializers.CharField( source='get_public_key_comment', required=False, read_only=True, max_length=128 diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 962fbcd24..aa997d27f 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # +import phonenumbers + from functools import partial from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.serializers import CommonBulkSerializerMixin -from common.serializers.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField +from common.serializers.fields import ( + EncryptedField, ObjectRelatedField, LabeledChoiceField, PhoneField +) from common.utils import pretty_string, get_logger from common.validators import PhoneValidator from rbac.builtin import BuiltinRole @@ -101,6 +105,9 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer label=_("Password"), required=False, allow_blank=True, allow_null=True, max_length=1024, ) + phone = PhoneField( + validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, label=_("Phone") + ) custom_m2m_fields = { "system_roles": [BuiltinRole.system_user], "org_roles": [BuiltinRole.org_user], @@ -167,7 +174,6 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer "created_by": {"read_only": True, "allow_blank": True}, "role": {"default": "User"}, "is_otp_secret_key_bound": {"label": _("Is OTP bound")}, - "phone": {"validators": [PhoneValidator()]}, 'mfa_level': {'label': _("MFA level")}, } diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8ef8712ab..e01baa999 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -29,6 +29,7 @@ pycparser==2.21 cryptography==38.0.4 pycryptodome==3.15.0 pycryptodomex==3.15.0 +phonenumbers==8.13.8 gmssl==3.2.1 itsdangerous==1.1.0 pyotp==2.6.0 @@ -65,6 +66,7 @@ pyjwkest==1.4.2 jsonfield2==4.0.0.post0 geoip2==4.5.0 ipip-ipdb==1.6.1 +pywinrm==0.4.3 # Django environment Django==3.2.17 django-bootstrap3==14.2.0 @@ -121,7 +123,7 @@ django-mysql==3.9.0 django-redis==5.2.0 python-redis-lock==3.7.0 pyOpenSSL==22.0.0 -redis==4.3.3 +redis==4.5.4 pyOpenSSL==22.0.0 pymongo==4.2.0 # Debug