diff --git a/apps/audits/api.py b/apps/audits/api.py index 0233bc1b2..71b15ceee 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -107,7 +107,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): serializer_class = CommandExecutionHostsRelationSerializer m2m_field = CommandExecution.hosts.field - permission_classes = (IsOrgAdmin,) + permission_classes = [IsOrgAdmin | IsOrgAuditor] filter_fields = [ 'id', 'asset', 'commandexecution' ] diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 986488f2b..4fbf3d5d3 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -34,16 +34,6 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView): class TicketStatusApi(mixins.AuthMixin, APIView): permission_classes = () - def get_ticket(self): - from tickets.models import Ticket - ticket_id = self.request.session.get("auth_ticket_id") - logger.debug('Login confirm ticket id: {}'.format(ticket_id)) - if not ticket_id: - ticket = None - else: - ticket = get_object_or_none(Ticket, pk=ticket_id) - return ticket - def get(self, request, *args, **kwargs): try: self.check_user_login_confirm() diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 38a8a852c..8d6b7765a 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -7,6 +7,7 @@ import time from django.conf import settings from django.contrib.auth import authenticate from django.shortcuts import reverse +from django.contrib.auth import BACKEND_SESSION_KEY from common.utils import get_object_or_none, get_request_ip, get_logger from users.models import User @@ -27,8 +28,14 @@ class AuthMixin: def get_user_from_session(self): if self.request.session.is_empty(): raise errors.SessionEmptyError() - if self.request.user and not self.request.user.is_anonymous: - return self.request.user + + if all((self.request.user, + not self.request.user.is_anonymous, + BACKEND_SESSION_KEY in self.request.session)): + user = self.request.user + user.backend = self.request.session[BACKEND_SESSION_KEY] + return user + user_id = self.request.session.get('user_id') if not user_id: user = None @@ -163,7 +170,7 @@ class AuthMixin: if not ticket_id: ticket = None else: - ticket = get_object_or_none(Ticket, pk=ticket_id) + ticket = Ticket.origin_objects.get(pk=ticket_id) return ticket def get_ticket_or_create(self, confirm_setting): diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index cb697c237..f6750a73d 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -34,7 +34,13 @@ def rsa_decrypt(cipher_text, rsa_private_key=None): if rsa_private_key is None: # rsa_private_key 为 None,可以能是API请求认证,不需要解密 return cipher_text + key = RSA.importKey(rsa_private_key) cipher = PKCS1_v1_5.new(key) - message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode() + cipher_decoded = base64.b64decode(cipher_text.encode()) + # Todo: 弄明白为何要以下这么写,https://xbuba.com/questions/57035263 + if len(cipher_decoded) == 127: + hex_fixed = '00' + cipher_decoded.hex() + cipher_decoded = base64.b16decode(hex_fixed.upper()) + message = cipher.decrypt(cipher_decoded, b'error').decode() return message diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 5493ac3c7..506a8cc87 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -18,6 +18,7 @@ from django.views.generic.edit import FormView from django.conf import settings from django.urls import reverse_lazy +from common.const.front_urls import TICKET_DETAIL from common.utils import get_request_ip, get_object_or_none from users.utils import ( redirect_user_first_login_or_index @@ -185,7 +186,7 @@ class UserLoginWaitConfirmView(TemplateView): context = super().get_context_data(**kwargs) if ticket: timestamp_created = datetime.datetime.timestamp(ticket.date_created) - ticket_detail_url = reverse('tickets:ticket-detail', kwargs={'pk': ticket_id}) + ticket_detail_url = TICKET_DETAIL.format(id=ticket_id) msg = _("""Wait for {} confirm, You also can copy link to her/him
Don't close this page""").format(ticket.assignees_display) else: diff --git a/apps/common/const/front_urls.py b/apps/common/const/front_urls.py new file mode 100644 index 000000000..12d47ed17 --- /dev/null +++ b/apps/common/const/front_urls.py @@ -0,0 +1,2 @@ + +TICKET_DETAIL = '/ui/#/tickets/tickets/{id}' diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 43aa3fe3f..40f665af1 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -182,3 +182,9 @@ class CanUpdateDeleteUser(permissions.BasePermission): if request.method in ['PUT', 'PATCH']: return self.has_update_object_permission(request, view, obj) return True + + +class IsObjectOwner(IsValidUser): + def has_object_permission(self, request, view, obj): + return (super().has_object_permission(request, view, obj) and + request.user == getattr(obj, 'user', None)) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 3d8b6098f..dcbf439da 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -247,7 +247,7 @@ class Config(dict): 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, 'WS_LISTEN_PORT': 8070, - 'LOGIN_LOG_KEEP_DAYS': 90, + 'LOGIN_LOG_KEEP_DAYS': 9999, 'TASK_LOG_KEEP_DAYS': 10, 'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'SECURITY_MFA_VERIFY_TTL': 3600, diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index a0b386383..134d599a6 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -52,6 +52,7 @@ def redirect_format_api(request, *args, **kwargs): return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404) +@csrf_exempt def redirect_old_apps_view(request, *args, **kwargs): path = request.get_full_path() if path.find('/core') != -1: diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 27d124dba..e0e23c4e4 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 6a8c43190..ec532c0b6 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-08-07 18:48+0800\n" +"POT-Creation-Date: 2020-08-19 18:10+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: applications/const.py:52 +#: applications/const.py:53 msgid "Custom" msgstr "自定义" @@ -27,9 +27,9 @@ msgstr "自定义" #: assets/models/cmd_filter.py:21 assets/models/domain.py:20 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:22 perms/models/base.py:48 settings/models.py:27 -#: terminal/models.py:26 terminal/models.py:342 terminal/models.py:374 -#: terminal/models.py:411 users/forms/profile.py:20 users/models/group.py:15 -#: users/models/user.py:489 users/templates/users/_select_user_modal.html:13 +#: terminal/models.py:27 terminal/models.py:344 terminal/models.py:376 +#: terminal/models.py:413 users/forms/profile.py:20 users/models/group.py:15 +#: users/models/user.py:491 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 @@ -47,7 +47,7 @@ msgid "Name" msgstr "名称" #: applications/models/database_app.py:22 applications/models/k8s_app.py:14 -#: assets/models/cmd_filter.py:52 terminal/models.py:376 terminal/models.py:413 +#: assets/models/cmd_filter.py:52 terminal/models.py:378 terminal/models.py:415 #: tickets/models/ticket.py:40 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" @@ -77,9 +77,9 @@ msgstr "数据库" #: assets/models/cmd_filter.py:57 assets/models/domain.py:21 #: assets/models/domain.py:54 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:25 -#: perms/models/base.py:56 settings/models.py:32 terminal/models.py:36 -#: terminal/models.py:381 terminal/models.py:418 users/models/group.py:16 -#: users/models/user.py:522 users/templates/users/user_detail.html:115 +#: perms/models/base.py:56 settings/models.py:32 terminal/models.py:37 +#: terminal/models.py:383 terminal/models.py:420 users/models/group.py:16 +#: users/models/user.py:524 users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 #: users/templates/users/user_group_detail.html:62 @@ -116,11 +116,11 @@ msgstr "Kubernetes应用" #: applications/models/remote_app.py:23 assets/models/asset.py:352 #: assets/models/authbook.py:26 assets/models/gathered_user.py:14 #: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:47 -#: assets/serializers/asset_user.py:84 assets/serializers/system_user.py:44 -#: assets/serializers/system_user.py:176 audits/models.py:38 +#: assets/serializers/asset_user.py:84 assets/serializers/system_user.py:46 +#: assets/serializers/system_user.py:179 audits/models.py:38 #: perms/forms/asset_permission.py:89 perms/models/asset_permission.py:90 #: templates/index.html:82 terminal/backends/command/models.py:19 -#: terminal/backends/command/serializers.py:13 terminal/models.py:187 +#: terminal/backends/command/serializers.py:13 terminal/models.py:188 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 @@ -146,8 +146,8 @@ msgstr "参数" #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: assets/models/group.py:21 common/db/models.py:67 common/mixins/models.py:49 -#: orgs/models.py:23 orgs/models.py:326 perms/models/base.py:54 -#: users/models/user.py:530 users/serializers/group.py:35 +#: orgs/models.py:23 orgs/models.py:389 perms/models/base.py:54 +#: users/models/user.py:532 users/serializers/group.py:35 #: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 @@ -161,7 +161,7 @@ msgstr "创建者" #: assets/models/domain.py:23 assets/models/gathered_user.py:19 #: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:69 #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 -#: orgs/models.py:24 orgs/models.py:324 perms/models/base.py:55 +#: orgs/models.py:24 orgs/models.py:387 perms/models/base.py:55 #: users/models/group.py:18 users/templates/users/user_group_detail.html:58 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149 msgid "Date created" @@ -233,7 +233,7 @@ msgid "Hostname" msgstr "主机名" #: assets/models/asset.py:190 assets/models/domain.py:52 -#: assets/models/user.py:116 terminal/serializers/session.py:29 +#: assets/models/user.py:117 terminal/serializers/session.py:29 msgid "Protocol" msgstr "协议" @@ -247,7 +247,7 @@ msgstr "协议组" msgid "Domain" msgstr "网域" -#: assets/models/asset.py:195 assets/models/user.py:111 +#: assets/models/asset.py:195 assets/models/user.py:112 #: perms/models/asset_permission.py:91 #: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 @@ -261,7 +261,7 @@ msgid "Is active" msgstr "激活" #: assets/models/asset.py:199 assets/models/cluster.py:19 -#: assets/models/user.py:65 templates/_nav.html:44 +#: assets/models/user.py:66 templates/_nav.html:44 #: xpack/plugins/cloud/models.py:133 xpack/plugins/cloud/serializers.py:83 msgid "Admin user" msgstr "管理用户" @@ -354,7 +354,7 @@ msgstr "" #: audits/models.py:99 authentication/forms.py:11 #: authentication/templates/authentication/login.html:21 #: authentication/templates/authentication/xpack_login.html:101 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:487 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:489 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -395,7 +395,7 @@ msgstr "SSH公钥" #: assets/models/base.py:239 assets/models/gathered_user.py:20 #: common/db/models.py:70 common/mixins/models.py:51 ops/models/adhoc.py:39 -#: orgs/models.py:325 +#: orgs/models.py:388 msgid "Date updated" msgstr "更新日期" @@ -407,7 +407,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:508 +#: assets/models/cluster.py:22 users/models/user.py:510 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -433,7 +433,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:655 +#: users/models/user.py:657 msgid "System" msgstr "系统" @@ -453,7 +453,7 @@ msgstr "北京电信" msgid "BGP full netcom" msgstr "BGP全网通" -#: assets/models/cmd_filter.py:33 assets/models/user.py:121 +#: assets/models/cmd_filter.py:33 assets/models/user.py:122 msgid "Command filter" msgstr "命令过滤器" @@ -462,7 +462,7 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:41 ops/models/command.py:23 -#: terminal/backends/command/serializers.py:15 terminal/models.py:196 +#: terminal/backends/command/serializers.py:15 terminal/models.py:197 msgid "Command" msgstr "命令" @@ -478,7 +478,7 @@ msgstr "允许" msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:53 assets/models/user.py:115 +#: assets/models/cmd_filter.py:53 assets/models/user.py:116 msgid "Priority" msgstr "优先级" @@ -547,15 +547,15 @@ msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: audits/models.py:69 audits/serializers.py:77 authentication/models.py:46 -#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:322 +#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:385 #: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 -#: terminal/backends/command/serializers.py:12 terminal/models.py:185 +#: terminal/backends/command/serializers.py:12 terminal/models.py:186 #: tickets/models/ticket.py:30 tickets/models/ticket.py:137 #: tickets/serializers/request_asset_perm.py:65 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 -#: users/models/user.py:157 users/models/user.py:643 +#: users/models/user.py:158 users/models/user.py:645 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -597,8 +597,8 @@ msgstr "收藏夹" msgid "Key" msgstr "键" -#: assets/models/node.py:511 assets/serializers/system_user.py:43 -#: assets/serializers/system_user.py:175 perms/forms/asset_permission.py:92 +#: assets/models/node.py:511 assets/serializers/system_user.py:45 +#: assets/serializers/system_user.py:178 perms/forms/asset_permission.py:92 #: perms/forms/asset_permission.py:99 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 @@ -607,65 +607,73 @@ msgstr "键" msgid "Node" msgstr "节点" -#: assets/models/user.py:107 +#: assets/models/user.py:108 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:108 +#: assets/models/user.py:109 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:110 +#: assets/models/user.py:111 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:112 templates/_nav.html:39 +#: assets/models/user.py:113 templates/_nav.html:39 #: xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产管理" -#: assets/models/user.py:113 templates/_nav.html:17 +#: assets/models/user.py:114 templates/_nav.html:17 #: users/views/profile/password.py:42 users/views/profile/pubkey.py:36 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:114 users/templates/users/user_group_list.html:90 +#: assets/models/user.py:115 users/templates/users/user_group_list.html:90 #: users/templates/users/user_profile.html:124 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:117 +#: assets/models/user.py:118 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:118 +#: assets/models/user.py:119 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:119 +#: assets/models/user.py:120 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:120 +#: assets/models/user.py:121 msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:122 +#: assets/models/user.py:123 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:123 authentication/models.py:88 +#: assets/models/user.py:124 authentication/models.py:88 msgid "Token" msgstr "" -#: assets/models/user.py:198 audits/models.py:39 +#: assets/models/user.py:125 +msgid "Home" +msgstr "家目录" + +#: assets/models/user.py:126 +msgid "System groups" +msgstr "用户组" + +#: assets/models/user.py:201 audits/models.py:39 #: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 #: perms/models/asset_permission.py:92 #: perms/models/database_app_permission.py:22 #: perms/models/k8s_app_permission.py:22 #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models.py:189 +#: terminal/backends/command/serializers.py:14 terminal/models.py:190 #: tickets/serializers/request_asset_perm.py:27 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 @@ -725,14 +733,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:519 users/templates/users/user_password_update.html:48 +#: users/models/user.py:521 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:516 +#: assets/serializers/asset_user.py:79 users/models/user.py:518 msgid "Private key" msgstr "ssh私钥" @@ -753,23 +761,23 @@ msgstr "值" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:45 assets/serializers/system_user.py:177 +#: assets/serializers/system_user.py:47 assets/serializers/system_user.py:180 msgid "Login mode display" msgstr "登录模式显示" -#: assets/serializers/system_user.py:85 +#: assets/serializers/system_user.py:87 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:98 +#: assets/serializers/system_user.py:100 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:106 +#: assets/serializers/system_user.py:108 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:117 +#: assets/serializers/system_user.py:119 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -821,25 +829,25 @@ msgstr "更新节点资产硬件信息: {}" msgid "Gather assets users" msgstr "收集资产上的用户" -#: assets/tasks/push_system_user.py:148 +#: assets/tasks/push_system_user.py:176 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" -#: assets/tasks/push_system_user.py:179 +#: assets/tasks/push_system_user.py:207 msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" -#: assets/tasks/push_system_user.py:180 +#: assets/tasks/push_system_user.py:208 #: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:197 assets/tasks/push_system_user.py:213 +#: assets/tasks/push_system_user.py:225 assets/tasks/push_system_user.py:241 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:205 +#: assets/tasks/push_system_user.py:233 msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" @@ -919,7 +927,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/models.py:37 audits/models.py:60 audits/models.py:71 -#: terminal/models.py:192 +#: terminal/models.py:193 msgid "Remote addr" msgstr "远端地址" @@ -937,7 +945,7 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:29 +#: terminal/models.py:200 tickets/serializers/request_asset_perm.py:29 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/gathered_user/models.py:76 @@ -1017,7 +1025,7 @@ msgstr "Agent" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:511 +#: users/forms/profile.py:52 users/models/user.py:513 #: users/serializers/user.py:240 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" @@ -1191,7 +1199,7 @@ msgstr "登录复核 {}" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:218 authentication/views/login.py:237 +#: authentication/errors.py:218 authentication/views/login.py:243 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" @@ -1257,7 +1265,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:409 users/serializers/user.py:237 +#: users/models/user.py:411 users/serializers/user.py:237 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1266,7 +1274,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:410 users/serializers/user.py:238 +#: users/models/user.py:412 users/serializers/user.py:238 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1374,11 +1382,11 @@ msgstr "复制成功" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:83 +#: authentication/views/login.py:84 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:183 +#: authentication/views/login.py:189 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1386,19 +1394,19 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:188 +#: authentication/views/login.py:194 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:220 +#: authentication/views/login.py:226 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:221 +#: authentication/views/login.py:227 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:236 +#: authentication/views/login.py:242 msgid "Please change your password" msgstr "请修改密码" @@ -1670,14 +1678,14 @@ msgstr "磁盘使用率超过 80%: {} => {}" #: orgs/api.py:58 msgid "Organization contains undeleted resources" -msgstr "" +msgstr "组织包含未删除的资源" #: orgs/api.py:62 msgid "The current organization cannot be deleted" -msgstr "" +msgstr "当前组织不能被删除" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:40 -#: orgs/models.py:321 +#: orgs/models.py:384 msgid "Organization" msgstr "组织" @@ -1689,7 +1697,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:323 users/forms/user.py:27 users/models/user.py:499 +#: orgs/models.py:386 users/forms/user.py:27 users/models/user.py:501 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1714,7 +1722,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:495 users/serializers/user.py:49 +#: users/models/user.py:497 users/serializers/user.py:48 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 @@ -1781,7 +1789,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:31 -#: users/models/user.py:527 users/templates/users/user_detail.html:93 +#: users/models/user.py:529 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2469,96 +2477,97 @@ msgstr "风险等级" msgid "Bulk create not support" msgstr "不支持批量创建" -#: terminal/models.py:27 +#: terminal/models.py:28 msgid "Remote Address" msgstr "远端地址" -#: terminal/models.py:28 +#: terminal/models.py:29 msgid "SSH Port" msgstr "SSH端口" -#: terminal/models.py:29 +#: terminal/models.py:30 msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:30 +#: terminal/models.py:31 msgid "Command storage" msgstr "命令存储" -#: terminal/models.py:31 +#: terminal/models.py:32 msgid "Replay storage" msgstr "录像存储" -#: terminal/models.py:154 +#: terminal/models.py:155 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:155 +#: terminal/models.py:156 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:156 +#: terminal/models.py:157 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:157 +#: terminal/models.py:158 msgid "Connections" msgstr "连接数" -#: terminal/models.py:158 +#: terminal/models.py:159 msgid "Threads" msgstr "线程数" -#: terminal/models.py:159 +#: terminal/models.py:160 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:191 +#: terminal/models.py:192 msgid "Login from" msgstr "登录来源" -#: terminal/models.py:195 +#: terminal/models.py:196 msgid "Replay" msgstr "回放" -#: terminal/models.py:200 +#: terminal/models.py:201 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:343 +#: terminal/models.py:345 msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:45 +#: tickets/api/request_asset_perm.py:46 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:90 +#: tickets/api/request_asset_perm.py:91 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:93 +#: tickets/api/request_asset_perm.py:94 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:97 +#: tickets/api/request_asset_perm.py:98 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:101 +#: tickets/api/request_asset_perm.py:102 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:104 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:105 tickets/api/request_asset_perm.py:112 +#: xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:112 +#: tickets/api/request_asset_perm.py:120 msgid "From request ticket: {} {}" msgstr "来自工单申请: {} {}" -#: tickets/api/request_asset_perm.py:114 +#: tickets/api/request_asset_perm.py:122 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" @@ -2576,11 +2585,11 @@ msgstr "不能操作该工单" #: tickets/models/ticket.py:18 tickets/models/ticket.py:69 msgid "Open" -msgstr "开启" +msgstr "待处理" #: tickets/models/ticket.py:19 msgid "Closed" -msgstr "关闭" +msgstr "已完成" #: tickets/models/ticket.py:22 msgid "General" @@ -2741,7 +2750,7 @@ msgstr "" " \n" " " -#: users/api/user.py:147 +#: users/api/user.py:158 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2787,7 +2796,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:491 +#: users/forms/profile.py:89 users/models/user.py:493 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -2828,7 +2837,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:534 +#: users/forms/user.py:31 users/models/user.py:536 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -2848,93 +2857,93 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:37 +#: users/forms/user.py:124 users/serializers/user.py:36 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 users/serializers/user.py:38 +#: users/forms/user.py:125 users/serializers/user.py:37 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:45 +#: users/forms/user.py:132 users/serializers/user.py:44 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:156 -msgid "Super administrator" -msgstr "超级管理员" - -#: users/models/user.py:158 -msgid "Super auditor" -msgstr "超级审计员" +#: users/models/user.py:157 +msgid "System administrator" +msgstr "系统管理员" #: users/models/user.py:159 +msgid "System auditor" +msgstr "系统审计员" + +#: users/models/user.py:160 msgid "Application" msgstr "应用程序" -#: users/models/user.py:411 users/templates/users/user_profile.html:90 +#: users/models/user.py:413 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:478 +#: users/models/user.py:480 msgid "Local" msgstr "数据库" -#: users/models/user.py:502 +#: users/models/user.py:504 msgid "Avatar" msgstr "头像" -#: users/models/user.py:505 users/templates/users/user_detail.html:68 +#: users/models/user.py:507 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:538 +#: users/models/user.py:540 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:651 +#: users/models/user.py:653 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:654 +#: users/models/user.py:656 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:55 users/serializers/user.py:93 +#: users/serializers/user.py:53 users/serializers/user.py:88 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:59 +#: users/serializers/user.py:55 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:84 users/serializers/user.py:253 +#: users/serializers/user.py:79 users/serializers/user.py:253 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:85 +#: users/serializers/user.py:80 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:86 +#: users/serializers/user.py:81 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:87 +#: users/serializers/user.py:82 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:91 +#: users/serializers/user.py:86 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:92 +#: users/serializers/user.py:87 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:94 +#: users/serializers/user.py:89 msgid "Super role name" msgstr "超级角色名称" @@ -4072,6 +4081,12 @@ msgstr "企业版" msgid "Ultimate edition" msgstr "旗舰版" +#~ msgid "Organization User" +#~ msgstr "组织用户" + +#~ msgid "System User" +#~ msgstr "系统用户" + #~ msgid "Auditor" #~ msgstr "审计员" @@ -4489,9 +4504,6 @@ msgstr "旗舰版" #~ msgid "Have existed: " #~ msgstr "已经存在: " -#~ msgid "Home" -#~ msgstr "家目录" - #~ msgid "Uid" #~ msgstr "Uid" diff --git a/apps/orgs/migrations/0008_auto_20200819_2041.py b/apps/orgs/migrations/0008_auto_20200819_2041.py new file mode 100644 index 000000000..524d29e39 --- /dev/null +++ b/apps/orgs/migrations/0008_auto_20200819_2041.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-08-19 12:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0007_auto_20200728_1805'), + ] + + operations = [ + migrations.AlterField( + model_name='organizationmember', + name='role', + field=models.CharField(choices=[('Admin', 'Organization administrator'), ('Auditor', 'Organization auditor'), ('User', 'User')], default='User', max_length=16, verbose_name='Role'), + ), + ] diff --git a/apps/orgs/models.py b/apps/orgs/models.py index c72d1ae82..f76d88e2a 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -13,8 +13,8 @@ from common.db.models import ChoiceSet class ROLE(ChoiceSet): ADMIN = choices.ADMIN, _('Organization administrator') - USER = choices.USER, _('User') AUDITOR = choices.AUDITOR, _("Organization auditor") + USER = choices.USER, _('User') class Organization(models.Model): @@ -229,15 +229,44 @@ def _none2list(*args): return ([] if v is None else v for v in args) +class UserRoleMapper(dict): + def __init__(self, container=set): + super().__init__() + self.users = container() + self.admins = container() + self.auditors = container() + + self[ROLE.USER] = self.users + self[ROLE.ADMIN] = self.admins + self[ROLE.AUDITOR] = self.auditors + + class OrgMemeberManager(models.Manager): + def remove_users(self, org, users): + from users.models import User + pk_set = [] + for user in users: + if hasattr(user, 'pk'): + pk_set.append(user.pk) + else: + pk_set.append(user) + + send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, + model=User, pk_set=pk_set, using=self.db) + send(action="pre_remove") + self.filter(org_id=org.id, user_id__in=pk_set).delete() + send(action="post_remove") + def remove_users_by_role(self, org, users=None, admins=None, auditors=None): + from users.models import User + if not any((users, admins, auditors)): return users, admins, auditors = _none2list(users, admins, auditors) send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, - model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + model=User, pk_set=[*users, *admins, *auditors], using=self.db) send(action="pre_remove") self.filter(org_id=org.id).filter( @@ -248,6 +277,8 @@ class OrgMemeberManager(models.Manager): send(action="post_remove") def add_users_by_role(self, org, users=None, admins=None, auditors=None): + from users.models import User + if not any((users, admins, auditors)): return users, admins, auditors = _none2list(users, admins, auditors) @@ -266,7 +297,7 @@ class OrgMemeberManager(models.Manager): oms_add.append(self.model(org_id=org.id, user_id=user, role=role)) send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, - model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + model=User, pk_set=[*users, *admins, *auditors], using=self.db) send(action='pre_add') self.bulk_create(oms_add) @@ -278,24 +309,56 @@ class OrgMemeberManager(models.Manager): new_users = _convert_to_uuid_set(new_users) return (old_users - new_users), (new_users - old_users) + def set_user_roles(self, org, user, roles): + """ + 设置某个用户在某个组织里的角色 + """ + old_roles = set(self.filter(org_id=org.id, user=user).values_list('role', flat=True)) + new_roles = set(roles) + + roles_remove = old_roles - new_roles + roles_add = new_roles - old_roles + + to_remove = UserRoleMapper() + to_add = UserRoleMapper() + + for role in roles_remove: + if role in to_remove: + to_remove[role].add(user) + for role in roles_add: + if role in to_add: + to_add[role].add(user) + + self.remove_users_by_role( + org, + to_remove.users, + to_remove.admins, + to_remove.auditors + ) + + self.add_users_by_role( + org, + to_add.users, + to_add.admins, + to_add.auditors + ) + def set_users_by_role(self, org, users=None, admins=None, auditors=None): + """ + 给组织设置带角色的用户 + """ + oms = self.filter(org_id=org.id).values_list('role', 'user_id') - old_users, old_admins, old_auditors = set(), set(), set() - - mapper = { - ROLE.USER: old_users, - ROLE.ADMIN: old_admins, - ROLE.AUDITOR: old_auditors - } + old_mapper = UserRoleMapper() for role, user_id in oms: - if role in mapper: - mapper[role].add(user_id) + if role in old_mapper: + old_mapper[role].add(user_id) - users_remove, users_add = self._get_remove_add_set(users, old_users) - admins_remove, admins_add = self._get_remove_add_set(admins, old_admins) - auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_auditors) + users_remove, users_add = self._get_remove_add_set(users, old_mapper.users) + admins_remove, admins_add = self._get_remove_add_set(admins, old_mapper.admins) + auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_mapper.auditors) self.remove_users_by_role( org, diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index d7e8ae2d1..c36d67d89 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -6,13 +6,13 @@ from users.models.user import User from common.serializers import AdaptedBulkListSerializer from common.drf.serializers import BulkModelSerializer from common.db.models import concated_display as display -from .models import Organization, OrganizationMember, ROLE as ORG_ROLE +from .models import Organization, OrganizationMember class OrgSerializer(ModelSerializer): - users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) - admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) - auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) + admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) + auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) class Meta: model = Organization diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index 5b62b8dcf..5bfedabe0 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -1,3 +1,5 @@ +import textwrap + from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action @@ -8,7 +10,7 @@ from orgs.models import Organization, ROLE as ORG_ROLE from users.models.user import User from common.const.http import POST, GET from common.drf.api import JMSModelViewSet -from common.permissions import IsValidUser +from common.permissions import IsValidUser, IsObjectOwner from common.utils.django import get_object_or_none from common.utils.timezone import dt_parser from common.drf.serializers import EmptySerializer @@ -31,6 +33,7 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): 'default': serializers.RequestAssetPermTicketSerializer, 'approve': EmptySerializer, 'reject': EmptySerializer, + 'close': EmptySerializer, 'assignees': serializers.AssigneeSerializer, } permission_classes = (IsValidUser,) @@ -61,13 +64,13 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): ips = ', '.join(meta.get('ips', [])) confirmed_assets = ', '.join(meta.get('confirmed_assets', [])) - return f''' + return textwrap.dedent(f'''\ {_('IP group')}: {ips} {_('Hostname')}: {meta.get('hostname', '')} {_('System user')}: {meta.get('system_user', '')} {_('Confirmed assets')}: {confirmed_assets} {_('Confirmed system user')}: {meta.get('confirmed_system_user', '')} - ''' + ''') @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) def reject(self, request, *args, **kwargs): @@ -103,6 +106,13 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): self._create_asset_permission(instance, assets, system_user) return Response({'detail': _('Succeed')}) + @action(detail=True, methods=[POST], permission_classes=[IsAssignee | IsObjectOwner]) + def close(self, request, *args, **kwargs): + instance = self.get_object() + instance.status = Ticket.STATUS.CLOSED + instance.save() + return Response({'detail': _('Succeed')}) + def _create_asset_permission(self, instance: Ticket, assets, system_user): meta = instance.meta request = self.request diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index 152b5182b..f3d6ee91e 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -4,6 +4,7 @@ from urllib.parse import urljoin from django.conf import settings from django.utils.translation import ugettext as _ +from common.const.front_urls import TICKET_DETAIL from common.utils import get_logger from common.tasks import send_mail_async @@ -20,11 +21,7 @@ def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees): subject = '{}: {}'.format(_("New ticket"), ticket.title) # 这里要设置前端地址,因为要直接跳转到页面 - if ticket.type == ticket.TYPE.REQUEST_ASSET_PERM: - detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/request-asset-perm/{ticket.id}') - else: - detail_url = urljoin(settings.SITE_URL, f'/tickets/tickets/{ticket.id}') - + detail_url = urljoin(settings.SITE_URL, TICKET_DETAIL.format(id=ticket.id)) message = _("""

