From 905014d4412464295a3bc119ce765809dd8f5d84 Mon Sep 17 00:00:00 2001 From: fit2cloud-jiangweidong <80373698+fit2cloud-jiangweidong@users.noreply.github.com> Date: Thu, 9 Sep 2021 04:04:54 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E5=AF=86=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=95=B0=E6=8D=AE=E5=BA=93=E6=94=B9=E5=AF=86?= =?UTF-8?q?=20(#6709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 改密计划支持数据库改密 * fix: 将数据库账户信息不保存在资产信息里,保存到自己的存储中 * perf: 早餐村 * perf: 修改account * perf: 修改app和系统用户 * perf: 优化系统用户和应用关系 * fix: 修复oracle不可连接问题 Co-authored-by: ibuler Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: Michael Bai --- Dockerfile | 6 + apps/applications/api/account.py | 83 ++- apps/applications/api/application.py | 5 +- .../0010_appaccount_historicalappaccount.py | 76 +++ .../migrations/0011_auto_20210826_1759.py | 40 ++ apps/applications/models/__init__.py | 1 + apps/applications/models/account.py | 88 +++ apps/applications/serializers/application.py | 87 +-- apps/assets/api/system_user_relation.py | 6 +- apps/assets/models/authbook.py | 3 - apps/assets/models/user.py | 4 + apps/assets/serializers/system_user.py | 13 +- .../commands/services/services/base.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 88988 -> 89067 bytes apps/locale/zh/LC_MESSAGES/django.po | 541 +++++++----------- .../api/application/user_group_permission.py | 4 +- .../user_permission_applications.py | 4 +- .../application/user_permission.py | 6 +- apps/perms/signals_handler/__init__.py | 3 +- apps/perms/signals_handler/app_permission.py | 104 ++++ .../{common.py => asset_permission.py} | 113 +--- requirements/requirements.txt | 3 + 22 files changed, 662 insertions(+), 530 deletions(-) create mode 100644 apps/applications/migrations/0010_appaccount_historicalappaccount.py create mode 100644 apps/applications/migrations/0011_auto_20210826_1759.py create mode 100644 apps/perms/signals_handler/app_permission.py rename apps/perms/signals_handler/{common.py => asset_permission.py} (54%) diff --git a/Dockerfile b/Dockerfile index f1a8e17c4..769209673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,12 @@ COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver RUN mkdir -p /root/.ssh/ \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config +RUN mkdir -p /opt/jumpserver/oracle/ +ADD https://f2c-north-rel.oss-cn-qingdao.aliyuncs.com/2.0/north/jumpserver/instantclient-basiclite-linux.x64-21.1.0.0.0.tar /opt/jumpserver/oracle/ +RUN tar xvf /opt/jumpserver/oracle/instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ +RUN sh -c "echo /opt/jumpserver/oracle/instantclient_21_1 > /etc/ld.so.conf.d/oracle-instantclient.conf" +RUN ldconfig + RUN echo > config.yml VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs diff --git a/apps/applications/api/account.py b/apps/applications/api/account.py index 6c95a73c8..3193467c3 100644 --- a/apps/applications/api/account.py +++ b/apps/applications/api/account.py @@ -2,74 +2,57 @@ # from django_filters import rest_framework as filters -from django.db.models import F, Value, CharField -from django.db.models.functions import Concat -from django.http import Http404 +from django.db.models import F, Q from common.drf.filters import BaseFilterSet -from common.drf.api import JMSModelViewSet -from common.utils import unique -from perms.models import ApplicationPermission +from common.drf.api import JMSBulkModelViewSet +from ..models import Account from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify from .. import serializers class AccountFilterSet(BaseFilterSet): username = filters.CharFilter(field_name='username') - app = filters.CharFilter(field_name='applications', lookup_expr='exact') - app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact') + type = filters.CharFilter(field_name='type', lookup_expr='exact') + category = filters.CharFilter(field_name='category', lookup_expr='exact') + app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact') class Meta: - model = ApplicationPermission - fields = ['type', 'category'] + model = Account + fields = ['app', 'systemuser'] + + @property + def qs(self): + qs = super().qs + qs = self.filter_username(qs) + return qs + + def filter_username(self, qs): + username = self.get_query_param('username') + if not username: + return qs + qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct() + return qs -class ApplicationAccountViewSet(JMSModelViewSet): - permission_classes = (IsOrgAdmin, ) - search_fields = ['username', 'app_name'] +class ApplicationAccountViewSet(JMSBulkModelViewSet): + model = Account + search_fields = ['username', 'app_display'] filterset_class = AccountFilterSet - filterset_fields = ['username', 'app_name', 'type', 'category'] - serializer_class = serializers.ApplicationAccountSerializer - http_method_names = ['get', 'put', 'patch', 'options'] + filterset_fields = ['username', 'app_display', 'type', 'category', 'app'] + serializer_class = serializers.AppAccountSerializer + permission_classes = (IsOrgAdmin,) def get_queryset(self): - queryset = ApplicationPermission.objects\ - .exclude(system_users__isnull=True) \ - .exclude(applications__isnull=True) \ - .annotate(uid=Concat( - 'applications', Value('_'), 'system_users', output_field=CharField() - )) \ - .annotate(systemuser=F('system_users')) \ - .annotate(systemuser_display=F('system_users__name')) \ - .annotate(username=F('system_users__username')) \ - .annotate(password=F('system_users__password')) \ - .annotate(app=F('applications')) \ - .annotate(app_name=F("applications__name")) \ - .values('username', 'password', 'systemuser', 'systemuser_display', - 'app', 'app_name', 'category', 'type', 'uid', 'org_id') - return queryset - - def get_object(self): - obj = self.get_queryset().filter( - uid=self.kwargs['pk'] - ).first() - if not obj: - raise Http404() - return obj - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser'])) - return queryset_list - - @staticmethod - def filter_spm_queryset(resource_ids, queryset): - queryset = queryset.filter(uid__in=resource_ids) + queryset = Account.objects.all() \ + .annotate(type=F('app__type')) \ + .annotate(app_display=F('app__name')) \ + .annotate(systemuser_display=F('systemuser__name')) \ + .annotate(category=F('app__category')) return queryset class ApplicationAccountSecretViewSet(ApplicationAccountViewSet): - serializer_class = serializers.ApplicationAccountSecretSerializer + serializer_class = serializers.AppAccountSecretSerializer permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] http_method_names = ['get', 'options'] - diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 1d933bedc..1090e0095 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -23,9 +23,8 @@ class ApplicationViewSet(OrgBulkModelViewSet): search_fields = ('name', 'type', 'category') permission_classes = (IsOrgAdminOrAppUser,) serializer_classes = { - 'default': serializers.ApplicationSerializer, - 'get_tree': TreeNodeSerializer, - 'suggestion': serializers.MiniApplicationSerializer + 'default': serializers.AppSerializer, + 'get_tree': TreeNodeSerializer } @action(methods=['GET'], detail=False, url_path='tree') diff --git a/apps/applications/migrations/0010_appaccount_historicalappaccount.py b/apps/applications/migrations/0010_appaccount_historicalappaccount.py new file mode 100644 index 000000000..fc2cf2ab9 --- /dev/null +++ b/apps/applications/migrations/0010_appaccount_historicalappaccount.py @@ -0,0 +1,76 @@ +# Generated by Django 3.1.12 on 2021-08-26 09:07 + +import assets.models.base +import common.fields.model +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0076_delete_assetuser'), + ('applications', '0009_applicationuser'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalAccount', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), + ('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), + ('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), + ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('version', models.IntegerField(default=1, verbose_name='Version')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')), + ], + options={ + 'verbose_name': 'historical Account', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='Account', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), + ('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), + ('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('version', models.IntegerField(default=1, verbose_name='Version')), + ('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')), + ('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')), + ], + options={ + 'verbose_name': 'Account', + 'unique_together': {('username', 'app', 'systemuser')}, + }, + bases=(models.Model, assets.models.base.AuthMixin), + ), + ] diff --git a/apps/applications/migrations/0011_auto_20210826_1759.py b/apps/applications/migrations/0011_auto_20210826_1759.py new file mode 100644 index 000000000..937102c07 --- /dev/null +++ b/apps/applications/migrations/0011_auto_20210826_1759.py @@ -0,0 +1,40 @@ +# Generated by Django 3.1.12 on 2021-08-26 09:59 + +from django.db import migrations, transaction +from django.db.models import F + + +def migrate_app_account(apps, schema_editor): + db_alias = schema_editor.connection.alias + app_perm_model = apps.get_model("perms", "ApplicationPermission") + app_account_model = apps.get_model("applications", 'Account') + + queryset = app_perm_model.objects \ + .exclude(system_users__isnull=True) \ + .exclude(applications__isnull=True) \ + .annotate(systemuser=F('system_users')) \ + .annotate(app=F('applications')) \ + .values('app', 'systemuser', 'org_id') + + accounts = [] + for p in queryset: + if not p['app']: + continue + account = app_account_model( + app_id=p['app'], systemuser_id=p['systemuser'], + version=1, org_id=p['org_id'] + ) + accounts.append(account) + + app_account_model.objects.using(db_alias).bulk_create(accounts, ignore_conflicts=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0010_appaccount_historicalappaccount'), + ] + + operations = [ + migrations.RunPython(migrate_app_account) + ] diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py index a12310aa4..4bd20e32f 100644 --- a/apps/applications/models/__init__.py +++ b/apps/applications/models/__init__.py @@ -1 +1,2 @@ from .application import * +from .account import * diff --git a/apps/applications/models/account.py b/apps/applications/models/account.py index e69de29bb..ff514590a 100644 --- a/apps/applications/models/account.py +++ b/apps/applications/models/account.py @@ -0,0 +1,88 @@ +from django.db import models +from simple_history.models import HistoricalRecords +from django.utils.translation import ugettext_lazy as _ + +from common.utils import lazyproperty +from assets.models.base import BaseUser + + +class Account(BaseUser): + app = models.ForeignKey('applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Database')) + systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")) + version = models.IntegerField(default=1, verbose_name=_('Version')) + history = HistoricalRecords() + + auth_attrs = ['username', 'password', 'private_key', 'public_key'] + + class Meta: + verbose_name = _('Account') + unique_together = [('username', 'app', 'systemuser')] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.auth_snapshot = {} + + def get_or_systemuser_attr(self, attr): + val = getattr(self, attr, None) + if val: + return val + if self.systemuser: + return getattr(self.systemuser, attr, '') + return '' + + def load_auth(self): + for attr in self.auth_attrs: + value = self.get_or_systemuser_attr(attr) + self.auth_snapshot[attr] = [getattr(self, attr), value] + setattr(self, attr, value) + + def unload_auth(self): + if not self.systemuser: + return + + for attr, values in self.auth_snapshot.items(): + origin_value, loaded_value = values + current_value = getattr(self, attr, '') + if current_value == loaded_value: + setattr(self, attr, origin_value) + + def save(self, *args, **kwargs): + self.unload_auth() + instance = super().save(*args, **kwargs) + self.load_auth() + return instance + + @lazyproperty + def category(self): + return self.app.category + + @lazyproperty + def type(self): + return self.app.type + + @lazyproperty + def app_display(self): + return self.systemuser.name + + @property + def username_display(self): + return self.get_or_systemuser_attr('username') or '' + + @lazyproperty + def systemuser_display(self): + if not self.systemuser: + return '' + return str(self.systemuser) + + @property + def smart_name(self): + username = self.username_display + + if self.app: + app = str(self.app) + else: + app = '*' + return '{}@{}'.format(username, app) + + def __str__(self): + return self.smart_name diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 8310e9f75..79cbdd3bc 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -4,20 +4,23 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from orgs.models import Organization from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from assets.serializers.base import AuthSerializerMixin from common.drf.serializers import MethodSerializer -from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping +from .attrs import ( + category_serializer_classes_mapping, + type_serializer_classes_mapping +) from .. import models from .. import const __all__ = [ - 'ApplicationSerializer', 'ApplicationSerializerMixin', 'MiniApplicationSerializer', - 'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer' + 'AppSerializer', 'AppSerializerMixin', + 'AppAccountSerializer', 'AppAccountSecretSerializer' ] -class ApplicationSerializerMixin(serializers.Serializer): +class AppSerializerMixin(serializers.Serializer): attrs = MethodSerializer() def get_attrs_serializer(self): @@ -45,8 +48,14 @@ class ApplicationSerializerMixin(serializers.Serializer): serializer = serializer_class return serializer + def create(self, validated_data): + return super().create(validated_data) -class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer): + def update(self, instance, validated_data): + return super().update(instance, validated_data) + + +class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer): category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) @@ -69,48 +78,54 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri return _attrs -class ApplicationAccountSerializer(serializers.Serializer): - id = serializers.ReadOnlyField(label=_("Id"), source='uid') - username = serializers.ReadOnlyField(label=_("Username")) - password = serializers.CharField(write_only=True, label=_("Password")) - systemuser = serializers.ReadOnlyField(label=_('System user')) - systemuser_display = serializers.ReadOnlyField(label=_("System user display")) - app = serializers.ReadOnlyField(label=_('App')) - app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True) +class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True) category_display = serializers.SerializerMethodField(label=_('Category display')) type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True) type_display = serializers.SerializerMethodField(label=_('Type display')) - uid = serializers.ReadOnlyField(label=_("Union id")) - org_id = serializers.ReadOnlyField(label=_("Organization")) - org_name = serializers.SerializerMethodField(label=_("Org name")) category_mapper = dict(const.AppCategory.choices) type_mapper = dict(const.AppType.choices) - def create(self, validated_data): - pass - - def update(self, instance, validated_data): - pass + class Meta: + model = models.Account + fields_mini = ['id', 'username', 'version'] + fields_write_only = ['password', 'private_key'] + fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display'] + fields = fields_mini + fields_fk + fields_write_only + [ + 'type', 'type_display', 'category', 'category_display', + ] + extra_kwargs = { + 'username': {'default': '', 'required': False}, + 'password': {'write_only': True}, + 'app_display': {'label': _('Application display')} + } + use_model_bulk_create = True + model_bulk_create_kwargs = { + 'ignore_conflicts': True + } def get_category_display(self, obj): - return self.category_mapper.get(obj['category']) + return self.category_mapper.get(obj.category) def get_type_display(self, obj): - return self.type_mapper.get(obj['type']) + return self.type_mapper.get(obj.type) - @staticmethod - def get_org_name(obj): - org = Organization.get_instance(obj['org_id']) - return org.name + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('systemuser', 'app') + return queryset + + def to_representation(self, instance): + instance.load_auth() + return super().to_representation(instance) -class ApplicationAccountSecretSerializer(ApplicationAccountSerializer): - password = serializers.CharField(write_only=False, label=_("Password")) - - -class MiniApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = models.Application - fields = ApplicationSerializer.Meta.fields_mini +class AppAccountSecretSerializer(AppAccountSerializer): + class Meta(AppAccountSerializer.Meta): + extra_kwargs = { + 'password': {'write_only': False}, + 'private_key': {'write_only': False}, + 'public_key': {'write_only': False}, + } diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py index cd7b64a68..374d45cc2 100644 --- a/apps/assets/api/system_user_relation.py +++ b/apps/assets/api/system_user_relation.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from collections import defaultdict -from django.db.models import F, Value +from django.db.models import F, Value, Model from django.db.models.signals import m2m_changed from django.db.models.functions import Concat @@ -13,13 +13,15 @@ from .. import models, serializers __all__ = [ 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', - 'SystemUserUserRelationViewSet', + 'SystemUserUserRelationViewSet', 'BaseRelationViewSet', ] logger = get_logger(__name__) class RelationMixin: + model: Model + def get_queryset(self): queryset = self.model.objects.all() if not current_org.is_root(): diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 3153608cd..c4e39bcd4 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -16,7 +16,6 @@ class AuthBook(BaseUser, AbsConnectivity): systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")) version = models.IntegerField(default=1, verbose_name=_('Version')) history = HistoricalRecords() - _systemuser_display = '' auth_attrs = ['username', 'password', 'private_key', 'public_key'] @@ -64,8 +63,6 @@ class AuthBook(BaseUser, AbsConnectivity): @lazyproperty def systemuser_display(self): - if self._systemuser_display: - return self._systemuser_display if not self.systemuser: return '' return str(self.systemuser) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index ee802e311..3677144c2 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -73,6 +73,10 @@ class ProtocolMixin: def can_perm_to_asset(self): return self.protocol in self.ASSET_CATEGORY_PROTOCOLS + @property + def is_asset_protocol(self): + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS + class AuthMixin: username_same_with_user: bool diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index c5b9c2064..69848ce20 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -14,7 +14,7 @@ __all__ = [ 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', - 'SystemUserTempAuthSerializer', + 'SystemUserTempAuthSerializer', 'RelationMixin', ] @@ -31,12 +31,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): fields_mini = ['id', 'name', 'username'] fields_write_only = ['password', 'public_key', 'private_key'] fields_small = fields_mini + fields_write_only + [ - 'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display', - 'priority', 'sudo', 'shell', 'sftp_root', 'token', 'ssh_key_fingerprint', - 'home', 'system_groups', 'ad_domain', + 'token', 'ssh_key_fingerprint', + 'type', 'type_display', 'protocol', 'is_asset_protocol', + 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', 'username_same_with_user', 'auto_push', 'auto_generate_key', - 'date_created', 'date_updated', - 'comment', 'created_by', + 'date_created', 'date_updated', 'comment', 'created_by', ] fields_m2m = ['cmd_filters', 'assets_amount'] fields = fields_small + fields_m2m @@ -53,6 +53,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'login_mode_display': {'label': _('Login mode display')}, 'created_by': {'read_only': True}, 'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')}, + 'is_asset_protocol': {'label': _('Is asset protocol')} } def validate_auto_push(self, value): diff --git a/apps/common/management/commands/services/services/base.py b/apps/common/management/commands/services/services/base.py index 5063fb92e..0fb6cb1b3 100644 --- a/apps/common/management/commands/services/services/base.py +++ b/apps/common/management/commands/services/services/base.py @@ -160,7 +160,7 @@ class BaseService(object): if self.process: try: self.process.wait(1) # 不wait,子进程可能无法回收 - except subprocess.TimeoutExpired: + except: pass if self.is_running: diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 3059ebf7a88143f717926c659746475e4cc0b48c..378b949db9a7c8a383691c81694bab826e9c5a46 100644 GIT binary patch delta 25878 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 G$o~U6D9*D0 delta 25846 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 jJB!Euv2@P1o0S7P1|+;Ue8%l*