feat(ticket): 调整申请资产工单

pull/4422/head
xinwen 2020-07-09 15:41:02 +08:00 committed by 老广
parent b331730422
commit f8e248f0af
21 changed files with 522 additions and 232 deletions

View File

@ -1,11 +1,21 @@
from rest_framework.viewsets import GenericViewSet, ModelViewSet 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 pass
class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet): class JMSModelViewSet(SerializerMixin2,
QuerySetMixin,
ExtraFilterFieldsMixin,
PaginatedResponseMixin,
ModelViewSet):
pass pass

View File

@ -67,6 +67,17 @@ class ExtraFilterFieldsMixin:
return queryset 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): class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
pass pass

View File

@ -11,6 +11,8 @@ import time
import ipaddress import ipaddress
import psutil import psutil
from .timezone import dt_formater
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
ipip_db = None ipip_db = None

View File

@ -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

View File

@ -96,3 +96,5 @@ XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID
LOGO_URLS = DYNAMIC.LOGO_URLS LOGO_URLS = DYNAMIC.LOGO_URLS
CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -28,7 +28,7 @@ msgstr "自定义"
#: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:22 #: 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 #: 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 #: 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/_select_user_modal.html:13
#: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:37
#: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_asset_permission.html:154
@ -47,7 +47,7 @@ msgid "Name"
msgstr "名称" msgstr "名称"
#: applications/models/database_app.py:22 assets/models/cmd_filter.py:52 #: 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 #: users/templates/users/user_granted_database_app.html:35
msgid "Type" msgid "Type"
msgstr "类型" msgstr "类型"
@ -77,7 +77,7 @@ msgstr "数据库"
#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: 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 #: 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 #: 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_detail.html:115
#: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_database_app.html:38
#: users/templates/users/user_granted_remote_app.html:37 #: 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/base.py:240 assets/models/cluster.py:28
#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: 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 #: 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 #: 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/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 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30
@ -189,7 +189,7 @@ msgstr "基础"
msgid "Charset" msgid "Charset"
msgstr "编码" msgstr "编码"
#: assets/models/asset.py:148 tickets/models/ticket.py:40 #: assets/models/asset.py:148 tickets/models/ticket.py:41
msgid "Meta" msgid "Meta"
msgstr "元数据" msgstr "元数据"
@ -211,7 +211,7 @@ msgstr "IP"
#: assets/models/asset.py:187 assets/serializers/asset_user.py:45 #: assets/models/asset.py:187 assets/serializers/asset_user.py:45
#: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 #: 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/_granted_assets.html:25
#: users/templates/users/user_asset_permission.html:157 #: users/templates/users/user_asset_permission.html:157
msgid "Hostname" msgid "Hostname"
@ -339,7 +339,7 @@ msgstr ""
#: audits/models.py:99 authentication/forms.py:11 #: audits/models.py:99 authentication/forms.py:11
#: authentication/templates/authentication/login.html:21 #: authentication/templates/authentication/login.html:21
#: authentication/templates/authentication/xpack_login.html:101 #: 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/_select_user_modal.html:14
#: users/templates/users/user_detail.html:53 #: users/templates/users/user_detail.html:53
#: users/templates/users/user_list.html:15 #: users/templates/users/user_list.html:15
@ -391,7 +391,7 @@ msgstr "带宽"
msgid "Contact" msgid "Contact"
msgstr "联系人" 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 #: users/templates/users/user_detail.html:62
msgid "Phone" msgid "Phone"
msgstr "手机" msgstr "手机"
@ -417,7 +417,7 @@ msgid "Default"
msgstr "默认" msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14 #: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:635 #: users/models/user.py:655
msgid "System" msgid "System"
msgstr "系统" msgstr "系统"
@ -485,7 +485,7 @@ msgstr "每行一个命令"
#: assets/models/cmd_filter.py:56 audits/models.py:57 #: assets/models/cmd_filter.py:56 audits/models.py:57
#: authentication/templates/authentication/_access_key_modal.html:34 #: authentication/templates/authentication/_access_key_modal.html:34
#: perms/forms/asset_permission.py:20 #: 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 #: tickets/serializers/ticket.py:26
#: users/templates/users/_granted_assets.html:29 #: users/templates/users/_granted_assets.html:29
#: users/templates/users/user_asset_permission.html:44 #: 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 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49
#: templates/index.html:78 terminal/backends/command/models.py:18 #: 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:185
#: tickets/models/ticket.py:35 tickets/models/ticket.py:130 #: tickets/models/ticket.py:36 tickets/models/ticket.py:135
#: tickets/serializers/request_asset_perm.py:55 #: tickets/serializers/request_asset_perm.py:61
#: tickets/serializers/ticket.py:27 users/forms/group.py:15 #: 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/serializers/group.py:20
#: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:38
#: users/templates/users/user_asset_permission.html:64 #: 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 #: perms/models/remote_app_permission.py:16 templates/_nav.html:45
#: terminal/backends/command/models.py:20 #: 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: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/_granted_assets.html:27
#: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:42
#: users/templates/users/user_asset_permission.html:76 #: users/templates/users/user_asset_permission.html:76
@ -708,14 +708,14 @@ msgid "Backend"
msgstr "后端" msgstr "后端"
#: assets/serializers/asset_user.py:75 users/forms/profile.py:148 #: 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.html:69
#: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_profile_update.html:46
#: users/templates/users/user_pubkey_update.html:46 #: users/templates/users/user_pubkey_update.html:46
msgid "Public key" msgid "Public key"
msgstr "SSH公钥" 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" msgid "Private key"
msgstr "ssh私钥" msgstr "ssh私钥"
@ -875,7 +875,7 @@ msgstr "没有匹配到资产,结束任务"
#: users/templates/users/user_list.html:98 #: users/templates/users/user_list.html:98
#: users/templates/users/user_remote_app_permission.html:111 #: users/templates/users/user_remote_app_permission.html:111
msgid "Delete" msgid "Delete"
msgstr "删除" msgstr "删除文件"
#: audits/models.py:27 #: audits/models.py:27
msgid "Upload" msgid "Upload"
@ -920,7 +920,7 @@ msgid "Success"
msgstr "成功" msgstr "成功"
#: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 #: 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:177
#: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/change_auth_plan/models.py:308
#: xpack/plugins/gathered_user/models.py:76 #: xpack/plugins/gathered_user/models.py:76
@ -1000,8 +1000,8 @@ msgstr "Agent"
#: audits/models.py:104 #: audits/models.py:104
#: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: authentication/templates/authentication/login_otp.html:6 #: authentication/templates/authentication/login_otp.html:6
#: users/forms/profile.py:52 users/models/user.py:495 #: users/forms/profile.py:52 users/models/user.py:511
#: users/serializers/user.py:224 users/templates/users/user_detail.html:77 #: users/serializers/user.py:234 users/templates/users/user_detail.html:77
#: users/templates/users/user_profile.html:87 #: users/templates/users/user_profile.html:87
msgid "MFA" msgid "MFA"
msgstr "多因子认证" msgstr "多因子认证"
@ -1011,7 +1011,7 @@ msgstr "多因子认证"
msgid "Reason" msgid "Reason"
msgstr "原因" 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 #: tickets/serializers/ticket.py:25 xpack/plugins/cloud/models.py:211
#: xpack/plugins/cloud/models.py:269 #: xpack/plugins/cloud/models.py:269
msgid "Status" msgid "Status"
@ -1193,7 +1193,7 @@ msgstr "SSH密钥"
msgid "Reviewers" msgid "Reviewers"
msgstr "审批人" 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 #: users/templates/users/user_detail.html:250
msgid "Login confirm" msgid "Login confirm"
msgstr "登录复核" msgstr "登录复核"
@ -1228,7 +1228,7 @@ msgid "Show"
msgstr "显示" msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66 #: 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:94
#: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:163
#: users/templates/users/user_profile.html:166 #: users/templates/users/user_profile.html:166
@ -1237,7 +1237,7 @@ msgid "Disable"
msgstr "禁用" msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67 #: 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:92
#: users/templates/users/user_profile.html:170 #: users/templates/users/user_profile.html:170
msgid "Enable" msgid "Enable"
@ -1249,7 +1249,7 @@ msgstr "删除成功"
#: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_access_key_modal.html:155
#: authentication/templates/authentication/_mfa_confirm_modal.html:53 #: 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" msgid "Close"
msgstr "关闭" msgstr "关闭"
@ -1628,11 +1628,11 @@ msgstr "磁盘使用率超过 80%: {} => {}"
#: orgs/api.py:54 #: orgs/api.py:54
msgid "Organization contains undeleted resources" msgid "Organization contains undeleted resources"
msgstr "组织内包含未删除的资源" msgstr ""
#: orgs/api.py:58 #: orgs/api.py:58
msgid "The current organization cannot be deleted" msgid "The current organization cannot be deleted"
msgstr "当能删除当前所在组织" msgstr ""
#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40
#: orgs/models.py:311 #: orgs/models.py:311
@ -1647,7 +1647,7 @@ msgstr "组织管理员"
msgid "Organization auditor" msgid "Organization auditor"
msgstr "组织审计员" 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/_select_user_modal.html:15
#: users/templates/users/user_detail.html:73 #: users/templates/users/user_detail.html:73
#: users/templates/users/user_list.html:16 #: 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/asset_permission.py:86 perms/forms/database_app_permission.py:41
#: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: 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 #: 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/_select_user_modal.html:16
#: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:39
#: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_asset_permission.html:67
@ -1719,15 +1719,15 @@ msgstr "上传下载"
#: perms/models/asset_permission.py:40 #: perms/models/asset_permission.py:40
msgid "Clipboard copy" msgid "Clipboard copy"
msgstr "剪切板复制" msgstr ""
#: perms/models/asset_permission.py:41 #: perms/models/asset_permission.py:41
msgid "Clipboard paste" msgid "Clipboard paste"
msgstr "剪切板粘贴" msgstr ""
#: perms/models/asset_permission.py:42 #: perms/models/asset_permission.py:42
msgid "Clipboard copy paste" msgid "Clipboard copy paste"
msgstr "剪切板复制粘贴" msgstr ""
#: perms/models/asset_permission.py:93 #: perms/models/asset_permission.py:93
msgid "Actions" msgid "Actions"
@ -1738,8 +1738,8 @@ msgstr "动作"
msgid "Asset permission" msgid "Asset permission"
msgstr "资产授权" msgstr "资产授权"
#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:27
#: users/models/user.py:511 users/templates/users/user_detail.html:93 #: users/models/user.py:527 users/templates/users/user_detail.html:93
#: users/templates/users/user_profile.html:120 #: users/templates/users/user_profile.html:120
msgid "Date expired" msgid "Date expired"
msgstr "失效日期" msgstr "失效日期"
@ -2479,124 +2479,151 @@ msgstr "结束日期"
msgid "Args" msgid "Args"
msgstr "参数" msgstr "参数"
#: tickets/api/request_asset_perm.py:41 #: tickets/api/request_asset_perm.py:43
msgid "Ticket closed" msgid "Ticket closed"
msgstr "工单已关闭" msgstr "工单已关闭"
#: tickets/api/request_asset_perm.py:44 #: tickets/api/request_asset_perm.py:46
#, python-format #, python-format
msgid "Ticket has %s" msgid "Ticket has %s"
msgstr "工单已%s" msgstr "工单已%s"
#: tickets/api/request_asset_perm.py:69 #: tickets/api/request_asset_perm.py:93
msgid "Superuser"
msgstr "超级管理员"
#: tickets/api/request_asset_perm.py:99
msgid "Confirm assets first" msgid "Confirm assets first"
msgstr "请先确认资产" msgstr "请先确认资产"
#: tickets/api/request_asset_perm.py:102 #: tickets/api/request_asset_perm.py:96
msgid "Confirmed assets changed" msgid "Confirmed assets changed"
msgstr "确认的资产变更了" msgstr "确认的资产变更了"
#: tickets/api/request_asset_perm.py:106 #: tickets/api/request_asset_perm.py:100
msgid "Confirm system-user first" msgid "Confirm system-user first"
msgstr "请先确认系统用户" msgstr "请先确认系统用户"
#: tickets/api/request_asset_perm.py:110 #: tickets/api/request_asset_perm.py:104
msgid "Confirmed system-user changed" msgid "Confirmed system-user changed"
msgstr "确认的系统用户变更了" 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" msgid "Succeed"
msgstr "成功" 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 {}" msgid "{} request assets, approved by {}"
msgstr "{} 申请资产,通过人 {}" msgstr "{} 申请资产,通过人 {}"
#: tickets/models/ticket.py:18 tickets/models/ticket.py:72 #: tickets/models/ticket.py:19 tickets/models/ticket.py:75
msgid "Open" msgid "Open"
msgstr "开启" msgstr "开启"
#: tickets/models/ticket.py:19 #: tickets/models/ticket.py:20
msgid "Closed" msgid "Closed"
msgstr "关闭" msgstr "关闭"
#: tickets/models/ticket.py:25 #: tickets/models/ticket.py:26
msgid "General" msgid "General"
msgstr "一般" msgstr "一般"
#: tickets/models/ticket.py:27 #: tickets/models/ticket.py:28
msgid "Request asset permission" msgid "Request asset permission"
msgstr "申请资产权限" msgstr "申请资产权限"
#: tickets/models/ticket.py:32 #: tickets/models/ticket.py:33
msgid "Approve" msgid "Approve"
msgstr "同意" msgstr "同意"
#: tickets/models/ticket.py:33 #: tickets/models/ticket.py:34
msgid "Reject" msgid "Reject"
msgstr "拒绝" 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" msgid "User display name"
msgstr "用户显示名称" msgstr "用户显示名称"
#: tickets/models/ticket.py:38 #: tickets/models/ticket.py:39
msgid "Title" msgid "Title"
msgstr "标题" msgstr "标题"
#: tickets/models/ticket.py:39 tickets/models/ticket.py:132 #: tickets/models/ticket.py:40 tickets/models/ticket.py:137
msgid "Body" msgid "Body"
msgstr "内容" msgstr "内容"
#: tickets/models/ticket.py:41 #: tickets/models/ticket.py:42
msgid "Assignee" msgid "Assignee"
msgstr "处理人" msgstr "处理人"
#: tickets/models/ticket.py:42 #: tickets/models/ticket.py:43
msgid "Assignee display name" msgid "Assignee display name"
msgstr "处理人名称" msgstr "处理人名称"
#: tickets/models/ticket.py:43 #: tickets/models/ticket.py:44
msgid "Assignees" msgid "Assignees"
msgstr "待处理人" msgstr "待处理人"
#: tickets/models/ticket.py:44 #: tickets/models/ticket.py:45
msgid "Assignees display name" msgid "Assignees display name"
msgstr "待处理人名称" msgstr "待处理人名称"
#: tickets/models/ticket.py:73 #: tickets/models/ticket.py:76
msgid "{} {} this ticket" msgid "{} {} this ticket"
msgstr "{} {} 这个工单" msgstr "{} {} 这个工单"
#: tickets/models/ticket.py:84 #: tickets/models/ticket.py:87
msgid "this ticket" msgid "this ticket"
msgstr "这个工单" msgstr "这个工单"
#: tickets/serializers/request_asset_perm.py:12 #: tickets/serializers/request_asset_perm.py:19
msgid "IP group" msgid "IP group"
msgstr "IP组" msgstr "IP组"
#: tickets/serializers/request_asset_perm.py:24 #: tickets/serializers/request_asset_perm.py:31
msgid "Confirmed assets" msgid "Confirmed assets"
msgstr "确认的资产" msgstr "确认的资产"
#: tickets/serializers/request_asset_perm.py:28 #: tickets/serializers/request_asset_perm.py:34
msgid "Confirmed system user" msgid "Confirmed system user"
msgstr "确认的系统用户" msgstr "确认的系统用户"
#: tickets/serializers/request_asset_perm.py:65 #: tickets/serializers/request_asset_perm.py:83
msgid "Must be organization admin or superuser" msgid "Invalid `org_id`"
msgstr "必须是组织管理员或者超级管理员" 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}<br>\n"
" User: {username}<br>\n"
" Ip group: {ips}<br>\n"
" Hostname: {hostname}<br>\n"
" System user: {system_user}<br>\n"
" Date start: {date_start}<br>\n"
" Date expired: {date_expired}<br>\n"
" "
msgstr ""
"\n"
" 类型: {type}<br>\n"
" 用户: {username}<br>\n"
" IP 组: {ips}<br>\n"
" 主机名: {hostname}<br>\n"
" 系统用户: {system_user}<br>\n"
" 开始时间: {date_start}<br>\n"
" 过期时间: {date_expired}<br>\n"
" "
#: tickets/utils.py:20
msgid "New ticket" msgid "New ticket"
msgstr "新工单" msgstr "新工单"
#: tickets/utils.py:21 #: tickets/utils.py:28
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"\n" "\n"
@ -2621,11 +2648,11 @@ msgstr ""
" </div>\n" " </div>\n"
" " " "
#: tickets/utils.py:40 #: tickets/utils.py:47
msgid "Ticket has been reply" msgid "Ticket has been reply"
msgstr "工单已被回复" msgstr "工单已被回复"
#: tickets/utils.py:41 #: tickets/utils.py:48
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"\n" "\n"
@ -2656,7 +2683,7 @@ msgstr ""
" </div>\n" " </div>\n"
" " " "
#: users/api/user.py:126 #: users/api/user.py:147
msgid "Could not reset self otp, use profile reset instead" msgid "Could not reset self otp, use profile reset instead"
msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置"
@ -2702,7 +2729,7 @@ msgstr "确认密码"
msgid "Password does not match" msgid "Password does not match"
msgstr "密码不一致" 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_detail.html:57
#: users/templates/users/user_profile.html:59 #: users/templates/users/user_profile.html:59
msgid "Email" msgid "Email"
@ -2738,12 +2765,12 @@ msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同" msgstr "不能和原来的密钥相同"
#: users/forms/profile.py:137 users/forms/user.py:90 #: users/forms/profile.py:137 users/forms/user.py:90
#: users/serializers/user.py:185 users/serializers/user.py:266 #: users/serializers/user.py:194 users/serializers/user.py:276
#: users/serializers/user.py:324 #: users/serializers/user.py:334
msgid "Not a valid ssh public key" msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法" 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_detail.html:89
#: users/templates/users/user_list.html:18 #: users/templates/users/user_list.html:18
#: users/templates/users/user_profile.html:102 #: users/templates/users/user_profile.html:102
@ -2763,15 +2790,15 @@ msgstr "添加到用户组"
msgid "* Your password does not meet the requirements" msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求" 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" msgid "Reset link will be generated and sent to the user"
msgstr "生成重置密码链接,通过邮件发送给用户" msgstr "生成重置密码链接,通过邮件发送给用户"
#: users/forms/user.py:125 users/serializers/user.py:32 #: users/forms/user.py:125 users/serializers/user.py:37
msgid "Set password" msgid "Set password"
msgstr "设置密码" 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/models.py:61
#: xpack/plugins/change_auth_plan/serializers.py:30 #: xpack/plugins/change_auth_plan/serializers.py:30
msgid "Password strategy" msgid "Password strategy"
@ -2789,79 +2816,79 @@ msgstr "超级审计员"
msgid "Application" msgid "Application"
msgstr "应用程序" 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" msgid "Force enable"
msgstr "强制启用" msgstr "强制启用"
#: users/models/user.py:462 #: users/models/user.py:478
msgid "Local" msgid "Local"
msgstr "数据库" msgstr "数据库"
#: users/models/user.py:486 #: users/models/user.py:502
msgid "Avatar" msgid "Avatar"
msgstr "头像" 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" msgid "Wechat"
msgstr "微信" msgstr "微信"
#: users/models/user.py:522 #: users/models/user.py:538
msgid "Date password last updated" msgid "Date password last updated"
msgstr "最后更新密码日期" msgstr "最后更新密码日期"
#: users/models/user.py:631 #: users/models/user.py:651
msgid "Administrator" msgid "Administrator"
msgstr "管理员" msgstr "管理员"
#: users/models/user.py:634 #: users/models/user.py:654
msgid "Administrator is the super user of system" msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员" msgstr "Administrator是初始的超级管理员"
#: users/serializers/user.py:72 users/serializers/user.py:237 #: users/serializers/user.py:54 users/serializers/user.py:90
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/user.py:73
msgid "Is valid"
msgstr "账户是否有效"
#: users/serializers/user.py:74
msgid "Is expired"
msgstr " 是否过期"
#: users/serializers/user.py:75
msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/user.py:79
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/user.py:80
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/user.py:81
msgid "Organization role name" msgid "Organization role name"
msgstr "组织角色名称" msgstr "组织角色名称"
#: users/serializers/user.py:81 users/serializers/user.py:247
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/user.py:82 #: users/serializers/user.py:82
msgid "Is valid"
msgstr "账户是否有效"
#: users/serializers/user.py:83
msgid "Is expired"
msgstr " 是否过期"
#: users/serializers/user.py:84
msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/user.py:88
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/user.py:89
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/user.py:91
msgid "Super role name" msgid "Super role name"
msgstr "超级角色名称" msgstr "超级角色名称"
#: users/serializers/user.py:105 #: users/serializers/user.py:114
msgid "Role limit to {}" msgid "Role limit to {}"
msgstr "角色只能为 {}" 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" msgid "Password does not match security rules"
msgstr "密码不满足安全规则" msgstr "密码不满足安全规则"
#: users/serializers/user.py:282 #: users/serializers/user.py:292
msgid "The old password is incorrect" msgid "The old password is incorrect"
msgstr "旧密码错误" msgstr "旧密码错误"
#: users/serializers/user.py:296 #: users/serializers/user.py:306
msgid "The newly set password is inconsistent" msgid "The newly set password is inconsistent"
msgstr "两次密码不一致" msgstr "两次密码不一致"
@ -3992,18 +4019,15 @@ msgstr "旗舰版"
#~ msgid "Role name" #~ msgid "Role name"
#~ msgstr "角色名" #~ msgstr "角色名"
#~ msgid "GUI copy"
#~ msgstr "GUI 复制"
#~ msgid "GUI paste"
#~ msgstr "GUI 粘贴"
#~ msgid "Covered always" #~ msgid "Covered always"
#~ msgstr "总是被覆盖" #~ msgstr "总是被覆盖"
#~ msgid "Account name" #~ msgid "Account name"
#~ msgstr "账户名称" #~ msgstr "账户名称"
#~ msgid "Superuser"
#~ msgstr "超级管理员"
#~ msgid "Auditors cannot be join in the user group" #~ msgid "Auditors cannot be join in the user group"
#~ msgstr "审计员不能被加入到用户组" #~ msgstr "审计员不能被加入到用户组"