Your has a new ticket

diff --git a/apps/users/api/user.py b/apps/users/api/user.py index b1c039318..ae4550931 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -56,31 +56,25 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): def perform_create(self, serializer): validated_data = serializer.validated_data - if isinstance(validated_data, list): - org_roles = [item.pop('org_role', None) for item in validated_data] - else: - org_roles = [validated_data.pop('org_role', None)] + # `org_roles` 先 `pop` + if isinstance(validated_data, list): + org_roles = [item.pop('org_roles', []) for item in validated_data] + else: + org_roles = [validated_data.pop('org_roles', [])] + + # 创建用户 users = serializer.save() if isinstance(users, User): users = [users] - if current_org and current_org.is_real(): - mapper = { - ORG_ROLE.USER: [], - ORG_ROLE.ADMIN: [], - ORG_ROLE.AUDITOR: [] - } - for user, role in zip(users, org_roles): - if role in mapper: - mapper[role].append(user) - else: - mapper[ORG_ROLE.USER].append(user) - OrganizationMember.objects.set_users_by_role( - current_org, users=mapper[ORG_ROLE.USER], - admins=mapper[ORG_ROLE.ADMIN], - auditors=mapper[ORG_ROLE.AUDITOR] - ) + # 只有真实存在的组织才真正关联用户 + if current_org and current_org.is_real(): + for user, roles in zip(users, org_roles): + if not roles: + # 当前组织创建的用户,至少是该组织的`User` + roles.append(ORG_ROLE.USER) + OrganizationMember.objects.set_user_roles(current_org, user, roles) self.send_created_signal(users) def get_permissions(self): @@ -101,6 +95,23 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): self.check_object_permissions(self.request, obj) self.perform_destroy(obj) + def perform_update(self, serializer): + validated_data = serializer.validated_data + # `org_roles` 先 `pop` + if isinstance(validated_data, list): + org_roles = [item.pop('org_roles', None) for item in validated_data] + else: + org_roles = [validated_data.pop('org_roles', None)] + + users = serializer.save() + if isinstance(users, User): + users = [users] + if current_org and current_org.is_real(): + for user, roles in zip(users, org_roles): + if roles is not None: + # roles 是 `Node` 表明不需要更新 + OrganizationMember.objects.set_user_roles(current_org, user, roles) + def perform_bulk_update(self, serializer): # TODO: 需要测试 users_ids = [ diff --git a/apps/users/migrations/0029_auto_20200814_1650.py b/apps/users/migrations/0029_auto_20200814_1650.py new file mode 100644 index 000000000..c6cf3517b --- /dev/null +++ b/apps/users/migrations/0029_auto_20200814_1650.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-08-14 08:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0028_auto_20200728_1805'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='role', + field=models.CharField(blank=True, choices=[('Admin', 'System administrator'), ('User', 'User'), ('Auditor', 'System auditor'), ('App', 'Application')], default='User', max_length=10, verbose_name='Role'), + ), + ] diff --git a/apps/users/migrations/0030_auto_20200819_2041.py b/apps/users/migrations/0030_auto_20200819_2041.py new file mode 100644 index 000000000..775d38ac5 --- /dev/null +++ b/apps/users/migrations/0030_auto_20200819_2041.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-08-19 12:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0029_auto_20200814_1650'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='role', + field=models.CharField(blank=True, choices=[('Admin', 'System administrator'), ('Auditor', 'System auditor'), ('User', 'User'), ('App', 'Application')], default='User', max_length=10, verbose_name='Role'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 6bda75b0a..f65b9398e 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -18,6 +18,7 @@ from django.shortcuts import reverse from common.local import LOCAL_DYNAMIC_SETTINGS from orgs.utils import current_org +from orgs.models import OrganizationMember from common.utils import date_expired_default, get_logger, lazyproperty from common import fields from common.const import choices @@ -153,9 +154,9 @@ class AuthMixin: class RoleMixin: class ROLE(ChoiceSet): - ADMIN = choices.ADMIN, _('Super administrator') + ADMIN = choices.ADMIN, _('System administrator') + AUDITOR = choices.AUDITOR, _('System auditor') USER = choices.USER, _('User') - AUDITOR = choices.AUDITOR, _('Super auditor') APP = 'App', _('Application') role = ROLE.USER @@ -164,15 +165,15 @@ class RoleMixin: def role_display(self): return self.get_role_display() - @property - def org_role_display(self): + @lazyproperty + def org_roles(self): from orgs.models import ROLE as ORG_ROLE if not current_org.is_real(): if self.is_superuser: - return ORG_ROLE.ADMIN.label + return [ORG_ROLE.ADMIN] else: - return ORG_ROLE.USER.label + return [ORG_ROLE.USER] if hasattr(self, 'gc_m2m_org_members__role'): names = self.gc_m2m_org_members__role @@ -184,8 +185,24 @@ class RoleMixin: roles = set(self.m2m_org_members.filter( org_id=current_org.id ).values_list('role', flat=True)) + roles = list(roles) + roles.sort() + return roles - return ' | '.join([str(ORG_ROLE[role]) for role in roles if role in ORG_ROLE]) + @lazyproperty + def org_roles_label_list(self): + from orgs.models import ROLE as ORG_ROLE + return [str(ORG_ROLE[role]) for role in self.org_roles if role in ORG_ROLE] + + @lazyproperty + def org_role_display(self): + return ' | '.join(self.org_roles_label_list) + + @lazyproperty + def total_role_display(self): + roles = list({self.role_display, *self.org_roles_label_list}) + roles.sort() + return ' | '.join(roles) def current_org_roles(self): from orgs.models import OrganizationMember, ROLE as ORG_ROLE @@ -314,12 +331,7 @@ class RoleMixin: def remove(self): if not current_org.is_real(): return - if self.can_user_current_org: - current_org.users.remove(self) - if self.can_admin_current_org: - current_org.admins.remove(self) - if self.can_audit_current_org: - current_org.auditors.remove(self) + OrganizationMember.objects.remove_users(current_org, [self]) class TokenMixin: diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 908978d75..d6968a2d8 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -7,7 +7,6 @@ from rest_framework import serializers from common.utils import validate_ssh_public_key from common.mixins import CommonBulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser from common.drf.fields import GroupConcatedPrimaryKeyRelatedField from orgs.models import ROLE as ORG_ROLE @@ -51,17 +50,12 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): login_blocked = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField() - org_role = serializers.ChoiceField( - label=_('Organization role name'), write_only=True, - allow_null=True, required=False, allow_blank=True, - choices=ORG_ROLE.choices - ) - total_role_display = serializers.SerializerMethodField(label=_('Total role name')) + org_roles = serializers.ListField(label=_('Organization role name'), allow_null=True, required=False, + child=serializers.ChoiceField(choices=ORG_ROLE.choices)) key_prefix_block = "_LOGIN_BLOCK_{}" class Meta: model = User - list_serializer_class = AdaptedBulkListSerializer # mini 是指能识别对象的最小单元 fields_mini = ['id', 'name', 'username'] # small 指的是 不需要计算的直接能从一张表中获取到的数据 @@ -75,7 +69,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): ] fields = fields_small + [ 'groups', 'role', 'groups_display', 'role_display', - 'can_update', 'can_delete', 'login_blocked', 'org_role' + 'can_update', 'can_delete', 'login_blocked', 'org_roles' ] extra_kwargs = { @@ -92,6 +86,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'source_display': {'label': _('Source name')}, 'org_role_display': {'label': _('Organization role name')}, 'role_display': {'label': _('Super role name')}, + 'total_role_display': {'label': _('Total role name')} } def __init__(self, *args, **kwargs): @@ -110,9 +105,6 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): choices.pop(User.ROLE.AUDITOR, None) role._choices = choices - def get_total_role_display(self, instance): - return ' | '.join({str(instance.role_display), str(instance.org_role_display)}) - def validate_role(self, value): request = self.context.get('request') if not request.user.is_superuser and value != User.ROLE.USER: