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