diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 523689e72aea8b9b86a1a6507d1e1d391746b64d..3d5b67b34281cea82fddc6d62bf184c8631853e1 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -1,11 +1,21 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet -from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin +from ..mixins.api import ( + SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin +) -class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet): +class JmsGenericViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + GenericViewSet): pass -class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet): +class JMSModelViewSet(SerializerMixin2, + QuerySetMixin, + ExtraFilterFieldsMixin, + PaginatedResponseMixin, + ModelViewSet): pass diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index a58e8b079d9ff96f72f7dbf970fb3e79a62a96d3..5c17a5cca2cf736726dac0e28beb77a61cba24b3 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -67,6 +67,17 @@ class ExtraFilterFieldsMixin: return queryset +class PaginatedResponseMixin: + def get_paginated_response_with_query_set(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 CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): pass diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 77a48dc6082ed1dcab0bca9ca3c1d18c6b9732b4..fc9137814bcb0972a2b8e2633f1c792e037701ed 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -11,6 +11,8 @@ import time import ipaddress import psutil +from .timezone import dt_formater + UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') ipip_db = None diff --git a/apps/common/utils/timezone.py b/apps/common/utils/timezone.py new file mode 100644 index 0000000000000000000000000000000000000000..2b8779baebd45068ee7acf6f9337bc1aa5e6d85e --- /dev/null +++ b/apps/common/utils/timezone.py @@ -0,0 +1,33 @@ +import datetime + +import pytz +from django.utils import timezone as dj_timezone +from rest_framework.fields import DateTimeField + +max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) + + +def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo): + assert dj_timezone.is_aware(dt) + return tzinfo.normalize(dt.astimezone(tzinfo)) + + +def as_china_cst(dt: datetime.datetime): + return astimezone(dt, pytz.timezone('Asia/Shanghai')) + + +def as_current_tz(dt: datetime.datetime): + return astimezone(dt, dj_timezone.get_current_timezone()) + + +def utcnow(): + return dj_timezone.now() + + +def now(): + return as_current_tz(utcnow()) + + +_rest_dt_field = DateTimeField() +dt_parser = _rest_dt_field.to_internal_value +dt_formater = _rest_dt_field.to_representation diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index e7d687dc3e003b5a1a33e9a5010cb2ad2fff534c..b691d3ce580452257397feb112b604605b9d8836 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -96,3 +96,5 @@ XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID LOGO_URLS = DYNAMIC.LOGO_URLS CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED + +DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f72e19c129cdb9a828ece94b3631b84326a27fb3..6469256430141bfb82a02a2689a1564796fd76c6 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 dd64657a77326a3fb444eede08d8f22c50a8da13..03a50defd282e948c48553b3a337522e1819ab2d 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-07-27 19:01+0800\n" +"POT-Creation-Date: 2020-07-28 11:25+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -28,7 +28,7 @@ msgstr "自定义" #: 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:473 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:489 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -47,7 +47,7 @@ msgid "Name" msgstr "名称" #: applications/models/database_app.py:22 assets/models/cmd_filter.py:52 -#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:45 +#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:46 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -77,7 +77,7 @@ msgstr "数据库" #: 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:506 +#: users/models/group.py:16 users/models/user.py:522 #: 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 @@ -132,7 +132,7 @@ 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/mixins/models.py:49 orgs/models.py:23 -#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:514 +#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:530 #: 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 @@ -189,7 +189,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:148 tickets/models/ticket.py:40 +#: assets/models/asset.py:148 tickets/models/ticket.py:41 msgid "Meta" msgstr "元数据" @@ -211,7 +211,7 @@ msgstr "IP" #: assets/models/asset.py:187 assets/serializers/asset_user.py:45 #: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 -#: tickets/serializers/request_asset_perm.py:14 +#: tickets/serializers/request_asset_perm.py:21 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -339,7 +339,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:471 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:487 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -391,7 +391,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:492 +#: assets/models/cluster.py:22 users/models/user.py:508 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -417,7 +417,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:635 +#: users/models/user.py:655 msgid "System" msgstr "系统" @@ -485,7 +485,7 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:56 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 #: perms/forms/asset_permission.py:20 -#: tickets/serializers/request_asset_perm.py:54 +#: tickets/serializers/request_asset_perm.py:60 #: tickets/serializers/ticket.py:26 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 @@ -540,10 +540,10 @@ msgstr "默认资产组" #: 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 -#: tickets/models/ticket.py:35 tickets/models/ticket.py:130 -#: tickets/serializers/request_asset_perm.py:55 +#: tickets/models/ticket.py:36 tickets/models/ticket.py:135 +#: tickets/serializers/request_asset_perm.py:61 #: tickets/serializers/ticket.py:27 users/forms/group.py:15 -#: users/models/user.py:157 users/models/user.py:623 +#: users/models/user.py:157 users/models/user.py:643 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -649,7 +649,7 @@ msgstr "SFTP根路径" #: 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 -#: tickets/serializers/request_asset_perm.py:16 +#: tickets/serializers/request_asset_perm.py:23 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -708,14 +708,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:503 users/templates/users/user_password_update.html:48 +#: users/models/user.py:519 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:500 +#: assets/serializers/asset_user.py:79 users/models/user.py:516 msgid "Private key" msgstr "ssh私钥" @@ -875,7 +875,7 @@ msgstr "没有匹配到资产,结束任务" #: users/templates/users/user_list.html:98 #: users/templates/users/user_remote_app_permission.html:111 msgid "Delete" -msgstr "删除" +msgstr "删除文件" #: audits/models.py:27 msgid "Upload" @@ -920,7 +920,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:18 +#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:25 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/gathered_user/models.py:76 @@ -1000,8 +1000,8 @@ 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:495 -#: users/serializers/user.py:224 users/templates/users/user_detail.html:77 +#: users/forms/profile.py:52 users/models/user.py:511 +#: users/serializers/user.py:234 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1011,7 +1011,7 @@ msgstr "多因子认证" msgid "Reason" msgstr "原因" -#: audits/models.py:106 tickets/serializers/request_asset_perm.py:53 +#: audits/models.py:106 tickets/serializers/request_asset_perm.py:59 #: tickets/serializers/ticket.py:25 xpack/plugins/cloud/models.py:211 #: xpack/plugins/cloud/models.py:269 msgid "Status" @@ -1193,7 +1193,7 @@ msgstr "SSH密钥" msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:53 tickets/models/ticket.py:26 +#: authentication/models.py:53 tickets/models/ticket.py:27 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" @@ -1228,7 +1228,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:393 users/serializers/user.py:221 +#: users/models/user.py:409 users/serializers/user.py:231 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1237,7 +1237,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:394 users/serializers/user.py:222 +#: users/models/user.py:410 users/serializers/user.py:232 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1249,7 +1249,7 @@ msgstr "删除成功" #: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/models/ticket.py:70 +#: templates/_modal.html:22 tickets/models/ticket.py:73 msgid "Close" msgstr "关闭" @@ -1628,11 +1628,11 @@ msgstr "磁盘使用率超过 80%: {} => {}" #: orgs/api.py:54 msgid "Organization contains undeleted resources" -msgstr "组织内包含未删除的资源" +msgstr "" #: orgs/api.py:58 msgid "The current organization cannot be deleted" -msgstr "当能删除当前所在组织" +msgstr "" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 #: orgs/models.py:311 @@ -1647,7 +1647,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:483 +#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:499 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1672,7 +1672,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:479 users/serializers/user.py:43 +#: users/models/user.py:495 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 @@ -1719,15 +1719,15 @@ msgstr "上传下载" #: perms/models/asset_permission.py:40 msgid "Clipboard copy" -msgstr "剪切板复制" +msgstr "" #: perms/models/asset_permission.py:41 msgid "Clipboard paste" -msgstr "剪切板粘贴" +msgstr "" #: perms/models/asset_permission.py:42 msgid "Clipboard copy paste" -msgstr "剪切板复制粘贴" +msgstr "" #: perms/models/asset_permission.py:93 msgid "Actions" @@ -1738,8 +1738,8 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 -#: users/models/user.py:511 users/templates/users/user_detail.html:93 +#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:27 +#: users/models/user.py:527 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2479,124 +2479,151 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/api/request_asset_perm.py:41 +#: tickets/api/request_asset_perm.py:43 msgid "Ticket closed" msgstr "工单已关闭" -#: tickets/api/request_asset_perm.py:44 +#: tickets/api/request_asset_perm.py:46 #, python-format msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:69 -msgid "Superuser" -msgstr "超级管理员" - -#: tickets/api/request_asset_perm.py:99 +#: tickets/api/request_asset_perm.py:93 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:102 +#: tickets/api/request_asset_perm.py:96 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:106 +#: tickets/api/request_asset_perm.py:100 msgid "Confirm system-user first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:110 +#: tickets/api/request_asset_perm.py:104 msgid "Confirmed system-user changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:113 xpack/plugins/cloud/models.py:202 +#: tickets/api/request_asset_perm.py:107 xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: tickets/api/request_asset_perm.py:121 +#: tickets/api/request_asset_perm.py:114 +msgid "From request ticket: {} {}" +msgstr "来自工单申请: {} {}" + +#: tickets/api/request_asset_perm.py:116 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" -#: tickets/models/ticket.py:18 tickets/models/ticket.py:72 +#: tickets/models/ticket.py:19 tickets/models/ticket.py:75 msgid "Open" msgstr "开启" -#: tickets/models/ticket.py:19 +#: tickets/models/ticket.py:20 msgid "Closed" msgstr "关闭" -#: tickets/models/ticket.py:25 +#: tickets/models/ticket.py:26 msgid "General" msgstr "一般" -#: tickets/models/ticket.py:27 +#: tickets/models/ticket.py:28 msgid "Request asset permission" msgstr "申请资产权限" -#: tickets/models/ticket.py:32 +#: tickets/models/ticket.py:33 msgid "Approve" msgstr "同意" -#: tickets/models/ticket.py:33 +#: tickets/models/ticket.py:34 msgid "Reject" msgstr "拒绝" -#: tickets/models/ticket.py:36 tickets/models/ticket.py:131 +#: tickets/models/ticket.py:37 tickets/models/ticket.py:136 msgid "User display name" msgstr "用户显示名称" -#: tickets/models/ticket.py:38 +#: tickets/models/ticket.py:39 msgid "Title" msgstr "标题" -#: tickets/models/ticket.py:39 tickets/models/ticket.py:132 +#: tickets/models/ticket.py:40 tickets/models/ticket.py:137 msgid "Body" msgstr "内容" -#: tickets/models/ticket.py:41 +#: tickets/models/ticket.py:42 msgid "Assignee" msgstr "处理人" -#: tickets/models/ticket.py:42 +#: tickets/models/ticket.py:43 msgid "Assignee display name" msgstr "处理人名称" -#: tickets/models/ticket.py:43 +#: tickets/models/ticket.py:44 msgid "Assignees" msgstr "待处理人" -#: tickets/models/ticket.py:44 +#: tickets/models/ticket.py:45 msgid "Assignees display name" msgstr "待处理人名称" -#: tickets/models/ticket.py:73 +#: tickets/models/ticket.py:76 msgid "{} {} this ticket" msgstr "{} {} 这个工单" -#: tickets/models/ticket.py:84 +#: tickets/models/ticket.py:87 msgid "this ticket" msgstr "这个工单" -#: tickets/serializers/request_asset_perm.py:12 +#: tickets/serializers/request_asset_perm.py:19 msgid "IP group" msgstr "IP组" -#: tickets/serializers/request_asset_perm.py:24 +#: tickets/serializers/request_asset_perm.py:31 msgid "Confirmed assets" msgstr "确认的资产" -#: tickets/serializers/request_asset_perm.py:28 +#: tickets/serializers/request_asset_perm.py:34 msgid "Confirmed system user" msgstr "确认的系统用户" -#: tickets/serializers/request_asset_perm.py:65 -msgid "Must be organization admin or superuser" -msgstr "必须是组织管理员或者超级管理员" +#: tickets/serializers/request_asset_perm.py:83 +msgid "Invalid `org_id`" +msgstr "无效的 `org_id`" -#: tickets/utils.py:18 +#: tickets/serializers/request_asset_perm.py:93 +msgid "Field `assignees` must be organization admin or superuser" +msgstr "字段 assignees 必须是组织管理员或者超级管理员" + +#: tickets/serializers/request_asset_perm.py:143 +#, python-brace-format +msgid "" +"\n" +" Type: {type}
\n" +" User: {username}
\n" +" Ip group: {ips}
\n" +" Hostname: {hostname}
\n" +" System user: {system_user}
\n" +" Date start: {date_start}
\n" +" Date expired: {date_expired}
\n" +" " +msgstr "" +"\n" +" 类型: {type}
\n" +" 用户: {username}
\n" +" IP 组: {ips}
\n" +" 主机名: {hostname}
\n" +" 系统用户: {system_user}
\n" +" 开始时间: {date_start}
\n" +" 过期时间: {date_expired}
\n" +" " + +#: tickets/utils.py:20 msgid "New ticket" msgstr "新工单" -#: tickets/utils.py:21 +#: tickets/utils.py:28 #, python-brace-format msgid "" "\n" @@ -2621,11 +2648,11 @@ msgstr "" " \n" " " -#: tickets/utils.py:40 +#: tickets/utils.py:47 msgid "Ticket has been reply" msgstr "工单已被回复" -#: tickets/utils.py:41 +#: tickets/utils.py:48 #, python-brace-format msgid "" "\n" @@ -2656,7 +2683,7 @@ msgstr "" " \n" " " -#: users/api/user.py:126 +#: users/api/user.py:147 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2702,7 +2729,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:475 +#: users/forms/profile.py:89 users/models/user.py:491 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -2738,12 +2765,12 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:185 users/serializers/user.py:266 -#: users/serializers/user.py:324 +#: users/serializers/user.py:194 users/serializers/user.py:276 +#: users/serializers/user.py:334 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:518 +#: users/forms/user.py:31 users/models/user.py:534 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -2763,15 +2790,15 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:31 +#: 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:32 +#: users/forms/user.py:125 users/serializers/user.py:37 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:39 +#: 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" @@ -2789,79 +2816,79 @@ msgstr "超级审计员" msgid "Application" msgstr "应用程序" -#: users/models/user.py:395 users/templates/users/user_profile.html:90 +#: users/models/user.py:411 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:462 +#: users/models/user.py:478 msgid "Local" msgstr "数据库" -#: users/models/user.py:486 +#: users/models/user.py:502 msgid "Avatar" msgstr "头像" -#: users/models/user.py:489 users/templates/users/user_detail.html:68 +#: users/models/user.py:505 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:522 +#: users/models/user.py:538 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:631 +#: users/models/user.py:651 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:634 +#: users/models/user.py:654 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:72 users/serializers/user.py:237 +#: users/serializers/user.py:54 users/serializers/user.py:90 +msgid "Organization role name" +msgstr "组织角色名称" + +#: users/serializers/user.py:81 users/serializers/user.py:247 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:73 +#: users/serializers/user.py:82 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:74 +#: users/serializers/user.py:83 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:75 +#: users/serializers/user.py:84 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:79 +#: users/serializers/user.py:88 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:80 +#: users/serializers/user.py:89 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:81 -msgid "Organization role name" -msgstr "组织角色名称" - -#: users/serializers/user.py:82 +#: users/serializers/user.py:91 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:105 +#: users/serializers/user.py:114 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:117 users/serializers/user.py:290 +#: users/serializers/user.py:126 users/serializers/user.py:300 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:282 +#: users/serializers/user.py:292 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:296 +#: users/serializers/user.py:306 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -3992,18 +4019,15 @@ msgstr "旗舰版" #~ msgid "Role name" #~ msgstr "角色名" -#~ msgid "GUI copy" -#~ msgstr "GUI 复制" - -#~ msgid "GUI paste" -#~ msgstr "GUI 粘贴" - #~ msgid "Covered always" #~ msgstr "总是被覆盖" #~ msgid "Account name" #~ msgstr "账户名称" +#~ msgid "Superuser" +#~ msgstr "超级管理员" + #~ msgid "Auditors cannot be join in the user group" #~ msgstr "审计员不能被加入到用户组" diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 649c450cc96dbd2bdf67142ea205c929192a0dcb..c6c18902d44f234e96144d94add047ff129c9b52 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -62,7 +62,6 @@ class OrgModelMixin(models.Model): org = get_current_org() if org is None: return super().save(*args, **kwargs) - if org.is_real() or org.is_system(): self.org_id = org.id elif org.is_default(): diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py index c79d57d58617cf5aeade3495dac0e4dc77e67858..40f9088389b78d03ad1e1d0e06402ec8cf29efc0 100644 --- a/apps/tickets/api/request_asset_perm.py +++ b/apps/tickets/api/request_asset_perm.py @@ -1,22 +1,23 @@ -from collections import namedtuple - from django.db.transaction import atomic -from django.db.models import F +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.request import Request +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.utils.django import get_object_or_none +from common.utils.timezone import dt_parser from common.drf.serializers import EmptySerializer from perms.models.asset_permission import AssetPermission, Asset from assets.models.user import SystemUser from ..exceptions import ( ConfirmedAssetsChanged, ConfirmedSystemUserChanged, - TicketClosed, TicketActionYet, NotHaveConfirmedAssets, + TicketClosed, TicketActionAlready, NotHaveConfirmedAssets, NotHaveConfirmedSystemUser ) from .. import serializers @@ -25,15 +26,15 @@ from ..permissions import IsAssignee class RequestAssetPermTicketViewSet(JMSModelViewSet): - queryset = Ticket.objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) + queryset = Ticket.origin_objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) serializer_classes = { 'default': serializers.RequestAssetPermTicketSerializer, 'approve': EmptySerializer, 'reject': EmptySerializer, - 'assignees': serializers.OrgAssigneeSerializer, + 'assignees': serializers.AssigneeSerializer, } permission_classes = (IsValidUser,) - filter_fields = ['status', 'title', 'action', 'user_display'] + filter_fields = ['status', 'title', 'action', 'user_display', 'org_id'] search_fields = ['user_display', 'title'] def _check_can_set_action(self, instance, action): @@ -41,49 +42,39 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): raise TicketClosed(detail=_('Ticket closed')) if instance.action == action: action_display = dict(instance.ACTION_CHOICES).get(action) - raise TicketActionYet(detail=_('Ticket has %s') % action_display) + raise TicketActionAlready(detail=_('Ticket has %s') % action_display) @action(detail=False, methods=[GET], permission_classes=[IsValidUser]) - def assignees(self, request, *args, **kwargs): - org_mapper = {} - UserTuple = namedtuple('UserTuple', ('id', 'name', 'username')) + def assignees(self, request: Request, *args, **kwargs): user = request.user - superusers = User.objects.filter(role=User.ROLE.ADMIN) - - admins_with_org = User.objects.filter(related_admin_orgs__users=user).annotate( - org_id=F('related_admin_orgs__id'), org_name=F('related_admin_orgs__name') - ) - - for user in admins_with_org: - org_id = user.org_id - - if org_id not in org_mapper: - org_mapper[org_id] = { - 'org_name': user.org_name, - 'org_admins': set() # 去重 - } - org_mapper[org_id]['org_admins'].add(UserTuple(user.id, user.name, user.username)) - - result = [ - { - 'org_name': _('Superuser'), - 'org_admins': set(UserTuple(user.id, user.name, user.username) - for user in superusers) - } - ] - - for org in org_mapper.values(): - result.append(org) - serializer_class = self.get_serializer_class() - serilizer = serializer_class(instance=result, many=True) - return Response(data=serilizer.data) + org_id = request.query_params.get('org_id', Organization.DEFAULT_ID) + + q = Q(role=User.ROLE.ADMIN) + if org_id != Organization.DEFAULT_ID: + q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) + org_admins = User.objects.filter(q).distinct() + + return self.get_paginated_response_with_query_set(org_admins) + + def _get_extra_comment(self, instance): + meta = instance.meta + ips = ', '.join(meta.get('ips', [])) + confirmed_assets = ', '.join(meta.get('confirmed_assets', [])) + + return 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): instance = self.get_object() action = instance.ACTION_REJECT self._check_can_set_action(instance, action) - instance.perform_action(action, request.user) + instance.perform_action(action, request.user, self._get_extra_comment(instance)) return Response() @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) @@ -109,29 +100,33 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet): if system_user is None: raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed')) - self._create_asset_permission(instance, assets, system_user) + self._create_asset_permission(instance, assets, system_user, request.user) return Response({'detail': _('Succeed')}) - def _create_asset_permission(self, instance: Ticket, assets, system_user): + def _create_asset_permission(self, instance: Ticket, assets, system_user, user): meta = instance.meta request = self.request + ap_kwargs = { - 'name': meta.get('name', ''), + 'name': _('From request ticket: {} {}').format(instance.user_display, instance.id), 'created_by': self.request.user.username, 'comment': _('{} request assets, approved by {}').format(instance.user_display, - instance.assignee_display) + instance.assignees_display) } - date_start = meta.get('date_start') - date_expired = meta.get('date_expired') + date_start = dt_parser(meta.get('date_start')) + date_expired = dt_parser(meta.get('date_expired')) if date_start: ap_kwargs['date_start'] = date_start if date_expired: ap_kwargs['date_expired'] = date_expired with atomic(): - instance.perform_action(instance.ACTION_APPROVE, request.user) + instance.perform_action(instance.ACTION_APPROVE, + request.user, + self._get_extra_comment(instance)) ap = AssetPermission.objects.create(**ap_kwargs) ap.system_users.add(system_user) ap.assets.add(*assets) + ap.users.add(user) return ap diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 5e3d90701e6d7288fd895a2ee2a7373ef2b094ea..5a49c746d917f3d960178e4ea4439d3be1c3d053 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -11,7 +11,7 @@ from .. import serializers, models, mixins class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet): serializer_class = serializers.TicketSerializer - queryset = models.Ticket.objects.all() + queryset = models.Ticket.origin_objects.all() permission_classes = (IsValidUser,) filter_fields = ['status', 'title', 'action', 'user_display'] search_fields = ['user_display', 'title'] diff --git a/apps/tickets/exceptions.py b/apps/tickets/exceptions.py index b8cb7ba5ed78b143702424bf22c98e6e2f36907e..3332139b5e7a268d9e712119f1472782e6b39be0 100644 --- a/apps/tickets/exceptions.py +++ b/apps/tickets/exceptions.py @@ -21,5 +21,9 @@ class TicketClosed(JMSException): pass -class TicketActionYet(JMSException): +class TicketActionAlready(JMSException): + pass + + +class OrgIdRequiredException(JMSException): pass diff --git a/apps/tickets/migrations/0002_auto_20200723_1232.py b/apps/tickets/migrations/0002_auto_20200723_1232.py deleted file mode 100644 index 26d980a4a7b5e68080577184758a8f8d4ba9f4ca..0000000000000000000000000000000000000000 --- a/apps/tickets/migrations/0002_auto_20200723_1232.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2020-07-23 04:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tickets', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='ticket', - name='type', - field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('request_asset', 'Request asset permission')], default='general', max_length=16, verbose_name='Type'), - ), - ] diff --git a/apps/tickets/migrations/0002_auto_20200728_1146.py b/apps/tickets/migrations/0002_auto_20200728_1146.py new file mode 100644 index 0000000000000000000000000000000000000000..3033951443372b3b8500acef588ab68d7690fd0a --- /dev/null +++ b/apps/tickets/migrations/0002_auto_20200728_1146.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.10 on 2020-07-28 03:46 + +from django.db import migrations, models +import django.db.models.manager + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='ticket', + managers=[ + ('origin_objects', django.db.models.manager.Manager()), + ], + ), + migrations.AddField( + model_name='ticket', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('request_asset', 'Request asset permission')], default='general', max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/tickets/mixins.py b/apps/tickets/mixins.py index 6f052df66e0f8637a527cbc222f419fa38036392..c4a48d866a2be10468d5599ba39371cb86778c6b 100644 --- a/apps/tickets/mixins.py +++ b/apps/tickets/mixins.py @@ -6,11 +6,12 @@ from .models import Ticket class TicketMixin: def get_queryset(self): + queryset = super().get_queryset() assign = self.request.GET.get('assign', None) if assign is None: - queryset = Ticket.get_related_tickets(self.request.user) + queryset = Ticket.get_related_tickets(self.request.user, queryset) elif assign in ['1']: - queryset = Ticket.get_assigned_tickets(self.request.user) + queryset = Ticket.get_assigned_tickets(self.request.user, queryset) else: - queryset = Ticket.get_my_tickets(self.request.user) + queryset = Ticket.get_my_tickets(self.request.user, queryset) return queryset diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 631761069cc2f1355cd02d60e5dd5753b874ec48..4f68aa6e0a12287b44abf583a1c523ba047065bf 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -7,11 +7,12 @@ from django.utils.translation import ugettext_lazy as _ from common.mixins.models import CommonModelMixin from common.fields.model import JsonDictTextField +from orgs.mixins.models import OrgModelMixin __all__ = ['Ticket', 'Comment'] -class Ticket(CommonModelMixin): +class Ticket(OrgModelMixin, CommonModelMixin): STATUS_OPEN = 'open' STATUS_CLOSED = 'closed' STATUS_CHOICES = ( @@ -46,6 +47,8 @@ class Ticket(CommonModelMixin): status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open') action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True) + origin_objects = models.Manager() + def __str__(self): return '{}: {}'.format(self.user_display, self.title) @@ -79,13 +82,15 @@ class Ticket(CommonModelMixin): self.status = status self.save() - def create_action_comment(self, action, user): + def create_action_comment(self, action, user, extra_comment=None): action_display = dict(self.ACTION_CHOICES).get(action) body = '{} {} {}'.format(user, action_display, _("this ticket")) + if extra_comment is not None: + body += extra_comment self.comments.create(body=body, user=user, user_display=str(user)) - def perform_action(self, action, user): - self.create_action_comment(action, user) + def perform_action(self, action, user, extra_comment=None): + self.create_action_comment(action, user, extra_comment) self.action = action self.status = self.STATUS_CLOSED self.assignee = user diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py index 9bdce49c1d639806f8fa7027cafdaef2a5d22525..3b2d72b7c3152a812067d0272a154e470fe1c450 100644 --- a/apps/tickets/serializers/request_asset_perm.py +++ b/apps/tickets/serializers/request_asset_perm.py @@ -1,8 +1,15 @@ +from itertools import chain + from rest_framework import serializers +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.db.models import Q +from common.utils.timezone import dt_parser, dt_formater +from orgs.utils import tmp_to_root_org +from orgs.models import Organization, ROLE as ORG_ROLE +from assets.models.asset import Asset from users.models.user import User from ..models import Ticket @@ -22,9 +29,8 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): source='meta.confirmed_assets', default=list, required=False, label=_('Confirmed assets')) - confirmed_system_user = serializers.ListField(child=serializers.UUIDField(), - source='meta.confirmed_system_user', - default=list, required=False, + confirmed_system_user = serializers.UUIDField(source='meta.confirmed_system_user', + default='', required=False, label=_('Confirmed system user')) assets_waitlist_url = serializers.SerializerMethodField() system_user_waitlist_url = serializers.SerializerMethodField() @@ -36,7 +42,7 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): 'status', 'action', 'date_created', 'date_updated', 'system_user_waitlist_url', 'type', 'type_display', 'action_display', 'ips', 'confirmed_assets', 'date_start', 'date_expired', 'confirmed_system_user', 'hostname', - 'assets_waitlist_url', 'system_user' + 'assets_waitlist_url', 'system_user', 'org_id' ] m2m_fields = [ 'user', 'user_display', 'assignees', 'assignees_display', @@ -52,26 +58,44 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): extra_kwargs = { 'status': {'label': _('Status')}, 'action': {'label': _('Action')}, - 'user_display': {'label': _('User')} + 'user_display': {'label': _('User')}, + 'org_id': {'required': True} } - def validate_assignees(self, assignees): + def validate(self, attrs): + org_id = attrs.get('org_id') + assignees = attrs.get('assignees') + + instance = self.instance + if instance is not None: + if org_id and not assignees: + assignees = list(instance.assignees.all()) + elif assignees and not org_id: + org_id = instance.org_id + elif assignees and org_id: + pass + else: + return attrs + user = self.context['request'].user + org = Organization.get_instance(org_id) + if org is None: + raise serializers.ValidationError(_('Invalid `org_id`')) - count = User.objects.filter(Q(related_admin_orgs__users=user) | Q(role=User.ROLE.ADMIN)).filter( - id__in=[assignee.id for assignee in assignees]).distinct().count() + q = Q(role=User.ROLE.ADMIN) + if not org.is_default(): + q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) + q &= Q(id__in=[assignee.id for assignee in assignees]) + count = User.objects.filter(q).distinct().count() if count != len(assignees): - raise serializers.ValidationError(_('Must be organization admin or superuser')) - return assignees + raise serializers.ValidationError(_('Field `assignees` must be organization admin or superuser')) + return attrs def get_system_user_waitlist_url(self, instance: Ticket): if not self._is_assignee(instance): return None - meta = instance.meta - url = reverse('api-assets:system-user-list') - query = meta.get('system_user', '') - return '{}?search={}'.format(url, query) + return reverse('api-assets:system-user-list') def get_assets_waitlist_url(self, instance: Ticket): if not self._is_assignee(instance): @@ -81,37 +105,106 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer): query = '' meta = instance.meta - ips = meta.get('ips', []) hostname = meta.get('hostname') - - if ips: - query = '?ips=%s' % ','.join(ips) - elif hostname: + if hostname: query = '?search=%s' % hostname return asset_api + query + def _recommend_assets(self, data, instance): + confirmed_assets = data.get('confirmed_assets') + if not confirmed_assets and self._is_assignee(instance): + ips = data.get('ips') + hostname = data.get('hostname') + limit = 5 + + q = Q(id=None) + if ips: + limit = len(ips) + 2 + q |= Q(ip__in=ips) + if hostname: + q |= Q(hostname__icontains=hostname) + + data['confirmed_assets'] = list( + map(lambda x: str(x), chain(*Asset.objects.filter(q)[0: limit].values_list('id')))) + + def to_representation(self, instance): + data = super().to_representation(instance) + self._recommend_assets(data, instance) + return data + + def _create_body(self, validated_data): + meta = validated_data['meta'] + type = dict(Ticket.TYPE_CHOICES).get(validated_data.get('type', '')) + date_start = dt_parser(meta.get('date_start')).strftime(settings.DATETIME_DISPLAY_FORMAT) + date_expired = dt_parser(meta.get('date_expired')).strftime(settings.DATETIME_DISPLAY_FORMAT) + + validated_data['body'] = _(''' + Type: {type}
+ User: {username}
+ Ip group: {ips}
+ Hostname: {hostname}
+ System user: {system_user}
+ Date start: {date_start}
+ Date expired: {date_expired}
+ ''').format( + type=type, + username=validated_data.get('user', ''), + ips=', '.join(meta.get('ips', [])), + hostname=meta.get('hostname', ''), + system_user=meta.get('system_user', ''), + date_start=date_start, + date_expired=date_expired + ) + def create(self, validated_data): + # `type` 与 `user` 用户不可提交, validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM validated_data['user'] = self.context['request'].user + # `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉 self._pop_confirmed_fields() + self._create_body(validated_data) return super().create(validated_data) def save(self, **kwargs): + """ + 做了一些数据转换 + """ meta = self.validated_data.get('meta', {}) + + org_id = self.validated_data.get('org_id') + if org_id is not None and org_id == Organization.DEFAULT_ID: + self.validated_data['org_id'] = '' + + # 时间的转换,好烦😭,可能有更好的办法吧 date_start = meta.get('date_start') if date_start: - meta['date_start'] = date_start.strftime('%Y-%m-%d %H:%M:%S%z') + meta['date_start'] = dt_formater(date_start) date_expired = meta.get('date_expired') if date_expired: - meta['date_expired'] = date_expired.strftime('%Y-%m-%d %H:%M:%S%z') - return super().save(**kwargs) + meta['date_expired'] = dt_formater(date_expired) + + # UUID 的转换 + confirmed_system_user = meta.get('confirmed_system_user') + if confirmed_system_user: + meta['confirmed_system_user'] = str(confirmed_system_user) + + confirmed_assets = meta.get('confirmed_assets') + if confirmed_assets: + new_confirmed_assets = [] + for asset in confirmed_assets: + new_confirmed_assets.append(str(asset)) + meta['confirmed_assets'] = new_confirmed_assets + with tmp_to_root_org(): + return super().save(**kwargs) def update(self, instance, validated_data): new_meta = validated_data['meta'] if not self._is_assignee(instance): self._pop_confirmed_fields() + + # Json 字段保存的坑😭 old_meta = instance.meta meta = {} meta.update(old_meta) @@ -134,8 +227,3 @@ class AssigneeSerializer(serializers.Serializer): id = serializers.UUIDField() name = serializers.CharField() username = serializers.CharField() - - -class OrgAssigneeSerializer(serializers.Serializer): - org_name = serializers.CharField() - org_admins = AssigneeSerializer(many=True) diff --git a/apps/tickets/tests.py b/apps/tickets/tests.py index 7ce503c2dd97ba78597f6ff6e4393132753573f6..2b02a9016e58b86790831693f3d6fd9937d35f39 100644 --- a/apps/tickets/tests.py +++ b/apps/tickets/tests.py @@ -1,3 +1,89 @@ -from django.test import TestCase +import datetime -# Create your tests here. +from common.utils.timezone import now +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from orgs.models import Organization, OrganizationMember, ROLE as ORG_ROLE +from orgs.utils import set_current_org +from users.models.user import User +from assets.models import Asset, AdminUser, SystemUser + + +class TicketTest(APITestCase): + def setUp(self): + Organization.objects.bulk_create([ + Organization(name='org-01'), + Organization(name='org-02'), + Organization(name='org-03'), + ]) + org_01, org_02, org_03 = Organization.objects.all() + self.org_01, self.org_02, self.org_03 = org_01, org_02, org_03 + + set_current_org(org_01) + + AdminUser.objects.bulk_create([ + AdminUser(name='au-01', username='au-01'), + AdminUser(name='au-02', username='au-02'), + AdminUser(name='au-03', username='au-03'), + ]) + + SystemUser.objects.bulk_create([ + SystemUser(name='su-01', username='su-01'), + SystemUser(name='su-02', username='su-02'), + SystemUser(name='su-03', username='su-03'), + ]) + + admin_users = AdminUser.objects.all() + Asset.objects.bulk_create([ + Asset(hostname='asset-01', ip='192.168.1.1', public_ip='192.168.1.1', admin_user=admin_users[0]), + Asset(hostname='asset-02', ip='192.168.1.2', public_ip='192.168.1.2', admin_user=admin_users[0]), + Asset(hostname='asset-03', ip='192.168.1.3', public_ip='192.168.1.3', admin_user=admin_users[0]), + ]) + + new_user = User.objects.create + new_org_memeber = OrganizationMember.objects.create + + u = new_user(name='user-01', username='user-01', email='user-01@jms.com') + new_org_memeber(org=org_01, user=u, role=ORG_ROLE.USER) + new_org_memeber(org=org_02, user=u, role=ORG_ROLE.USER) + self.user_01 = u + + u = new_user(name='org-admin-01', username='org-admin-01', email='org-admin-01@jms.com') + new_org_memeber(org=org_01, user=u, role=ORG_ROLE.ADMIN) + self.org_admin_01 = u + + u = new_user(name='org-admin-02', username='org-admin-02', email='org-admin-02@jms.com') + new_org_memeber(org=org_02, user=u, role=ORG_ROLE.ADMIN) + self.org_admin_02 = u + + def test_create_request_asset_perm(self): + url = reverse('api-tickets:ticket-request-asset-perm') + ticket_url = reverse('api-tickets:ticket') + + self.client.force_login(self.user_01) + + date_start = now() + date_expired = date_start + datetime.timedelta(days=7) + + data = { + "title": "request-01", + "ips": [ + "192.168.1.1" + ], + "date_start": date_start, + "date_expired": date_expired, + "hostname": "", + "system_user": "", + "org_id": self.org_01.id, + "assignees": [ + str(self.org_admin_01.id), + str(self.org_admin_02.id), + ] + } + + self.client.post(data) + + self.client.force_login(self.org_admin_01) + res = self.client.get(ticket_url, params={'assgin': 1}) diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index b086aa9d39897f2384520d78a261ddd564c34f79..a7bd3f6e5528f2e19901f80c349259dc0c90c6bb 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -7,7 +7,7 @@ from .. import api app_name = 'tickets' router = BulkRouter() -# router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm') +router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm') router.register('tickets', api.TicketViewSet, 'ticket') router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index 13727a77d0499d9d43b2baad1fb8145caec2a38e..97b5334e08e7a94ae88a4196e58f5b500faef1fe 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -1,23 +1,30 @@ # -*- coding: utf-8 -*- # +from urllib.parse import urljoin from django.conf import settings from django.utils.translation import ugettext as _ -from common.utils import get_logger, reverse +from common.utils import get_logger from common.tasks import send_mail_async logger = get_logger(__name__) +from tickets.models import Ticket -def send_new_ticket_mail_to_assignees(ticket, assignees): +def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees): recipient_list = [user.email for user in assignees] user = ticket.user if not recipient_list: logger.error("Ticket not has assignees: {}".format(ticket.id)) return subject = '{}: {}'.format(_("New ticket"), ticket.title) - detail_url = reverse('tickets:ticket-detail', - kwargs={'pk': ticket.id}, external=True) + + # 这里要设置前端地址,因为要直接跳转到页面 + 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}') + message = _("""

Your has a new ticket

diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 456cd1fbcb735412dad00d7f9e3498f1232dd7fb..82791dbf5f0197e34028288a2d0021266a43ddeb 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -233,6 +233,11 @@ class RoleMixin: def is_app(self): return self.role == self.ROLE.APP + @lazyproperty + def user_all_orgs(self): + from orgs.models import Organization + return Organization.get_user_all_orgs(self) + @lazyproperty def user_orgs(self): from orgs.models import Organization diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 76485eb93eeb639354940969fc9048c3f5e1d90a..21c38d7bc8a58ee21280fec8171431da0f6bcd4a 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -27,6 +27,11 @@ class UserOrgSerializer(serializers.Serializer): name = serializers.CharField() +class UserOrgLabelSerializer(serializers.Serializer): + value = serializers.CharField(source='id') + label = serializers.CharField(source='name') + + class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') CUSTOM_PASSWORD = _('Set password') @@ -214,6 +219,7 @@ class UserRoleSerializer(serializers.Serializer): class UserProfileSerializer(UserSerializer): admin_or_audit_orgs = UserOrgSerializer(many=True, read_only=True) + user_all_orgs = UserOrgLabelSerializer(many=True, read_only=True) current_org_roles = serializers.ListField(read_only=True) public_key_comment = serializers.CharField( source='get_public_key_comment', required=False, read_only=True, max_length=128 @@ -231,7 +237,7 @@ class UserProfileSerializer(UserSerializer): class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + [ 'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles', - 'guide_url' + 'guide_url', 'user_all_orgs' ] extra_kwargs = dict(UserSerializer.Meta.extra_kwargs) extra_kwargs.update({