feat: 支持二级登录资产 (#7143)

* feat: 支持su切换系统用户

* feat: 支持su切换系统用户

* feat: 支持su切换系统用户
pull/7144/head
Jiangjie.Bai 2021-11-05 16:11:29 +08:00 committed by GitHub
parent bbe2678df3
commit 07c60ca75d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 33 deletions

View File

@ -8,6 +8,7 @@ from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from common.mixins.api import SuggestionMixin from common.mixins.api import SuggestionMixin
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from rest_framework.decorators import action
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
@ -45,6 +46,32 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
ordering = ('name', ) ordering = ('name', )
permission_classes = (IsOrgAdminOrAppUser,) 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): class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
""" """

View File

@ -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'),
),
]

View File

@ -208,6 +208,9 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) 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) system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256) 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): def __str__(self):
username = self.username username = self.username
@ -267,6 +270,21 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
assets = Asset.objects.filter(id__in=asset_ids) assets = Asset.objects.filter(id__in=asset_ids)
return assets 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: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]

View File

@ -40,6 +40,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'login_mode', 'login_mode_display', 'priority', 'login_mode', 'login_mode_display', 'priority',
'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key', 'username_same_with_user', 'auto_push', 'auto_generate_key',
'su_enabled', 'su_from',
'date_created', 'date_updated', 'comment', 'created_by', 'date_created', 'date_updated', 'comment', 'created_by',
] ]
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes'] fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
@ -57,7 +58,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'created_by': {'read_only': True},
'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')}, '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): def validate_auto_push(self, value):
@ -146,6 +148,29 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
raise serializers.ValidationError(_("Password or private key required")) raise serializers.ValidationError(_("Password or private key required"))
return password 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): def _validate_admin_user(self, attrs):
if self.instance: if self.instance:
tp = self.instance.type tp = self.instance.type

View File

@ -140,3 +140,5 @@ def on_system_user_update(instance: SystemUser, created, **kwargs):
logger.info("System user update signal recv: {}".format(instance)) logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid() assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets]) 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)

View File

