diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 213858e13..b9f1007d9 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -8,6 +8,7 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from common.mixins.api import SuggestionMixin from orgs.utils import tmp_to_root_org +from rest_framework.decorators import action from ..models import SystemUser, Asset from .. import serializers from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer @@ -45,6 +46,32 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): ordering = ('name', ) permission_classes = (IsOrgAdminOrAppUser,) + @action(methods=['get'], detail=False, url_path='su-from') + def su_from(self, request, *args, **kwargs): + """ API 获取可选的 su_from 系统用户""" + queryset = self.filter_queryset(self.get_queryset()) + queryset = queryset.filter( + protocol=SystemUser.Protocol.ssh, login_mode=SystemUser.LOGIN_AUTO + ) + return self.get_paginate_response_if_need(queryset) + + @action(methods=['get'], detail=True, url_path='su-to') + def su_to(self, request, *args, **kwargs): + """ 获取系统用户的所有 su_to 系统用户 """ + pk = kwargs.get('pk') + system_user = get_object_or_404(SystemUser, pk=pk) + queryset = system_user.su_to.all() + queryset = self.filter_queryset(queryset) + return self.get_paginate_response_if_need(queryset) + + def get_paginate_response_if_need(self, queryset): + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ diff --git a/apps/assets/migrations/0080_auto_20211104_1347.py b/apps/assets/migrations/0080_auto_20211104_1347.py new file mode 100644 index 000000000..a456c825e --- /dev/null +++ b/apps/assets/migrations/0080_auto_20211104_1347.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.13 on 2021-11-04 05:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0079_auto_20211102_1922'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='su_enabled', + field=models.BooleanField(default=False, verbose_name='Switch user'), + ), + migrations.AddField( + model_name='systemuser', + name='su_from', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='assets.systemuser', verbose_name='Switch from'), + ), + ] diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 52e3c2af8..48f2bac30 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -208,6 +208,9 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser): home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) ad_domain = models.CharField(default='', max_length=256) + # linux su 命令 (switch user) + su_enabled = models.BooleanField(default=False, verbose_name=_('Switch user')) + su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) def __str__(self): username = self.username @@ -267,6 +270,21 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser): assets = Asset.objects.filter(id__in=asset_ids) return assets + def add_related_assets(self, assets_or_ids): + self.assets.add(*tuple(assets_or_ids)) + self.add_related_assets_to_su_from_if_need(assets_or_ids) + + def add_related_assets_to_su_from_if_need(self, assets_or_ids): + if self.protocol not in [self.Protocol.ssh.value]: + return + if not self.su_enabled: + return + if not self.su_from: + return + if self.su_from.protocol != self.protocol: + return + self.su_from.assets.add(*tuple(assets_or_ids)) + class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index b662d062c..b86740f8c 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -40,6 +40,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'login_mode', 'login_mode_display', 'priority', 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', 'username_same_with_user', 'auto_push', 'auto_generate_key', + 'su_enabled', 'su_from', 'date_created', 'date_updated', 'comment', 'created_by', ] fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes'] @@ -57,7 +58,8 @@ 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')} + 'is_asset_protocol': {'label': _('Is asset protocol')}, + 'su_from': {'help_text': _('Only ssh and automatic login system users are supported')} } def validate_auto_push(self, value): @@ -146,6 +148,29 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): raise serializers.ValidationError(_("Password or private key required")) return password + def validate_su_from(self, su_from: SystemUser): + # self: su enabled + su_enabled = self.get_initial_value('su_enabled', default=False) + if not su_enabled: + return + if not su_from: + error = _('This field is required.') + raise serializers.ValidationError(error) + # self: protocol ssh + protocol = self.get_initial_value('protocol', default=SystemUser.Protocol.ssh.value) + if protocol not in [SystemUser.Protocol.ssh.value]: + error = _('Only ssh protocol system users are allowed') + raise serializers.ValidationError(error) + # su_from: protocol same + if su_from.protocol != protocol: + error = _('The protocol must be consistent with the current user: {}').format(protocol) + raise serializers.ValidationError(error) + # su_from: login model auto + if su_from.login_mode != su_from.LOGIN_AUTO: + error = _('Only system users with automatic login are allowed') + raise serializers.ValidationError(error) + return su_from + def _validate_admin_user(self, attrs): if self.instance: tp = self.instance.type diff --git a/apps/assets/signals_handler/system_user.py b/apps/assets/signals_handler/system_user.py index 00111030c..00b19e110 100644 --- a/apps/assets/signals_handler/system_user.py +++ b/apps/assets/signals_handler/system_user.py @@ -140,3 +140,5 @@ def on_system_user_update(instance: SystemUser, created, **kwargs): logger.info("System user update signal recv: {}".format(instance)) assets = instance.assets.all().valid() push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets]) + # add assets to su_from + instance.add_related_assets_to_su_from_if_need(assets) diff --git a/apps/common/mixins/serializers.py b/apps/common/mixins/serializers.py index e9a1641c4..72b7610d4 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/mixins/serializers.py @@ -298,9 +298,12 @@ class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin): def get_initial_value(self, attr, default=None): value = self.initial_data.get(attr) - if not value and self.instance: + if value is not None: + return value + if self.instance: value = getattr(self.instance, attr, default) - return value + return value + return default class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8d7ab082e..5dbd80da8 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:bdad14124356449843ef2e77801fd1add5147862488baa2f24f5f14f1ad8f125 -size 90869 +oid sha256:ed378b8e141b884f9b6d82135b37446c02d6621b46f282461e733d610b36030d +size 91465 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index f0d24b15a..9584d0cc5 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: 2021-11-03 11:14+0800\n" +"POT-Creation-Date: 2021-11-05 11:41+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -129,7 +129,7 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:37 #: assets/models/asset.py:357 assets/models/authbook.py:18 -#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:233 +#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:258 #: audits/models.py:38 perms/models/asset_permission.py:99 #: templates/index.html:82 terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 @@ -262,7 +262,7 @@ msgid "Custom" msgstr "自定义" #: applications/models/account.py:11 assets/models/authbook.py:19 -#: assets/models/user.py:273 audits/models.py:39 +#: assets/models/user.py:291 audits/models.py:39 #: perms/models/application_permission.py:32 #: perms/models/asset_permission.py:101 templates/_nav.html:45 #: terminal/backends/command/models.py:20 @@ -379,6 +379,7 @@ msgid "Application path" msgstr "应用路径" #: applications/serializers/attrs/application_category/remote_app.py:45 +#: assets/serializers/system_user.py:157 #: xpack/plugins/change_auth_plan/serializers/asset.py:65 #: xpack/plugins/change_auth_plan/serializers/asset.py:68 #: xpack/plugins/change_auth_plan/serializers/asset.py:71 @@ -476,7 +477,7 @@ msgid "Is active" msgstr "激活" #: assets/models/asset.py:193 assets/models/cluster.py:19 -#: assets/models/user.py:187 assets/models/user.py:322 templates/_nav.html:44 +#: assets/models/user.py:187 assets/models/user.py:340 templates/_nav.html:44 msgid "Admin user" msgstr "特权用户" @@ -763,7 +764,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:559 assets/serializers/system_user.py:232 +#: assets/models/node.py:559 assets/serializers/system_user.py:257 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -835,6 +836,14 @@ msgstr "家目录" msgid "System groups" msgstr "用户组" +#: assets/models/user.py:212 +msgid "Switch user" +msgstr "切换用户" + +#: assets/models/user.py:213 +msgid "Switch from" +msgstr "切换自" + #: assets/models/utils.py:35 #, python-format msgid "%(value)s is not an even number" @@ -864,7 +873,7 @@ msgstr "节点名称" msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:104 assets/serializers/system_user.py:251 +#: assets/serializers/asset.py:104 assets/serializers/system_user.py:276 #: orgs/mixins/serializers.py:26 msgid "Org name" msgstr "组织名称" @@ -878,7 +887,7 @@ msgid "private key invalid" msgstr "密钥不合法" #: assets/serializers/domain.py:13 assets/serializers/label.py:12 -#: assets/serializers/system_user.py:56 +#: assets/serializers/system_user.py:57 #: perms/serializers/asset/permission.py:72 msgid "Assets amount" msgstr "资产数量" @@ -912,48 +921,64 @@ msgstr "密钥指纹" msgid "Apps amount" msgstr "应用数量" -#: assets/serializers/system_user.py:55 +#: assets/serializers/system_user.py:56 #: perms/serializers/asset/permission.py:73 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:57 assets/serializers/system_user.py:234 +#: assets/serializers/system_user.py:58 assets/serializers/system_user.py:259 msgid "Login mode display" msgstr "认证方式名称" -#: assets/serializers/system_user.py:59 +#: assets/serializers/system_user.py:60 msgid "Ad domain" msgstr "Ad 网域" -#: assets/serializers/system_user.py:60 +#: assets/serializers/system_user.py:61 msgid "Is asset protocol" msgstr "资产协议" -#: assets/serializers/system_user.py:100 +#: assets/serializers/system_user.py:62 +msgid "Only ssh and automatic login system users are supported" +msgstr "仅支持ssh协议和自动登录的系统用户" + +#: assets/serializers/system_user.py:102 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:110 common/validators.py:14 +#: assets/serializers/system_user.py:112 common/validators.py:14 msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: assets/serializers/system_user.py:119 +#: assets/serializers/system_user.py:121 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:134 +#: assets/serializers/system_user.py:136 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:146 +#: assets/serializers/system_user.py:148 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" -#: assets/serializers/system_user.py:250 +#: assets/serializers/system_user.py:162 +msgid "Only ssh protocol system users are allowed" +msgstr "仅允许ssh协议的系统用户" + +#: assets/serializers/system_user.py:166 +msgid "The protocol must be consistent with the current user: {}" +msgstr "协议必须和当前用户保持一致: {}" + +#: assets/serializers/system_user.py:170 +msgid "Only system users with automatic login are allowed" +msgstr "仅允许自动登录的系统用户" + +#: assets/serializers/system_user.py:275 msgid "System user name" msgstr "系统用户名称" -#: assets/serializers/system_user.py:260 +#: assets/serializers/system_user.py:285 msgid "Asset hostname" msgstr "资产主机名" @@ -3149,7 +3174,8 @@ msgstr "邮件的内容" msgid "" "Tips: When creating a user, send the content of the email, support " "{username} {name} {email} label" -msgstr "提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" +msgstr "" +"提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" #: settings/serializers/email.py:64 msgid "Tips: Email signature (eg:jumpserver)" @@ -5404,8 +5430,8 @@ msgstr "* 新密码不能是最近 {} 次的密码" msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: xpack/plugins/change_auth_plan/api/app.py:113 -#: xpack/plugins/change_auth_plan/api/asset.py:100 +#: xpack/plugins/change_auth_plan/api/app.py:114 +#: xpack/plugins/change_auth_plan/api/asset.py:101 msgid "The parameter 'action' must be [{}]" msgstr "参数 'action' 必须是 [{}]" @@ -5536,15 +5562,15 @@ msgstr "* 请输入正确的密码长度" msgid "* Password length range 6-30 bits" msgstr "* 密码长度范围 6-30 位" -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:248 +#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:249 msgid "Invalid/incorrect password" msgstr "无效/错误 密码" -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:250 +#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:251 msgid "Failed to connect to the host" msgstr "连接主机失败" -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:252 +#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:253 msgid "Data could not be sent to remote" msgstr "无法将数据发送到远程" @@ -5902,7 +5928,7 @@ msgstr "执行次数" msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/utils.py:65 +#: xpack/plugins/cloud/utils.py:68 msgid "Account unavailable" msgstr "账户无效" diff --git a/apps/perms/signals_handler/app_permission.py b/apps/perms/signals_handler/app_permission.py index 84ee58807..779c99dca 100644 --- a/apps/perms/signals_handler/app_permission.py +++ b/apps/perms/signals_handler/app_permission.py @@ -53,7 +53,7 @@ def set_remote_app_asset_system_users_if_need(instance: ApplicationPermission, s system_users = system_users or instance.system_users.all() for system_user in system_users: - system_user.assets.add(*asset_ids) + system_user.add_related_assets(asset_ids) if system_user.username_same_with_user: users = users or instance.users.all() diff --git a/apps/perms/signals_handler/asset_permission.py b/apps/perms/signals_handler/asset_permission.py index 0b2c1aeee..e889c318a 100644 --- a/apps/perms/signals_handler/asset_permission.py +++ b/apps/perms/signals_handler/asset_permission.py @@ -70,7 +70,8 @@ def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwa # TODO 待优化 system_users = instance.system_users.all() for system_user in system_users: - system_user.assets.add(*tuple(assets)) + system_user: SystemUser + system_user.add_related_assets(assets) @receiver(m2m_changed, sender=AssetPermission.system_users.through) @@ -88,7 +89,7 @@ def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs for system_user in system_users: system_user.nodes.add(*tuple(nodes)) - system_user.assets.add(*tuple(assets)) + system_user.add_related_assets(assets) # 动态系统用户,需要关联用户和用户组了 if system_user.username_same_with_user: