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