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 b6b9f74fc..234bb9ca1 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 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}' + ) + ) + ]