mirror of https://github.com/jumpserver/jumpserver
feat(perms): 添加ApplicationPermission API(包含用户/用户组/授权/校验等API)
parent
4847b7a680
commit
1d550cbe64
|
@ -1,4 +1,5 @@
|
|||
from .application import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
class SerializeApplicationToTreeNodeMixin:
|
||||
|
||||
@staticmethod
|
||||
def _serialize_db(db):
|
||||
return {
|
||||
'id': db.id,
|
||||
'name': db.name,
|
||||
'title': db.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'iconSkin': 'database',
|
||||
'meta': {'type': 'database_app'}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_remote_app(remote_app):
|
||||
return {
|
||||
'id': remote_app.id,
|
||||
'name': remote_app.name,
|
||||
'title': remote_app.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'isParent': False,
|
||||
'iconSkin': 'chrome',
|
||||
'meta': {'type': 'remote_app'}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_cloud(cloud):
|
||||
return {
|
||||
'id': cloud.id,
|
||||
'name': cloud.name,
|
||||
'title': cloud.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'isParent': False,
|
||||
'iconSkin': 'k8s',
|
||||
'meta': {'type': 'k8s_app'}
|
||||
}
|
||||
|
||||
def dispatch_serialize(self, application):
|
||||
method_name = f'_serialize_{application.category}'
|
||||
data = getattr(self, method_name)(application)
|
||||
return data
|
||||
|
||||
def serialize_applications(self, applications):
|
||||
data = [self.dispatch_serialize(application) for application in applications]
|
||||
return data
|
|
@ -113,3 +113,6 @@ class Application(CommonModelMixin, OrgModelMixin):
|
|||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return '{}({})'.format(self.name, self.get_category_display())
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
from .asset_permission import *
|
||||
from .application_permission import *
|
||||
from .user_permission import *
|
||||
from .user_permission_application import *
|
||||
from .asset_permission_relation import *
|
||||
from .application_permission_relation import *
|
||||
from .user_group_permission import *
|
||||
from .user_group_permission_application import *
|
||||
from .remote_app_permission import *
|
||||
from .remote_app_permission_relation import *
|
||||
from .user_remote_app_permission import *
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Q
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from common.utils import get_object_or_none
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import ApplicationPermission
|
||||
from ..hands import (
|
||||
User, UserGroup, Asset, Node, SystemUser,
|
||||
)
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class ApplicationPermissionViewSet(OrgModelViewSet):
|
||||
class ApplicationPermissionViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
应用授权列表的增删改查API
|
||||
"""
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
#
|
||||
from rest_framework import generics
|
||||
from django.db.models import F, Value
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from assets.models import Node, Asset
|
||||
from applications.models import Application
|
||||
from orgs.mixins.api import OrgRelationMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
|
@ -18,7 +17,9 @@ __all__ = [
|
|||
'ApplicationPermissionUserRelationViewSet',
|
||||
'ApplicationPermissionUserGroupRelationViewSet',
|
||||
'ApplicationPermissionApplicationRelationViewSet',
|
||||
'ApplicationPermissionSystemUserRelationViewSet'
|
||||
'ApplicationPermissionSystemUserRelationViewSet',
|
||||
'ApplicationPermissionAllApplicationListApi',
|
||||
'ApplicationPermissionAllUserListApi',
|
||||
]
|
||||
|
||||
|
||||
|
@ -96,3 +97,32 @@ class ApplicationPermissionSystemUserRelationViewSet(RelationMixin):
|
|||
Value(')')
|
||||
))
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationPermissionAllApplicationListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.ApplicationPermissionAllApplicationSerializer
|
||||
only_fields = serializers.ApplicationPermissionAllApplicationSerializer.Meta.only_fields
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
perm = get_object_or_404(models.ApplicationPermission, pk=pk)
|
||||
applications = Application.objects.filter(granted_by_permissions=perm)\
|
||||
.only(*self.only_fields).distinct()
|
||||
return applications
|
||||
|
||||
|
||||
class ApplicationPermissionAllUserListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.ApplicationPermissionAllUserSerializer
|
||||
only_fields = serializers.ApplicationPermissionAllUserSerializer.Meta.only_fields
|
||||
filter_fields = ('username', 'name')
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
perm = get_object_or_404(models.ApplicationPermission, pk=pk)
|
||||
users = perm.get_all_users().only(*self.only_fields).distinct()
|
||||
return users
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from applications.models import Application
|
||||
from perms import serializers
|
||||
|
||||
__all__ = [
|
||||
'UserGroupGrantedApplicationsApi'
|
||||
]
|
||||
|
||||
|
||||
class UserGroupGrantedApplicationsApi(ListAPIView):
|
||||
"""
|
||||
获取用户组直接授权的资产
|
||||
"""
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.ApplicationGrantedSerializer
|
||||
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
|
||||
filter_fields = ['id', 'name', 'comment']
|
||||
search_fields = ['name', 'comment']
|
||||
|
||||
def get_queryset(self):
|
||||
user_group_id = self.kwargs.get('pk', '')
|
||||
queryset = Application.objects\
|
||||
.filter(Q(granted_by_permissions__user_groups__id=user_group_id))\
|
||||
.distinct().only(*self.only_fields)
|
||||
return queryset
|
|
@ -0,0 +1,2 @@
|
|||
from .user_permission_applications import *
|
||||
from .common import *
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework.generics import (
|
||||
ListAPIView, get_object_or_404
|
||||
)
|
||||
|
||||
from applications.models import Application
|
||||
from perms.utils.application_permission import (
|
||||
get_application_system_users_id
|
||||
)
|
||||
from perms.api.user_permission.mixin import ForAdminMixin, ForUserMixin
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from ...hands import User, SystemUser
|
||||
from ... import serializers
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserGrantedApplicationSystemUsersApi',
|
||||
'MyGrantedApplicationSystemUsersApi',
|
||||
'ValidateUserApplicationPermissionApi'
|
||||
]
|
||||
|
||||
|
||||
class GrantedApplicationSystemUsersMixin(ListAPIView):
|
||||
serializer_class = serializers.ApplicationSystemUserSerializer
|
||||
only_fields = serializers.ApplicationSystemUserSerializer.Meta.only_fields
|
||||
user: None
|
||||
|
||||
def get_application_system_users_id(self, application):
|
||||
return get_application_system_users_id(self.user, application)
|
||||
|
||||
def get_queryset(self):
|
||||
application_id = self.kwargs.get('application_id')
|
||||
application = get_object_or_404(Application, id=application_id)
|
||||
system_users_id = self.get_application_system_users_id(application)
|
||||
system_users = SystemUser.objects.filter(id__in=system_users_id)\
|
||||
.only(*self.only_fields).order_by('priority')
|
||||
return system_users
|
||||
|
||||
|
||||
class UserGrantedApplicationSystemUsersApi(ForAdminMixin, GrantedApplicationSystemUsersMixin):
|
||||
pass
|
||||
|
||||
|
||||
class MyGrantedApplicationSystemUsersApi(ForUserMixin, GrantedApplicationSystemUsersMixin):
|
||||
pass
|
||||
|
||||
|
||||
class ValidateUserApplicationPermissionApi(APIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_id = request.query_params.get('user_id', '')
|
||||
application_id = request.query_params.get('application_id', '')
|
||||
system_user_id = request.query_params.get('system_user_id', '')
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(user_id)
|
||||
application_id = uuid.UUID(application_id)
|
||||
system_user_id = uuid.UUID(system_user_id)
|
||||
except ValueError:
|
||||
return Response({'msg': False}, status=403)
|
||||
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
application = get_object_or_404(Application, id=application_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
|
||||
system_users_id = get_application_system_users_id(user, application)
|
||||
if system_user.id in system_users_id:
|
||||
return Response({'msg': True}, status=200)
|
||||
|
||||
return Response({'msg': False}, status=403)
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from applications.api.mixin import SerializeApplicationToTreeNodeMixin
|
||||
from perms import serializers
|
||||
from perms.api.user_permission.mixin import ForAdminMixin, ForUserMixin
|
||||
from perms.utils.user_application_permission import (
|
||||
get_user_granted_all_applications
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserAllGrantedApplicationsApi',
|
||||
'MyAllGrantedApplicationsApi',
|
||||
'UserAllGrantedApplicationsAsTreeApi',
|
||||
'MyAllGrantedApplicationsAsTreeApi',
|
||||
]
|
||||
|
||||
|
||||
class AllGrantedApplicationsMixin(ListAPIView):
|
||||
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
|
||||
serializer_class = serializers.ApplicationGrantedSerializer
|
||||
filter_fields = ['id', 'name', 'comment']
|
||||
search_fields = ['name', 'comment']
|
||||
user: None
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = get_user_granted_all_applications(self.user)
|
||||
return queryset.only(*self.only_fields)
|
||||
|
||||
|
||||
class UserAllGrantedApplicationsApi(ForAdminMixin, AllGrantedApplicationsMixin):
|
||||
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
|
||||
serializer_class = serializers.ApplicationGrantedSerializer
|
||||
filter_fields = ['id', 'name', 'comment']
|
||||
search_fields = ['name', 'comment']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = get_user_granted_all_applications(self.user)
|
||||
return queryset.only(*self.only_fields)
|
||||
|
||||
|
||||
class MyAllGrantedApplicationsApi(ForUserMixin, AllGrantedApplicationsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin):
|
||||
"""
|
||||
将应用序列化成树的结构返回
|
||||
"""
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
data = self.serialize_applications(queryset)
|
||||
return Response(data=data)
|
||||
|
||||
|
||||
class UserAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, UserAllGrantedApplicationsApi):
|
||||
pass
|
||||
|
||||
|
||||
class MyAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, MyAllGrantedApplicationsApi):
|
||||
pass
|
|
@ -2,10 +2,12 @@
|
|||
#
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from .base import BasePermission
|
||||
from users.models import User
|
||||
|
||||
__all__ = [
|
||||
'ApplicationPermission',
|
||||
|
@ -36,3 +38,11 @@ class ApplicationPermission(BasePermission):
|
|||
@lazyproperty
|
||||
def system_users_amount(self):
|
||||
return self.system_users.count()
|
||||
|
||||
def get_all_users(self):
|
||||
users_id = self.users.all().values_list('id', flat=True)
|
||||
user_groups_id = self.user_groups.all().values_list('id', flat=True)
|
||||
users = User.objects.filter(
|
||||
Q(id__in=users_id) | Q(groups__id__in=user_groups_id)
|
||||
)
|
||||
return users
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.db.models import Count
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from perms.models import ApplicationPermission
|
||||
|
||||
|
|
|
@ -4,15 +4,15 @@ from rest_framework import serializers
|
|||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from assets.models import Asset, Node
|
||||
from ..models import ApplicationPermission
|
||||
from users.models import User
|
||||
|
||||
__all__ = [
|
||||
'ApplicationPermissionUserRelationSerializer',
|
||||
'ApplicationPermissionUserGroupRelationSerializer',
|
||||
'ApplicationPermissionApplicationRelationSerializer',
|
||||
'ApplicationPermissionSystemUserRelationSerializer'
|
||||
'ApplicationPermissionSystemUserRelationSerializer',
|
||||
'ApplicationPermissionAllApplicationSerializer',
|
||||
'ApplicationPermissionAllUserSerializer'
|
||||
]
|
||||
|
||||
|
||||
|
@ -67,3 +67,26 @@ class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serialize
|
|||
'id', 'systemuser', 'systemuser_display'
|
||||
]
|
||||
|
||||
|
||||
class ApplicationPermissionAllApplicationSerializer(serializers.Serializer):
|
||||
application = serializers.UUIDField(read_only=True, source='id')
|
||||
application_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
only_fields = ['id', 'name']
|
||||
|
||||
@staticmethod
|
||||
def get_application_display(obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class ApplicationPermissionAllUserSerializer(serializers.Serializer):
|
||||
user = serializers.UUIDField(read_only=True, source='id')
|
||||
user_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
only_fields = ['id', 'username', 'name']
|
||||
|
||||
@staticmethod
|
||||
def get_user_display(obj):
|
||||
return str(obj)
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from assets.models import Node, SystemUser, Asset
|
||||
from assets.serializers import ProtocolsField
|
||||
from .asset_permission import ActionsField
|
||||
from applications.models import Application
|
||||
|
||||
__all__ = [
|
||||
'NodeGrantedSerializer',
|
||||
|
@ -15,6 +16,8 @@ __all__ = [
|
|||
'RemoteAppSystemUserSerializer',
|
||||
'DatabaseAppSystemUserSerializer',
|
||||
'K8sAppSystemUserSerializer',
|
||||
'ApplicationGrantedSerializer',
|
||||
'ApplicationSystemUserSerializer'
|
||||
]
|
||||
|
||||
|
||||
|
@ -34,6 +37,19 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = fields
|
||||
|
||||
|
||||
class ApplicationSystemUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
查看授权的应用系统用户的数据结构,这个和SystemUserSerializer不同,字段少
|
||||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
only_fields = (
|
||||
'id', 'name', 'username', 'priority', 'protocol', 'login_mode'
|
||||
)
|
||||
fields = list(only_fields)
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class RemoteAppSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
|
@ -92,3 +108,16 @@ class NodeGrantedSerializer(serializers.ModelSerializer):
|
|||
|
||||
class ActionsSerializer(serializers.Serializer):
|
||||
actions = ActionsField(read_only=True)
|
||||
|
||||
|
||||
class ApplicationGrantedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
被授权应用的数据结构
|
||||
"""
|
||||
class Meta:
|
||||
model = Application
|
||||
only_fields = [
|
||||
'id', 'name', 'domain', 'category', 'type', 'comment', 'org_id'
|
||||
]
|
||||
fields = only_fields + ['org_name']
|
||||
read_only_fields = fields
|
||||
|
|
|
@ -13,17 +13,17 @@ router.register('application-permissions-user-groups-relations', api.Application
|
|||
router.register('application-permissions-applications-relations', api.ApplicationPermissionApplicationRelationViewSet, 'application-permissions-application-relation')
|
||||
router.register('application-permissions-system-users-relations', api.ApplicationPermissionSystemUserRelationViewSet, 'application-permissions-system-users-relation')
|
||||
|
||||
"""
|
||||
user_permission_urlpatterns = [
|
||||
path('<uuid:pk>/applications/', api.UserGrantedApplicationsApi.as_view(), name='user-applications'),
|
||||
path('applications/', api.UserGrantedApplicationsApi.as_view(), name='my-applications'),
|
||||
path('<uuid:pk>/applications/', api.UserAllGrantedApplicationsApi.as_view(), name='user-applications'),
|
||||
path('applications/', api.MyAllGrantedApplicationsApi.as_view(), name='my-applications'),
|
||||
|
||||
# Application as tree
|
||||
path('<uuid:pk>/applications/tree/', api.UserGrantedApplicationsAsTreeApi.as_view(), name='user-applications-as-tree'),
|
||||
path('applications/tree/', api.UserGrantedApplicationsAsTreeApi.as_view(), name='my-applications-as-tree'),
|
||||
# Application As Tree
|
||||
path('<uuid:pk>/applications/tree/', api.UserAllGrantedApplicationsAsTreeApi.as_view(), name='user-applications-as-tree'),
|
||||
path('applications/tree/', api.MyAllGrantedApplicationsAsTreeApi.as_view(), name='my-applications-as-tree'),
|
||||
|
||||
# Application System Users
|
||||
path('<uuid:pk>/applications/<uuid:application_id>/system-users/', api.UserGrantedApplicationSystemUsersApi.as_view(), name='user-application-system-users'),
|
||||
path('applications/<uuid:application_id>/system-users/', api.UserGrantedApplicationSystemUsersApi.as_view(), name='user-application-system-users'),
|
||||
path('applications/<uuid:application_id>/system-users/', api.MyGrantedApplicationSystemUsersApi.as_view(), name='my-application-system-users'),
|
||||
]
|
||||
|
||||
user_group_permission_urlpatterns = [
|
||||
|
@ -31,11 +31,11 @@ user_group_permission_urlpatterns = [
|
|||
]
|
||||
|
||||
permission_urlpatterns = [
|
||||
# 授权规则中授权的用户和数据库应用
|
||||
# 授权规则中授权的用户和应用
|
||||
path('<uuid:pk>/applications/all/', api.ApplicationPermissionAllApplicationListApi.as_view(), name='application-permission-all-applications'),
|
||||
path('<uuid:pk>/users/all/', api.ApplicationPermissionAllUserListApi.as_view(), name='application-permission-all-users'),
|
||||
|
||||
# 验证用户是否有某个数据库应用的权限
|
||||
# 验证用户是否有某个应用的权限
|
||||
path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'),
|
||||
]
|
||||
|
||||
|
@ -46,5 +46,3 @@ application_permission_urlpatterns = [
|
|||
]
|
||||
|
||||
application_permission_urlpatterns += router.urls
|
||||
"""
|
||||
application_permission_urlpatterns = router.urls
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
#
|
||||
|
||||
from .asset_permission import *
|
||||
from .application_permission import *
|
||||
from .remote_app_permission import *
|
||||
from .database_app_permission import *
|
||||
from .k8s_app_permission import *
|
||||
from .user_asset_permission import *
|
||||
from .user_application_permission import *
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..models import ApplicationPermission
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def get_application_system_users_id(user, application):
|
||||
queryset = ApplicationPermission.objects\
|
||||
.filter(Q(users=user) | Q(user_groups__users=user), Q(applications=application))\
|
||||
.valid()\
|
||||
.values_list('system_users', flat=True)
|
||||
return queryset
|
|
@ -0,0 +1,21 @@
|
|||
from perms.models import ApplicationPermission
|
||||
from applications.models import Application
|
||||
|
||||
|
||||
def get_user_all_applicationpermission_ids(user):
|
||||
application_perm_ids = set()
|
||||
application_perm_ids.update(
|
||||
ApplicationPermission.objects.valid().filter(users=user).distinct().values_list('id', flat=True)
|
||||
)
|
||||
application_perm_ids.update(
|
||||
ApplicationPermission.objects.valid().filter(user_groups__users=user).distinct().values_list('id', flat=True)
|
||||
)
|
||||
return application_perm_ids
|
||||
|
||||
|
||||
def get_user_granted_all_applications(user):
|
||||
application_perm_ids = get_user_all_applicationpermission_ids(user)
|
||||
applications = Application.objects.filter(
|
||||
granted_by_permissions__id__in=application_perm_ids
|
||||
).distinct()
|
||||
return applications
|
Loading…
Reference in New Issue