View File

@ -62,7 +62,6 @@ class OrgModelMixin(models.Model):
org = get_current_org() org = get_current_org()
if org is None: if org is None:
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
if org.is_real() or org.is_system(): if org.is_real() or org.is_system():
self.org_id = org.id self.org_id = org.id
elif org.is_default(): elif org.is_default():

View File

@ -1,22 +1,23 @@
from collections import namedtuple
from django.db.transaction import atomic 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 django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response 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 users.models.user import User
from common.const.http import POST, GET from common.const.http import POST, GET
from common.drf.api import JMSModelViewSet from common.drf.api import JMSModelViewSet
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils.django import get_object_or_none from common.utils.django import get_object_or_none
from common.utils.timezone import dt_parser
from common.drf.serializers import EmptySerializer from common.drf.serializers import EmptySerializer
from perms.models.asset_permission import AssetPermission, Asset from perms.models.asset_permission import AssetPermission, Asset
from assets.models.user import SystemUser from assets.models.user import SystemUser
from ..exceptions import ( from ..exceptions import (
ConfirmedAssetsChanged, ConfirmedSystemUserChanged, ConfirmedAssetsChanged, ConfirmedSystemUserChanged,
TicketClosed, TicketActionYet, NotHaveConfirmedAssets, TicketClosed, TicketActionAlready, NotHaveConfirmedAssets,
NotHaveConfirmedSystemUser NotHaveConfirmedSystemUser
) )
from .. import serializers from .. import serializers
@ -25,15 +26,15 @@ from ..permissions import IsAssignee
class RequestAssetPermTicketViewSet(JMSModelViewSet): 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 = { serializer_classes = {
'default': serializers.RequestAssetPermTicketSerializer, 'default': serializers.RequestAssetPermTicketSerializer,
'approve': EmptySerializer, 'approve': EmptySerializer,
'reject': EmptySerializer, 'reject': EmptySerializer,
'assignees': serializers.OrgAssigneeSerializer, 'assignees': serializers.AssigneeSerializer,
} }
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action', 'user_display'] filter_fields = ['status', 'title', 'action', 'user_display', 'org_id']
search_fields = ['user_display', 'title'] search_fields = ['user_display', 'title']
def _check_can_set_action(self, instance, action): def _check_can_set_action(self, instance, action):
@ -41,49 +42,39 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
raise TicketClosed(detail=_('Ticket closed')) raise TicketClosed(detail=_('Ticket closed'))
if instance.action == action: if instance.action == action:
action_display = dict(instance.ACTION_CHOICES).get(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]) @action(detail=False, methods=[GET], permission_classes=[IsValidUser])
def assignees(self, request, *args, **kwargs): def assignees(self, request: Request, *args, **kwargs):
org_mapper = {}
UserTuple = namedtuple('UserTuple', ('id', 'name', 'username'))
user = request.user user = request.user
superusers = User.objects.filter(role=User.ROLE.ADMIN) org_id = request.query_params.get('org_id', Organization.DEFAULT_ID)
admins_with_org = User.objects.filter(related_admin_orgs__users=user).annotate( q = Q(role=User.ROLE.ADMIN)
org_id=F('related_admin_orgs__id'), org_name=F('related_admin_orgs__name') 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()
for user in admins_with_org: return self.get_paginated_response_with_query_set(org_admins)
org_id = user.org_id
if org_id not in org_mapper: def _get_extra_comment(self, instance):
org_mapper[org_id] = { meta = instance.meta
'org_name': user.org_name, ips = ', '.join(meta.get('ips', []))
'org_admins': set() # 去重 confirmed_assets = ', '.join(meta.get('confirmed_assets', []))
}
org_mapper[org_id]['org_admins'].add(UserTuple(user.id, user.name, user.username))
result = [ return f'''
{ {_('IP group')}: {ips}
'org_name': _('Superuser'), {_('Hostname')}: {meta.get('hostname', '')}
'org_admins': set(UserTuple(user.id, user.name, user.username) {_('System user')}: {meta.get('system_user', '')}
for user in superusers) {_('Confirmed assets')}: {confirmed_assets}
} {_('Confirmed system user')}: {meta.get('confirmed_system_user', '')}
] '''
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)
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
def reject(self, request, *args, **kwargs): def reject(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
action = instance.ACTION_REJECT action = instance.ACTION_REJECT
self._check_can_set_action(instance, action) 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() return Response()
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
@ -109,29 +100,33 @@ class RequestAssetPermTicketViewSet(JMSModelViewSet):
if system_user is None: if system_user is None:
raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed')) 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')}) 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 meta = instance.meta
request = self.request request = self.request
ap_kwargs = { ap_kwargs = {
'name': meta.get('name', ''), 'name': _('From request ticket: {} {}').format(instance.user_display, instance.id),
'created_by': self.request.user.username, 'created_by': self.request.user.username,
'comment': _('{} request assets, approved by {}').format(instance.user_display, 'comment': _('{} request assets, approved by {}').format(instance.user_display,
instance.assignee_display) instance.assignees_display)
} }
date_start = meta.get('date_start') date_start = dt_parser(meta.get('date_start'))
date_expired = meta.get('date_expired') date_expired = dt_parser(meta.get('date_expired'))
if date_start: if date_start:
ap_kwargs['date_start'] = date_start ap_kwargs['date_start'] = date_start
if date_expired: if date_expired:
ap_kwargs['date_expired'] = date_expired ap_kwargs['date_expired'] = date_expired
with atomic(): 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 = AssetPermission.objects.create(**ap_kwargs)
ap.system_users.add(system_user) ap.system_users.add(system_user)
ap.assets.add(*assets) ap.assets.add(*assets)
ap.users.add(user)
return ap return ap

View File

@ -11,7 +11,7 @@ from .. import serializers, models, mixins
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet): class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketSerializer serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all() queryset = models.Ticket.origin_objects.all()
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action', 'user_display'] filter_fields = ['status', 'title', 'action', 'user_display']
search_fields = ['user_display', 'title'] search_fields = ['user_display', 'title']

View File

@ -21,5 +21,9 @@ class TicketClosed(JMSException):
pass pass
class TicketActionYet(JMSException): class TicketActionAlready(JMSException):
pass
class OrgIdRequiredException(JMSException):
pass pass

View File

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

View File

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

View File

@ -6,11 +6,12 @@ from .models import Ticket
class TicketMixin: class TicketMixin:
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset()
assign = self.request.GET.get('assign', None) assign = self.request.GET.get('assign', None)
if assign is 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']: elif assign in ['1']:
queryset = Ticket.get_assigned_tickets(self.request.user) queryset = Ticket.get_assigned_tickets(self.request.user, queryset)
else: else:
queryset = Ticket.get_my_tickets(self.request.user) queryset = Ticket.get_my_tickets(self.request.user, queryset)
return queryset return queryset

View File

@ -7,11 +7,12 @@ from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField from common.fields.model import JsonDictTextField
from orgs.mixins.models import OrgModelMixin
__all__ = ['Ticket', 'Comment'] __all__ = ['Ticket', 'Comment']
class Ticket(CommonModelMixin): class Ticket(OrgModelMixin, CommonModelMixin):
STATUS_OPEN = 'open' STATUS_OPEN = 'open'
STATUS_CLOSED = 'closed' STATUS_CLOSED = 'closed'
STATUS_CHOICES = ( STATUS_CHOICES = (
@ -46,6 +47,8 @@ class Ticket(CommonModelMixin):
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open') status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True) action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
origin_objects = models.Manager()
def __str__(self): def __str__(self):
return '{}: {}'.format(self.user_display, self.title) return '{}: {}'.format(self.user_display, self.title)
@ -79,13 +82,15 @@ class Ticket(CommonModelMixin):
self.status = status self.status = status
self.save() 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) action_display = dict(self.ACTION_CHOICES).get(action)
body = '{} {} {}'.format(user, action_display, _("this ticket")) 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)) self.comments.create(body=body, user=user, user_display=str(user))
def perform_action(self, action, user): def perform_action(self, action, user, extra_comment=None):
self.create_action_comment(action, user) self.create_action_comment(action, user, extra_comment)
self.action = action self.action = action
self.status = self.STATUS_CLOSED self.status = self.STATUS_CLOSED
self.assignee = user self.assignee = user