@ -298,9 +298,12 @@ class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin):
def get_initial_value(self, attr, default=None): def get_initial_value(self, attr, default=None):
value = self.initial_data.get(attr) 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) value = getattr(self.instance, attr, default)
return value return value
return default
class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin):

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bdad14124356449843ef2e77801fd1add5147862488baa2f24f5f14f1ad8f125 oid sha256:ed378b8e141b884f9b6d82135b37446c02d6621b46f282461e733d610b36030d
size 90869 size 91465

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -129,7 +129,7 @@ msgstr "系统用户"
#: acls/models/login_asset_acl.py:22 #: acls/models/login_asset_acl.py:22
#: applications/serializers/attrs/application_category/remote_app.py:37 #: applications/serializers/attrs/application_category/remote_app.py:37
#: assets/models/asset.py:357 assets/models/authbook.py:18 #: 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 #: audits/models.py:38 perms/models/asset_permission.py:99
#: templates/index.html:82 terminal/backends/command/models.py:19 #: templates/index.html:82 terminal/backends/command/models.py:19
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40
@ -262,7 +262,7 @@ msgid "Custom"
msgstr "自定义" msgstr "自定义"
#: applications/models/account.py:11 assets/models/authbook.py:19 #: 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/application_permission.py:32
#: perms/models/asset_permission.py:101 templates/_nav.html:45 #: perms/models/asset_permission.py:101 templates/_nav.html:45
#: terminal/backends/command/models.py:20 #: terminal/backends/command/models.py:20
@ -379,6 +379,7 @@ msgid "Application path"
msgstr "应用路径" msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:45 #: 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:65
#: xpack/plugins/change_auth_plan/serializers/asset.py:68 #: xpack/plugins/change_auth_plan/serializers/asset.py:68
#: xpack/plugins/change_auth_plan/serializers/asset.py:71 #: xpack/plugins/change_auth_plan/serializers/asset.py:71
@ -476,7 +477,7 @@ msgid "Is active"
msgstr "激活" msgstr "激活"
#: assets/models/asset.py:193 assets/models/cluster.py:19 #: 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" msgid "Admin user"
msgstr "特权用户" msgstr "特权用户"
@ -763,7 +764,7 @@ msgstr "全称"
msgid "Parent key" msgid "Parent key"
msgstr "ssh私钥" 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:41
#: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:73
#: users/templates/users/user_asset_permission.html:158 #: users/templates/users/user_asset_permission.html:158
@ -835,6 +836,14 @@ msgstr "家目录"
msgid "System groups" msgid "System groups"
msgstr "用户组" msgstr "用户组"
#: assets/models/user.py:212
msgid "Switch user"
msgstr "切换用户"
#: assets/models/user.py:213
msgid "Switch from"
msgstr "切换自"
#: assets/models/utils.py:35 #: assets/models/utils.py:35
#, python-format #, python-format
msgid "%(value)s is not an even number" msgid "%(value)s is not an even number"
@ -864,7 +873,7 @@ msgstr "节点名称"
msgid "Hardware info" msgid "Hardware info"
msgstr "硬件信息" 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 #: orgs/mixins/serializers.py:26
msgid "Org name" msgid "Org name"
msgstr "组织名称" msgstr "组织名称"
@ -878,7 +887,7 @@ msgid "private key invalid"
msgstr "密钥不合法" msgstr "密钥不合法"
#: assets/serializers/domain.py:13 assets/serializers/label.py:12 #: 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 #: perms/serializers/asset/permission.py:72
msgid "Assets amount" msgid "Assets amount"
msgstr "资产数量" msgstr "资产数量"
@ -912,48 +921,64 @@ msgstr "密钥指纹"
msgid "Apps amount" msgid "Apps amount"
msgstr "应用数量" msgstr "应用数量"
#: assets/serializers/system_user.py:55 #: assets/serializers/system_user.py:56
#: perms/serializers/asset/permission.py:73 #: perms/serializers/asset/permission.py:73
msgid "Nodes amount" msgid "Nodes amount"
msgstr "节点数量" 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" msgid "Login mode display"
msgstr "认证方式名称" msgstr "认证方式名称"
#: assets/serializers/system_user.py:59 #: assets/serializers/system_user.py:60
msgid "Ad domain" msgid "Ad domain"
msgstr "Ad 网域" msgstr "Ad 网域"
#: assets/serializers/system_user.py:60 #: assets/serializers/system_user.py:61
msgid "Is asset protocol" msgid "Is asset protocol"
msgstr "资产协议" 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" msgid "Username same with user with protocol {} only allow 1"
msgstr "用户名和用户相同的一种协议只允许存在一个" 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" msgid "Special char not allowed"
msgstr "不能包含特殊字符" msgstr "不能包含特殊字符"
#: assets/serializers/system_user.py:119 #: assets/serializers/system_user.py:121
msgid "* Automatic login mode must fill in the username." msgid "* Automatic login mode must fill in the username."
msgstr "自动登录模式,必须填写用户名" msgstr "自动登录模式,必须填写用户名"
#: assets/serializers/system_user.py:134 #: assets/serializers/system_user.py:136
msgid "Path should starts with /" msgid "Path should starts with /"
msgstr "路径应该以 / 开头" msgstr "路径应该以 / 开头"
#: assets/serializers/system_user.py:146 #: assets/serializers/system_user.py:148
msgid "Password or private key required" msgid "Password or private key required"
msgstr "密码或密钥密码需要一个" 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" msgid "System user name"
msgstr "系统用户名称" msgstr "系统用户名称"
#: assets/serializers/system_user.py:260 #: assets/serializers/system_user.py:285
msgid "Asset hostname" msgid "Asset hostname"
msgstr "资产主机名" msgstr "资产主机名"
@ -3149,7 +3174,8 @@ msgstr "邮件的内容"
msgid "" msgid ""
"Tips: When creating a user, send the content of the email, support " "Tips: When creating a user, send the content of the email, support "
"{username} {name} {email} label" "{username} {name} {email} label"
msgstr "提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" msgstr ""
"提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签"
#: settings/serializers/email.py:64 #: settings/serializers/email.py:64
msgid "Tips: Email signature (eg:jumpserver)" msgid "Tips: Email signature (eg:jumpserver)"
@ -5404,8 +5430,8 @@ msgstr "* 新密码不能是最近 {} 次的密码"
msgid "Reset password success, return to login page" msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面" msgstr "重置密码成功,返回到登录页面"
#: xpack/plugins/change_auth_plan/api/app.py:113 #: xpack/plugins/change_auth_plan/api/app.py:114
#: xpack/plugins/change_auth_plan/api/asset.py:100 #: xpack/plugins/change_auth_plan/api/asset.py:101
msgid "The parameter 'action' must be [{}]" msgid "The parameter 'action' must be [{}]"
msgstr "参数 'action' 必须是 [{}]" msgstr "参数 'action' 必须是 [{}]"
@ -5536,15 +5562,15 @@ msgstr "* 请输入正确的密码长度"
msgid "* Password length range 6-30 bits" msgid "* Password length range 6-30 bits"
msgstr "* 密码长度范围 6-30 位" 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" msgid "Invalid/incorrect password"
msgstr "无效/错误 密码" 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" msgid "Failed to connect to the host"
msgstr "连接主机失败" 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" msgid "Data could not be sent to remote"
msgstr "无法将数据发送到远程" msgstr "无法将数据发送到远程"
@ -5902,7 +5928,7 @@ msgstr "执行次数"
msgid "Instance count" msgid "Instance count"
msgstr "实例个数" msgstr "实例个数"
#: xpack/plugins/cloud/utils.py:65 #: xpack/plugins/cloud/utils.py:68
msgid "Account unavailable" msgid "Account unavailable"
msgstr "账户无效" msgstr "账户无效"

View File

@ -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() system_users = system_users or instance.system_users.all()
for system_user in system_users: 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: if system_user.username_same_with_user:
users = users or instance.users.all() users = users or instance.users.all()

View File

@ -70,7 +70,8 @@ def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwa
# TODO 待优化 # TODO 待优化
system_users = instance.system_users.all() system_users = instance.system_users.all()
for system_user in system_users: 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) @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: for system_user in system_users:
system_user.nodes.add(*tuple(nodes)) 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: if system_user.username_same_with_user: