From 39a75074af81b1ef8b700ffd5b797f90f0e3eb33 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 6 Jul 2020 11:14:20 +0800 Subject: [PATCH] =?UTF-8?q?[Feature]=20=E6=B7=BB=E5=8A=A0=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E8=B5=84=E4=BA=A7=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/asset.py | 6 +- apps/assets/filters.py | 22 +- apps/common/db/__init__.py | 0 apps/common/db/aggregates.py | 28 ++ apps/common/drf/api.py | 11 + apps/common/drf/serializers.py | 5 + apps/common/exceptions.py | 4 + apps/common/mixins/api.py | 41 ++- apps/locale/zh/LC_MESSAGES/django.mo | Bin 54242 -> 55113 bytes apps/locale/zh/LC_MESSAGES/django.po | 314 ++++++++++-------- apps/tickets/api/__init__.py | 1 + apps/tickets/api/request_asset_perm.py | 97 ++++++ apps/tickets/exceptions.py | 25 ++ apps/tickets/models/ticket.py | 4 +- apps/tickets/permissions.py | 3 + apps/tickets/serializers/__init__.py | 1 + .../tickets/serializers/request_asset_perm.py | 120 +++++++ apps/tickets/urls/api_urls.py | 1 + apps/users/api/user.py | 2 + apps/users/filters.py | 31 ++ 20 files changed, 568 insertions(+), 148 deletions(-) create mode 100644 apps/common/db/__init__.py create mode 100644 apps/common/db/aggregates.py create mode 100644 apps/common/drf/api.py create mode 100644 apps/common/drf/serializers.py create mode 100644 apps/tickets/api/request_asset_perm.py create mode 100644 apps/tickets/exceptions.py create mode 100644 apps/tickets/serializers/request_asset_perm.py create mode 100644 apps/users/filters.py diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index dee95ed06..7877c5b90 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -14,7 +14,7 @@ from .. import serializers from ..tasks import ( update_asset_hardware_info_manual, test_asset_connectivity_manual ) -from ..filters import AssetByNodeFilterBackend, LabelFilterBackend +from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend logger = get_logger(__file__) @@ -32,7 +32,7 @@ class AssetViewSet(OrgBulkModelViewSet): model = Asset filter_fields = ( "hostname", "ip", "systemuser__id", "admin_user__id", "platform__base", - "is_active" + "is_active", 'ip' ) search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") @@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet): 'display': serializers.AssetDisplaySerializer, } permission_classes = (IsOrgAdminOrAppUser,) - extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] + extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] def set_assets_node(self, assets): if not isinstance(assets, list): diff --git a/apps/assets/filters.py b/apps/assets/filters.py index 13d8f9e60..149ed12a8 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -import coreapi +from rest_framework.compat import coreapi, coreschema from rest_framework import filters from django.db.models import Q @@ -117,3 +117,23 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend): def perform_query(pattern, queryset): return queryset.filter(asset__nodes__key__regex=pattern).distinct() + +class IpInFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + ips = request.query_params.get('ips') + if not ips: + return queryset + ip_list = [i.strip() for i in ips.split(',')] + queryset = queryset.filter(ip__in=ip_list) + return queryset + + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='ips', location='query', required=False, type='string', + schema=coreschema.String( + title='ips', + description='ip in filter' + ) + ) + ] diff --git a/apps/common/db/__init__.py b/apps/common/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/db/aggregates.py b/apps/common/db/aggregates.py new file mode 100644 index 000000000..081c1fea8 --- /dev/null +++ b/apps/common/db/aggregates.py @@ -0,0 +1,28 @@ +from django.db.models import Aggregate + + +class GroupConcat(Aggregate): + function = 'GROUP_CONCAT' + template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))' + allow_distinct = False + + def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra): + order_by_clause = '' + if order_by is not None: + order = 'ASC' + prefix, body = order_by[1], order_by[1:] + if prefix == '-': + order = 'DESC' + elif prefix == '+': + pass + else: + body = order_by + order_by_clause = f'ORDER BY {body} {order}' + + super().__init__( + expression, + distinct='DISTINCT' if distinct else '', + order_by=order_by_clause, + separator=f'SEPARATOR {separator}', + **extra + ) diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py new file mode 100644 index 000000000..523689e72 --- /dev/null +++ b/apps/common/drf/api.py @@ -0,0 +1,11 @@ +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin + + +class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet): + pass + + +class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet): + pass diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py new file mode 100644 index 000000000..bd92415a1 --- /dev/null +++ b/apps/common/drf/serializers.py @@ -0,0 +1,5 @@ +from rest_framework.serializers import Serializer + + +class EmptySerializer(Serializer): + pass diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 3d98261b1..e95cc2801 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -1,3 +1,7 @@ # -*- coding: utf-8 -*- # +from rest_framework.exceptions import APIException + +class JMSException(APIException): + pass diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 0b7b5aed6..a58e8b079 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -4,6 +4,7 @@ import time from hashlib import md5 from threading import Thread from collections import defaultdict +from itertools import chain from django.db.models.signals import m2m_changed from django.core.cache import cache @@ -15,8 +16,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter from ..utils import lazyproperty __all__ = [ - "JSONResponseMixin", "CommonApiMixin", - 'AsyncApiMixin', 'RelationMixin' + 'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin', + 'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin' ] @@ -54,9 +55,10 @@ class ExtraFilterFieldsMixin: def get_filter_backends(self): if self.filter_backends != self.__class__.filter_backends: return self.filter_backends - backends = list(self.filter_backends) + \ - list(self.default_added_filters) + \ - list(self.extra_filter_backends) + backends = list(chain( + self.filter_backends, + self.default_added_filters, + self.extra_filter_backends)) return backends def filter_queryset(self, queryset): @@ -233,3 +235,32 @@ class RelationMixin: def perform_create(self, serializer): instance = serializer.save() self.send_post_add_signal(instance) + + +class SerializerMixin2: + serializer_classes = {} + + def get_serializer_class(self): + if self.serializer_classes: + serializer_class = self.serializer_classes.get( + self.action, self.serializer_classes.get('default') + ) + + if isinstance(serializer_class, dict): + serializer_class = serializer_class.get( + self.request.method.lower, serializer_class.get('default') + ) + + assert serializer_class, '`serializer_classes` config error' + return serializer_class + return super().get_serializer_class() + + +class QuerySetMixin: + def get_queryset(self): + queryset = super().get_queryset() + serializer_class = self.get_serializer_class() + if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): + queryset = serializer_class.setup_eager_loading(queryset) + + return queryset diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index b6b9f74fc3b3d15ae732dfa0281da50380a9962b..234bb9ca1d9b76f03f2d336555ef98e80076c1d0 100644 GIT binary patch delta 17212 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 b2PfX!vxoh3Mvmg2LGcF{+}pk0J<<6e^Xm-Y delta 16356 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 10985029e..5bff2be14 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-07 14:11+0800\n" +"POT-Creation-Date: 2020-07-08 15:05+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -47,7 +47,7 @@ msgid "Name" msgstr "名称" #: applications/models/database_app.py:22 assets/models/cmd_filter.py:51 -#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:43 +#: terminal/models.py:376 terminal/models.py:413 tickets/models/ticket.py:45 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -84,8 +84,8 @@ msgstr "数据库" #: users/templates/users/user_group_detail.html:62 #: users/templates/users/user_group_list.html:16 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:76 xpack/plugins/cloud/models.py:53 -#: xpack/plugins/cloud/models.py:140 xpack/plugins/gathered_user/models.py:26 +#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:53 +#: xpack/plugins/cloud/models.py:139 xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "备注" @@ -110,8 +110,8 @@ msgstr "数据库应用" #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 -#: xpack/plugins/change_auth_plan/models.py:282 -#: xpack/plugins/cloud/models.py:266 +#: xpack/plugins/change_auth_plan/models.py:283 +#: xpack/plugins/cloud/models.py:269 msgid "Asset" msgstr "资产" @@ -134,8 +134,8 @@ msgstr "参数" #: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:16 #: perms/models/base.py:54 users/models/user.py:508 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:80 xpack/plugins/cloud/models.py:56 -#: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 +#: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 +#: xpack/plugins/cloud/models.py:145 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -148,7 +148,7 @@ msgstr "创建者" #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 #: orgs/models.py:17 perms/models/base.py:55 users/models/group.py:18 #: users/templates/users/user_group_detail.html:58 -#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149 +#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:148 msgid "Date created" msgstr "创建日期" @@ -189,7 +189,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:148 tickets/models/ticket.py:38 +#: assets/models/asset.py:148 tickets/models/ticket.py:40 msgid "Meta" msgstr "元数据" @@ -211,6 +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:13 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -233,7 +234,7 @@ msgstr "网域" #: assets/models/asset.py:195 assets/models/user.py:109 #: perms/models/asset_permission.py:81 -#: xpack/plugins/change_auth_plan/models.py:55 +#: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "节点" @@ -246,7 +247,7 @@ msgstr "激活" #: assets/models/asset.py:199 assets/models/cluster.py:19 #: assets/models/user.py:65 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:133 xpack/plugins/cloud/serializers.py:82 +#: xpack/plugins/cloud/models.py:133 msgid "Admin user" msgstr "管理用户" @@ -343,8 +344,8 @@ msgstr "" #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 #: users/templates/users/user_profile.html:47 -#: xpack/plugins/change_auth_plan/models.py:46 -#: xpack/plugins/change_auth_plan/models.py:278 +#: xpack/plugins/change_auth_plan/models.py:47 +#: xpack/plugins/change_auth_plan/models.py:279 msgid "Username" msgstr "用户名" @@ -359,21 +360,21 @@ msgstr "用户名" #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:67 -#: xpack/plugins/change_auth_plan/models.py:190 -#: xpack/plugins/change_auth_plan/models.py:285 +#: xpack/plugins/change_auth_plan/models.py:68 +#: xpack/plugins/change_auth_plan/models.py:191 +#: xpack/plugins/change_auth_plan/models.py:286 msgid "Password" msgstr "密码" -#: assets/models/base.py:235 xpack/plugins/change_auth_plan/models.py:71 -#: xpack/plugins/change_auth_plan/models.py:197 -#: xpack/plugins/change_auth_plan/models.py:292 +#: assets/models/base.py:235 xpack/plugins/change_auth_plan/models.py:72 +#: xpack/plugins/change_auth_plan/models.py:198 +#: xpack/plugins/change_auth_plan/models.py:293 msgid "SSH private key" msgstr "SSH密钥" -#: assets/models/base.py:236 xpack/plugins/change_auth_plan/models.py:74 -#: xpack/plugins/change_auth_plan/models.py:193 -#: xpack/plugins/change_auth_plan/models.py:288 +#: assets/models/base.py:236 xpack/plugins/change_auth_plan/models.py:75 +#: xpack/plugins/change_auth_plan/models.py:194 +#: xpack/plugins/change_auth_plan/models.py:289 msgid "SSH public key" msgstr "SSH公钥" @@ -483,7 +484,9 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:55 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 -#: perms/forms/asset_permission.py:20 tickets/serializers/ticket.py:26 +#: perms/forms/asset_permission.py:20 +#: tickets/serializers/request_asset_perm.py:51 +#: tickets/serializers/ticket.py:26 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 @@ -536,7 +539,8 @@ 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:33 tickets/models/ticket.py:128 +#: tickets/models/ticket.py:35 tickets/models/ticket.py:130 +#: tickets/serializers/request_asset_perm.py:52 #: tickets/serializers/ticket.py:27 users/forms/group.py:15 #: users/models/user.py:160 users/models/user.py:176 users/models/user.py:615 #: users/serializers/group.py:20 @@ -586,7 +590,7 @@ msgstr "键" #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/cloud/models.py:129 xpack/plugins/cloud/serializers.py:83 +#: xpack/plugins/cloud/models.py:129 msgid "Node" msgstr "节点" @@ -603,7 +607,7 @@ msgid "Username same with user" msgstr "用户名与用户相同" #: assets/models/user.py:110 templates/_nav.html:39 -#: xpack/plugins/change_auth_plan/models.py:51 +#: xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产管理" @@ -799,7 +803,7 @@ msgid "Gather assets users" msgstr "收集资产上的用户" #: assets/tasks/push_system_user.py:148 -#: assets/tasks/system_user_connectivity.py:86 +#: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" @@ -808,7 +812,7 @@ msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" #: assets/tasks/push_system_user.py:180 -#: assets/tasks/system_user_connectivity.py:78 +#: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" @@ -820,19 +824,19 @@ msgstr "推送系统用户到入资产: {}" msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" -#: assets/tasks/system_user_connectivity.py:77 +#: assets/tasks/system_user_connectivity.py:80 msgid "Start test system user connectivity for platform: [{}]" msgstr "开始测试系统用户在该系统平台的可连接性: [{}]" -#: assets/tasks/system_user_connectivity.py:97 +#: assets/tasks/system_user_connectivity.py:100 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks/system_user_connectivity.py:105 +#: assets/tasks/system_user_connectivity.py:108 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks/system_user_connectivity.py:118 +#: assets/tasks/system_user_connectivity.py:121 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" @@ -914,8 +918,9 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models.py:199 xpack/plugins/change_auth_plan/models.py:176 -#: xpack/plugins/change_auth_plan/models.py:307 +#: terminal/models.py:199 tickets/serializers/request_asset_perm.py:15 +#: xpack/plugins/change_auth_plan/models.py:177 +#: xpack/plugins/change_auth_plan/models.py:308 #: xpack/plugins/gathered_user/models.py:76 msgid "Date start" msgstr "开始日期" @@ -970,7 +975,7 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:96 xpack/plugins/cloud/models.py:201 +#: audits/models.py:96 xpack/plugins/cloud/models.py:204 msgid "Failed" msgstr "失败" @@ -999,13 +1004,14 @@ msgstr "Agent" msgid "MFA" msgstr "多因子认证" -#: audits/models.py:105 xpack/plugins/change_auth_plan/models.py:303 -#: xpack/plugins/cloud/models.py:214 +#: audits/models.py:105 xpack/plugins/change_auth_plan/models.py:304 +#: xpack/plugins/cloud/models.py:217 msgid "Reason" msgstr "原因" -#: audits/models.py:106 tickets/serializers/ticket.py:25 -#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:269 +#: audits/models.py:106 tickets/serializers/request_asset_perm.py:50 +#: tickets/serializers/ticket.py:25 xpack/plugins/cloud/models.py:214 +#: xpack/plugins/cloud/models.py:272 msgid "Status" msgstr "状态" @@ -1018,7 +1024,7 @@ msgid "Is success" msgstr "是否成功" #: audits/serializers.py:73 ops/models/command.py:24 -#: xpack/plugins/cloud/models.py:209 +#: xpack/plugins/cloud/models.py:212 msgid "Result" msgstr "结果" @@ -1182,7 +1188,7 @@ msgstr "SSH密钥" msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:53 tickets/models/ticket.py:25 +#: authentication/models.py:53 tickets/models/ticket.py:26 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" @@ -1238,7 +1244,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:68 +#: templates/_modal.html:22 tickets/models/ticket.py:70 msgid "Close" msgstr "关闭" @@ -1556,8 +1562,8 @@ msgstr "开始时间" msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:242 xpack/plugins/change_auth_plan/models.py:179 -#: xpack/plugins/change_auth_plan/models.py:310 +#: ops/models/adhoc.py:242 xpack/plugins/change_auth_plan/models.py:180 +#: xpack/plugins/change_auth_plan/models.py:311 #: xpack/plugins/gathered_user/models.py:79 msgid "Time" msgstr "时间" @@ -1693,8 +1699,8 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/base.py:53 users/models/user.py:505 -#: users/templates/users/user_detail.html:93 +#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:17 +#: users/models/user.py:505 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2426,7 +2432,40 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: tickets/models/ticket.py:18 tickets/models/ticket.py:70 +#: tickets/api/request_asset_perm.py:36 +msgid "Ticket closed" +msgstr "工单已关闭" + +#: tickets/api/request_asset_perm.py:39 +#, python-format +msgid "Ticket has %s" +msgstr "工单已%s" + +#: tickets/api/request_asset_perm.py:59 +msgid "Confirm assets first" +msgstr "请先确认资产" + +#: tickets/api/request_asset_perm.py:62 +msgid "Confirmed assets changed" +msgstr "确认的资产变更了" + +#: tickets/api/request_asset_perm.py:66 +msgid "Confirm system-user first" +msgstr "请先确认系统用户" + +#: tickets/api/request_asset_perm.py:70 +msgid "Confirmed system-user changed" +msgstr "确认的系统用户变更了" + +#: tickets/api/request_asset_perm.py:73 xpack/plugins/cloud/models.py:205 +msgid "Succeed" +msgstr "成功" + +#: tickets/api/request_asset_perm.py:81 +msgid "{} request assets, approved by {}" +msgstr "{} 申请资产,通过人 {}" + +#: tickets/models/ticket.py:18 tickets/models/ticket.py:72 msgid "Open" msgstr "开启" @@ -2434,54 +2473,74 @@ msgstr "开启" msgid "Closed" msgstr "关闭" -#: tickets/models/ticket.py:24 +#: tickets/models/ticket.py:25 msgid "General" msgstr "一般" -#: tickets/models/ticket.py:30 +#: tickets/models/ticket.py:27 +msgid "Request asset permission" +msgstr "申请资产权限" + +#: tickets/models/ticket.py:32 msgid "Approve" msgstr "同意" -#: tickets/models/ticket.py:31 +#: tickets/models/ticket.py:33 msgid "Reject" msgstr "拒绝" -#: tickets/models/ticket.py:34 tickets/models/ticket.py:129 +#: tickets/models/ticket.py:36 tickets/models/ticket.py:131 msgid "User display name" msgstr "用户显示名称" -#: tickets/models/ticket.py:36 +#: tickets/models/ticket.py:38 msgid "Title" msgstr "标题" -#: tickets/models/ticket.py:37 tickets/models/ticket.py:130 +#: tickets/models/ticket.py:39 tickets/models/ticket.py:132 msgid "Body" msgstr "内容" -#: tickets/models/ticket.py:39 +#: tickets/models/ticket.py:41 msgid "Assignee" msgstr "处理人" -#: tickets/models/ticket.py:40 +#: tickets/models/ticket.py:42 msgid "Assignee display name" msgstr "处理人名称" -#: tickets/models/ticket.py:41 +#: tickets/models/ticket.py:43 msgid "Assignees" msgstr "待处理人" -#: tickets/models/ticket.py:42 +#: tickets/models/ticket.py:44 msgid "Assignees display name" msgstr "待处理人名称" -#: tickets/models/ticket.py:71 +#: tickets/models/ticket.py:73 msgid "{} {} this ticket" msgstr "{} {} 这个工单" -#: tickets/models/ticket.py:82 +#: tickets/models/ticket.py:84 msgid "this ticket" msgstr "这个工单" +#: tickets/serializers/request_asset_perm.py:11 +msgid "IP group" +msgstr "IP组" + +#: tickets/serializers/request_asset_perm.py:21 +msgid "Confirmed assets" +msgstr "确认的资产" + +#: tickets/serializers/request_asset_perm.py:25 +msgid "Confirmed system user" +msgstr "确认的系统用户" + +#: tickets/serializers/request_asset_perm.py:58 +msgid "Must be organization admin or superuser" +msgstr "必须是组织管理员或者超级管理员" + #: tickets/utils.py:18 msgid "New ticket" msgstr "新工单" @@ -2546,7 +2605,7 @@ msgstr "" " \n" " " -#: users/api/user.py:117 +#: users/api/user.py:119 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2670,7 +2729,7 @@ msgid "Set password" msgstr "设置密码" #: users/forms/user.py:132 users/serializers/user.py:38 -#: xpack/plugins/change_auth_plan/models.py:60 +#: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" @@ -2777,7 +2836,7 @@ msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:119 xpack/plugins/cloud/serializers.py:81 +#: xpack/plugins/cloud/models.py:119 msgid "Account" msgstr "账户" @@ -3546,65 +3605,65 @@ msgid "Token invalid or expired" msgstr "Token错误或失效" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:88 -#: xpack/plugins/change_auth_plan/models.py:183 +#: xpack/plugins/change_auth_plan/models.py:89 +#: xpack/plugins/change_auth_plan/models.py:184 msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models.py:40 +#: xpack/plugins/change_auth_plan/models.py:41 msgid "Custom password" msgstr "自定义密码" -#: xpack/plugins/change_auth_plan/models.py:41 +#: xpack/plugins/change_auth_plan/models.py:42 msgid "All assets use the same random password" msgstr "所有资产使用相同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:42 +#: xpack/plugins/change_auth_plan/models.py:43 msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:64 +#: xpack/plugins/change_auth_plan/models.py:65 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:187 +#: xpack/plugins/change_auth_plan/models.py:188 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:202 -#: xpack/plugins/change_auth_plan/models.py:296 +#: xpack/plugins/change_auth_plan/models.py:203 +#: xpack/plugins/change_auth_plan/models.py:297 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:269 +#: xpack/plugins/change_auth_plan/models.py:270 msgid "Ready" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:270 +#: xpack/plugins/change_auth_plan/models.py:271 msgid "Preflight check" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:271 +#: xpack/plugins/change_auth_plan/models.py:272 msgid "Change auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:272 +#: xpack/plugins/change_auth_plan/models.py:273 msgid "Verify auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:273 +#: xpack/plugins/change_auth_plan/models.py:274 msgid "Keep auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:274 +#: xpack/plugins/change_auth_plan/models.py:275 msgid "Finished" msgstr "结束" -#: xpack/plugins/change_auth_plan/models.py:300 +#: xpack/plugins/change_auth_plan/models.py:301 msgid "Step" msgstr "步骤" -#: xpack/plugins/change_auth_plan/models.py:317 +#: xpack/plugins/change_auth_plan/models.py:318 msgid "Change auth plan task" msgstr "改密计划任务" @@ -3672,59 +3731,55 @@ msgstr "地域" msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:137 xpack/plugins/cloud/serializers.py:85 -msgid "Always update" -msgstr "总是更新" +#: xpack/plugins/cloud/models.py:136 xpack/plugins/cloud/serializers.py:80 +msgid "Covered always" +msgstr "总是被覆盖" -#: xpack/plugins/cloud/models.py:143 +#: xpack/plugins/cloud/models.py:142 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:154 xpack/plugins/cloud/models.py:207 +#: xpack/plugins/cloud/models.py:153 xpack/plugins/cloud/models.py:210 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:202 -msgid "Succeed" -msgstr "成功" - -#: xpack/plugins/cloud/models.py:217 xpack/plugins/cloud/models.py:272 +#: xpack/plugins/cloud/models.py:220 xpack/plugins/cloud/models.py:275 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:245 +#: xpack/plugins/cloud/models.py:248 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/models.py:246 xpack/plugins/cloud/models.py:247 +#: xpack/plugins/cloud/models.py:249 xpack/plugins/cloud/models.py:250 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/models.py:248 +#: xpack/plugins/cloud/models.py:251 msgid "Released" msgstr "已释放" -#: xpack/plugins/cloud/models.py:253 +#: xpack/plugins/cloud/models.py:256 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:257 +#: xpack/plugins/cloud/models.py:260 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:260 +#: xpack/plugins/cloud/models.py:263 msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/models.py:263 +#: xpack/plugins/cloud/models.py:266 msgid "Region" msgstr "地域" -#: xpack/plugins/cloud/providers/aliyun.py:22 +#: xpack/plugins/cloud/providers/aliyun.py:19 msgid "Alibaba Cloud" msgstr "阿里云" -#: xpack/plugins/cloud/providers/aws.py:18 +#: xpack/plugins/cloud/providers/aws.py:15 msgid "AWS (International)" msgstr "AWS (国际)" @@ -3732,63 +3787,63 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/huaweicloud.py:20 +#: xpack/plugins/cloud/providers/huaweicloud.py:17 msgid "Huawei Cloud" msgstr "华为云" -#: xpack/plugins/cloud/providers/huaweicloud.py:23 +#: xpack/plugins/cloud/providers/huaweicloud.py:20 msgid "AF-Johannesburg" msgstr "非洲-约翰内斯堡" -#: xpack/plugins/cloud/providers/huaweicloud.py:24 +#: xpack/plugins/cloud/providers/huaweicloud.py:21 msgid "AP-Bangkok" msgstr "亚太-曼谷" -#: xpack/plugins/cloud/providers/huaweicloud.py:25 +#: xpack/plugins/cloud/providers/huaweicloud.py:22 msgid "AP-Hong Kong" msgstr "亚太-香港" -#: xpack/plugins/cloud/providers/huaweicloud.py:26 +#: xpack/plugins/cloud/providers/huaweicloud.py:23 msgid "AP-Singapore" msgstr "亚太-新加坡" -#: xpack/plugins/cloud/providers/huaweicloud.py:27 +#: xpack/plugins/cloud/providers/huaweicloud.py:24 msgid "CN East-Shanghai1" msgstr "华东-上海1" -#: xpack/plugins/cloud/providers/huaweicloud.py:28 +#: xpack/plugins/cloud/providers/huaweicloud.py:25 msgid "CN East-Shanghai2" msgstr "华东-上海2" -#: xpack/plugins/cloud/providers/huaweicloud.py:29 +#: xpack/plugins/cloud/providers/huaweicloud.py:26 msgid "CN North-Beijing1" msgstr "华北-北京1" -#: xpack/plugins/cloud/providers/huaweicloud.py:30 +#: xpack/plugins/cloud/providers/huaweicloud.py:27 msgid "CN North-Beijing4" msgstr "华北-北京4" -#: xpack/plugins/cloud/providers/huaweicloud.py:31 +#: xpack/plugins/cloud/providers/huaweicloud.py:28 msgid "CN Northeast-Dalian" msgstr "华北-大连" -#: xpack/plugins/cloud/providers/huaweicloud.py:32 +#: xpack/plugins/cloud/providers/huaweicloud.py:29 msgid "CN South-Guangzhou" msgstr "华南-广州" -#: xpack/plugins/cloud/providers/huaweicloud.py:33 +#: xpack/plugins/cloud/providers/huaweicloud.py:30 msgid "CN Southwest-Guiyang1" msgstr "西南-贵阳1" -#: xpack/plugins/cloud/providers/huaweicloud.py:34 +#: xpack/plugins/cloud/providers/huaweicloud.py:31 msgid "EU-Paris" msgstr "欧洲-巴黎" -#: xpack/plugins/cloud/providers/huaweicloud.py:35 +#: xpack/plugins/cloud/providers/huaweicloud.py:32 msgid "LA-Santiago" msgstr "拉美-圣地亚哥" -#: xpack/plugins/cloud/providers/qcloud.py:20 +#: xpack/plugins/cloud/providers/qcloud.py:17 msgid "Tencent Cloud" msgstr "腾讯云" @@ -3800,7 +3855,11 @@ msgstr "用户数量" msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:84 +#: xpack/plugins/cloud/serializers.py:78 +msgid "Account name" +msgstr "账户名称" + +#: xpack/plugins/cloud/serializers.py:79 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" @@ -3889,11 +3948,8 @@ msgstr "企业版" msgid "Ultimate edition" msgstr "旗舰版" -#~ msgid "Covered always" -#~ msgstr "总是被覆盖" - -#~ msgid "Account name" -#~ msgstr "账户名称" +#~ msgid "Always update" +#~ msgstr "总是更新" #~ msgid "Target URL" #~ msgstr "目标URL" @@ -4328,9 +4384,6 @@ msgstr "旗舰版" #~ "系统用户创建时,如果选择了自动推送,JumpServer 会使用 Ansible 自动推送系统" #~ "用户到资产中,如果资产(交换机)不支持 Ansible,请手动填写账号密码。" -#~ msgid "Create system user" -#~ msgstr "创建系统用户" - #~ msgid "Remove success" #~ msgstr "移除成功" @@ -4397,9 +4450,6 @@ msgstr "旗舰版" #~ msgid "Platform detail" #~ msgstr "平台详情" -#~ msgid "System user list" -#~ msgstr "系统用户列表" - #~ msgid "Update system user" #~ msgstr "更新系统用户" @@ -4469,9 +4519,6 @@ msgstr "旗舰版" #~ msgid "Task name" #~ msgstr "任务名称" -#~ msgid "Failed assets" -#~ msgstr "失败资产" - #~ msgid "No assets" #~ msgstr "没有资产" @@ -4633,9 +4680,6 @@ msgstr "旗舰版" #~ msgid "Asset permission list" #~ msgstr "资产授权列表" -#~ msgid "Create asset permission" -#~ msgstr "创建权限规则" - #~ msgid "Update asset permission" #~ msgstr "更新资产授权" @@ -5164,9 +5208,6 @@ msgstr "旗舰版" #~ msgid "Create ticket" #~ msgstr "提交工单" -#~ msgid "Ticket list" -#~ msgstr "工单列表" - #~ msgid "Ticket detail" #~ msgstr "工单详情" @@ -5536,9 +5577,6 @@ msgstr "旗舰版" #~ msgid "Have child node, cancel" #~ msgstr "存在子节点,不能删除" -#~ msgid "Have assets, cancel" -#~ msgstr "存在资产,不能删除" - #~ msgid "Add to node" #~ msgstr "添加到节点" diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index 0b5dd0e7d..4eb820587 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- # from .ticket import * +from .request_asset_perm import * diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py new file mode 100644 index 000000000..370d54b7f --- /dev/null +++ b/apps/tickets/api/request_asset_perm.py @@ -0,0 +1,97 @@ +from django.db.transaction import atomic +from django.utils.translation import ugettext_lazy as _ +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.const.http import POST +from common.drf.api import JMSModelViewSet +from common.permissions import IsValidUser +from common.utils.django import get_object_or_none +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, + NotHaveConfirmedSystemUser +) +from .. import serializers +from ..models import Ticket +from ..permissions import IsAssignee + + +class RequestAssetPermTicketViewSet(JMSModelViewSet): + queryset = Ticket.objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM) + serializer_classes = { + 'default': serializers.RequestAssetPermTicketSerializer, + 'approve': EmptySerializer, + 'reject': EmptySerializer, + } + permission_classes = (IsValidUser,) + filter_fields = ['status', 'title', 'action', 'user_display'] + search_fields = ['user_display', 'title'] + + def _check_can_set_action(self, instance, action): + if instance.status == instance.STATUS_CLOSED: + 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) + + @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) + return Response() + + @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) + def approve(self, request, *args, **kwargs): + instance = self.get_object() + action = instance.ACTION_APPROVE + self._check_can_set_action(instance, action) + + meta = instance.meta + confirmed_assets = meta.get('confirmed_assets', []) + assets = list(Asset.objects.filter(id__in=confirmed_assets)) + if not assets: + raise NotHaveConfirmedAssets(detail=_('Confirm assets first')) + + if len(assets) != len(confirmed_assets): + raise ConfirmedAssetsChanged(detail=_('Confirmed assets changed')) + + confirmed_system_user = meta.get('confirmed_system_user') + if not confirmed_system_user: + raise NotHaveConfirmedSystemUser(detail=_('Confirm system-user first')) + + system_user = get_object_or_none(SystemUser, id=confirmed_system_user) + if system_user is None: + raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed')) + + self._create_asset_permission(instance, assets, system_user) + return Response({'detail': _('Succeed')}) + + def _create_asset_permission(self, instance: Ticket, assets, system_user): + meta = instance.meta + request = self.request + ap_kwargs = { + 'name': meta.get('name', ''), + 'created_by': self.request.user.username, + 'comment': _('{} request assets, approved by {}').format(instance.user_display, + instance.assignee_display) + } + date_start = meta.get('date_start') + date_expired = 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) + ap = AssetPermission.objects.create(**ap_kwargs) + ap.system_users.add(system_user) + ap.assets.add(*assets) + + return ap diff --git a/apps/tickets/exceptions.py b/apps/tickets/exceptions.py new file mode 100644 index 000000000..b8cb7ba5e --- /dev/null +++ b/apps/tickets/exceptions.py @@ -0,0 +1,25 @@ +from common.exceptions import JMSException + + +class NotHaveConfirmedAssets(JMSException): + pass + + +class ConfirmedAssetsChanged(JMSException): + pass + + +class NotHaveConfirmedSystemUser(JMSException): + pass + + +class ConfirmedSystemUserChanged(JMSException): + pass + + +class TicketClosed(JMSException): + pass + + +class TicketActionYet(JMSException): + pass diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 0fdd1c8bd..631761069 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -20,9 +20,11 @@ class Ticket(CommonModelMixin): ) TYPE_GENERAL = 'general' TYPE_LOGIN_CONFIRM = 'login_confirm' + TYPE_REQUEST_ASSET_PERM = 'request_asset' TYPE_CHOICES = ( (TYPE_GENERAL, _("General")), - (TYPE_LOGIN_CONFIRM, _("Login confirm")) + (TYPE_LOGIN_CONFIRM, _("Login confirm")), + (TYPE_REQUEST_ASSET_PERM, _('Request asset permission')) ) ACTION_APPROVE = 'approve' ACTION_REJECT = 'reject' diff --git a/apps/tickets/permissions.py b/apps/tickets/permissions.py index 5bc7be14d..80db4cb94 100644 --- a/apps/tickets/permissions.py +++ b/apps/tickets/permissions.py @@ -4,3 +4,6 @@ from rest_framework.permissions import BasePermission +class IsAssignee(BasePermission): + def has_object_permission(self, request, view, obj): + return obj.is_assignee(request.user) diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py index 0b5dd0e7d..4eb820587 100644 --- a/apps/tickets/serializers/__init__.py +++ b/apps/tickets/serializers/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- # from .ticket import * +from .request_asset_perm import * diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py new file mode 100644 index 000000000..7519f4b1f --- /dev/null +++ b/apps/tickets/serializers/request_asset_perm.py @@ -0,0 +1,120 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse + +from orgs.utils import current_org +from ..models import Ticket + + +class RequestAssetPermTicketSerializer(serializers.ModelSerializer): + ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips', + default=list, label=_('IP group')) + hostname = serializers.CharField(max_length=256, source='meta.hostname', default=None, + allow_blank=True, label=_('Hostname')) + date_start = serializers.DateTimeField(source='meta.date_start', allow_null=True, + required=False, label=_('Date start')) + date_expired = serializers.DateTimeField(source='meta.date_expired', allow_null=True, + required=False, label=_('Date expired')) + confirmed_assets = serializers.ListField(child=serializers.UUIDField(), + 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, + label=_('Confirmed system user')) + assets_waitlist_url = serializers.SerializerMethodField() + system_user_waitlist_url = serializers.SerializerMethodField() + + class Meta: + model = Ticket + mini_fields = ['id', 'title'] + small_fields = [ + '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' + ] + m2m_fields = [ + 'user', 'user_display', 'assignees', 'assignees_display', + 'assignee', 'assignee_display' + ] + + fields = mini_fields + small_fields + m2m_fields + read_only_fields = [ + 'user_display', 'assignees_display', 'type', 'user', 'status', + 'date_created', 'date_updated', 'action', 'id', 'assignee', + 'assignee_display', + ] + extra_kwargs = { + 'status': {'label': _('Status')}, + 'action': {'label': _('Action')}, + 'user_display': {'label': _('User')} + } + + def validate_assignees(self, assignees): + count = current_org.org_admins().filter(id__in=[assignee.id for assignee in assignees]).count() + if count != len(assignees): + raise serializers.ValidationError(_('Must be organization admin or superuser')) + return assignees + + def get_system_user_waitlist_url(self, instance: Ticket): + if not self._is_assignee(instance): + return None + return {'url': reverse('api-assets:system-user-list')} + + def get_assets_waitlist_url(self, instance: Ticket): + if not self._is_assignee(instance): + return None + + asset_api = reverse('api-assets:asset-list') + query = '' + + meta = instance.meta + ips = meta.get('ips', []) + hostname = meta.get('hostname') + + if ips: + query = '?ips=%s' % ','.join(ips) + elif hostname: + query = '?search=%s' % hostname + + return asset_api + query + + def create(self, validated_data): + validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM + validated_data['user'] = self.context['request'].user + self._pop_confirmed_fields() + return super().create(validated_data) + + def save(self, **kwargs): + meta = self.validated_data.get('meta', {}) + date_start = meta.get('date_start') + if date_start: + meta['date_start'] = date_start.strftime('%Y-%m-%d %H:%M:%S%z') + + 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) + + def update(self, instance, validated_data): + new_meta = validated_data['meta'] + if not self._is_assignee(instance): + self._pop_confirmed_fields() + old_meta = instance.meta + meta = {} + meta.update(old_meta) + meta.update(new_meta) + validated_data['meta'] = meta + + return super().update(instance, validated_data) + + def _pop_confirmed_fields(self): + meta = self.validated_data['meta'] + meta.pop('confirmed_assets', None) + meta.pop('confirmed_system_user', None) + + def _is_assignee(self, obj: Ticket): + user = self.context['request'].user + return obj.is_assignee(user) diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index 33cb5a216..a7bd3f6e5 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -7,6 +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', api.TicketViewSet, 'ticket') router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 8fc184375..3ad748316 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -18,6 +18,7 @@ from ..serializers import UserSerializer, UserRetrieveSerializer from .mixins import UserQuerysetMixin from ..models import User from ..signals import post_user_create +from ..filters import OrgRoleUserFilterBackend logger = get_logger(__name__) @@ -35,6 +36,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): 'default': UserSerializer, 'retrieve': UserRetrieveSerializer } + extra_filter_backends = [OrgRoleUserFilterBackend] def get_queryset(self): return super().get_queryset().prefetch_related('groups') diff --git a/apps/users/filters.py b/apps/users/filters.py new file mode 100644 index 000000000..4fc052c20 --- /dev/null +++ b/apps/users/filters.py @@ -0,0 +1,31 @@ +from rest_framework.compat import coreapi, coreschema +from rest_framework import filters + +from orgs.utils import current_org + + +class OrgRoleUserFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + org_role = request.query_params.get('org_role') + if not org_role: + return queryset + + if org_role == 'admins': + return queryset & current_org.get_org_admins() + elif org_role == 'auditors': + return queryset & current_org.get_org_auditors() + elif org_role == 'users': + return queryset & current_org.get_org_users() + elif org_role == 'members': + return queryset & current_org.get_org_members() + + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='org_role', location='query', required=False, type='string', + schema=coreschema.String( + title='Organization role users', + description='Organization role users can be {admins|auditors|users|members}' + ) + ) + ]