View File

@ -1,8 +1,15 @@
from itertools import chain
from rest_framework import serializers from rest_framework import serializers
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse from django.urls import reverse
from django.db.models import Q 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 users.models.user import User
from ..models import Ticket from ..models import Ticket
@ -22,9 +29,8 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
source='meta.confirmed_assets', source='meta.confirmed_assets',
default=list, required=False, default=list, required=False,
label=_('Confirmed assets')) label=_('Confirmed assets'))
confirmed_system_user = serializers.ListField(child=serializers.UUIDField(), confirmed_system_user = serializers.UUIDField(source='meta.confirmed_system_user',
source='meta.confirmed_system_user', default='', required=False,
default=list, required=False,
label=_('Confirmed system user')) label=_('Confirmed system user'))
assets_waitlist_url = serializers.SerializerMethodField() assets_waitlist_url = serializers.SerializerMethodField()
system_user_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', 'status', 'action', 'date_created', 'date_updated', 'system_user_waitlist_url',
'type', 'type_display', 'action_display', 'ips', 'confirmed_assets', 'type', 'type_display', 'action_display', 'ips', 'confirmed_assets',
'date_start', 'date_expired', 'confirmed_system_user', 'hostname', 'date_start', 'date_expired', 'confirmed_system_user', 'hostname',
'assets_waitlist_url', 'system_user' 'assets_waitlist_url', 'system_user', 'org_id'
] ]
m2m_fields = [ m2m_fields = [
'user', 'user_display', 'assignees', 'assignees_display', 'user', 'user_display', 'assignees', 'assignees_display',
@ -52,26 +58,44 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
extra_kwargs = { extra_kwargs = {
'status': {'label': _('Status')}, 'status': {'label': _('Status')},
'action': {'label': _('Action')}, '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 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( q = Q(role=User.ROLE.ADMIN)
id__in=[assignee.id for assignee in assignees]).distinct().count() 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): if count != len(assignees):
raise serializers.ValidationError(_('Must be organization admin or superuser')) raise serializers.ValidationError(_('Field `assignees` must be organization admin or superuser'))
return assignees return attrs
def get_system_user_waitlist_url(self, instance: Ticket): def get_system_user_waitlist_url(self, instance: Ticket):
if not self._is_assignee(instance): if not self._is_assignee(instance):
return None return None
meta = instance.meta return reverse('api-assets:system-user-list')
url = reverse('api-assets:system-user-list')
query = meta.get('system_user', '')
return '{}?search={}'.format(url, query)
def get_assets_waitlist_url(self, instance: Ticket): def get_assets_waitlist_url(self, instance: Ticket):
if not self._is_assignee(instance): if not self._is_assignee(instance):
@ -81,37 +105,106 @@ class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
query = '' query = ''
meta = instance.meta meta = instance.meta
ips = meta.get('ips', [])
hostname = meta.get('hostname') hostname = meta.get('hostname')
if hostname:
if ips:
query = '?ips=%s' % ','.join(ips)
elif hostname:
query = '?search=%s' % hostname query = '?search=%s' % hostname
return asset_api + query 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}<br>
User: {username}<br>
Ip group: {ips}<br>
Hostname: {hostname}<br>
System user: {system_user}<br>
Date start: {date_start}<br>
Date expired: {date_expired}<br>
''').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): def create(self, validated_data):
# `type` 与 `user` 用户不可提交,
validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM
validated_data['user'] = self.context['request'].user validated_data['user'] = self.context['request'].user
# `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉
self._pop_confirmed_fields() self._pop_confirmed_fields()
self._create_body(validated_data)
return super().create(validated_data) return super().create(validated_data)
def save(self, **kwargs): def save(self, **kwargs):
"""
做了一些数据转换
"""
meta = self.validated_data.get('meta', {}) 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') date_start = meta.get('date_start')
if 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') date_expired = meta.get('date_expired')
if date_expired: if date_expired:
meta['date_expired'] = date_expired.strftime('%Y-%m-%d %H:%M:%S%z') meta['date_expired'] = dt_formater(date_expired)
return super().save(**kwargs)
# 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): def update(self, instance, validated_data):
new_meta = validated_data['meta'] new_meta = validated_data['meta']
if not self._is_assignee(instance): if not self._is_assignee(instance):
self._pop_confirmed_fields() self._pop_confirmed_fields()
# Json 字段保存的坑😭
old_meta = instance.meta old_meta = instance.meta
meta = {} meta = {}
meta.update(old_meta) meta.update(old_meta)
@ -134,8 +227,3 @@ class AssigneeSerializer(serializers.Serializer):
id = serializers.UUIDField() id = serializers.UUIDField()
name = serializers.CharField() name = serializers.CharField()
username = serializers.CharField() username = serializers.CharField()
class OrgAssigneeSerializer(serializers.Serializer):
org_name = serializers.CharField()
org_admins = AssigneeSerializer(many=True)

View File

@ -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})

View File

@ -7,7 +7,7 @@ from .. import api
app_name = 'tickets' app_name = 'tickets'
router = BulkRouter() 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', api.TicketViewSet, 'ticket')
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')

View File

@ -1,23 +1,30 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ 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 from common.tasks import send_mail_async
logger = get_logger(__name__) 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] recipient_list = [user.email for user in assignees]
user = ticket.user user = ticket.user
if not recipient_list: if not recipient_list:
logger.error("Ticket not has assignees: {}".format(ticket.id)) logger.error("Ticket not has assignees: {}".format(ticket.id))
return return
subject = '{}: {}'.format(_("New ticket"), ticket.title) 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 = _(""" message = _("""
<div> <div>
<p>Your has a new ticket</p> <p>Your has a new ticket</p>

View File

@ -233,6 +233,11 @@ class RoleMixin:
def is_app(self): def is_app(self):
return self.role == self.ROLE.APP 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 @lazyproperty
def user_orgs(self): def user_orgs(self):
from orgs.models import Organization from orgs.models import Organization

View File

@ -27,6 +27,11 @@ class UserOrgSerializer(serializers.Serializer):
name = serializers.CharField() name = serializers.CharField()
class UserOrgLabelSerializer(serializers.Serializer):
value = serializers.CharField(source='id')
label = serializers.CharField(source='name')
class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
CUSTOM_PASSWORD = _('Set password') CUSTOM_PASSWORD = _('Set password')
@ -214,6 +219,7 @@ class UserRoleSerializer(serializers.Serializer):
class UserProfileSerializer(UserSerializer): class UserProfileSerializer(UserSerializer):
admin_or_audit_orgs = UserOrgSerializer(many=True, read_only=True) 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) current_org_roles = serializers.ListField(read_only=True)
public_key_comment = serializers.CharField( public_key_comment = serializers.CharField(
source='get_public_key_comment', required=False, read_only=True, max_length=128 source='get_public_key_comment', required=False, read_only=True, max_length=128
@ -231,7 +237,7 @@ class UserProfileSerializer(UserSerializer):
class Meta(UserSerializer.Meta): class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [ fields = UserSerializer.Meta.fields + [
'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles', '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 = dict(UserSerializer.Meta.extra_kwargs)
extra_kwargs.update({ extra_kwargs.update({