diff --git a/apps/common/const.py b/apps/common/const/__init__.py similarity index 100% rename from apps/common/const.py rename to apps/common/const/__init__.py diff --git a/apps/common/const/http.py b/apps/common/const/http.py new file mode 100644 index 000000000..4717d38c9 --- /dev/null +++ b/apps/common/const/http.py @@ -0,0 +1,7 @@ + +GET = 'GET' +POST = 'POST' +PUT = 'PUT' +PATCH = 'PATCH' +DELETE = 'DELETE' +OPTIONS = 'OPTIONS' diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index bbabe9951..0b7b5aed6 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -3,7 +3,9 @@ import time from hashlib import md5 from threading import Thread +from collections import defaultdict +from django.db.models.signals import m2m_changed from django.core.cache import cache from django.http import JsonResponse from rest_framework.response import Response @@ -14,7 +16,7 @@ from ..utils import lazyproperty __all__ = [ "JSONResponseMixin", "CommonApiMixin", - 'AsyncApiMixin', + 'AsyncApiMixin', 'RelationMixin' ] @@ -187,3 +189,47 @@ class AsyncApiMixin(InterceptMixin): data["error"] = str(e) data["status"] = "error" cache.set(key, data, 600) + + +class RelationMixin: + m2m_field = None + from_field = None + to_field = None + to_model = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + assert self.m2m_field is not None, ''' + `m2m_field` should not be `None` + ''' + + self.from_field = self.m2m_field.m2m_field_name() + self.to_field = self.m2m_field.m2m_reverse_field_name() + self.to_model = self.m2m_field.related_model + self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through + + def get_queryset(self): + queryset = self.through.objects.all() + return queryset + + def send_post_add_signal(self, instances): + if not isinstance(instances, list): + instances = [instances] + + from_to_mapper = defaultdict(list) + + for i in instances: + to_id = getattr(i, self.to_field).id + from_obj = getattr(i, self.from_field) + from_to_mapper[from_obj].append(to_id) + + for from_obj, to_ids in from_to_mapper.items(): + m2m_changed.send( + sender=self.through, instance=from_obj, action='post_add', + reverse=False, model=self.to_model, pk_set=to_ids + ) + + def perform_create(self, serializer): + instance = serializer.save() + self.send_post_add_signal(instance) diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index f3f376694..2ad34831c 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404 from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework_bulk import BulkModelViewSet -from common.mixins import CommonApiMixin +from common.mixins import CommonApiMixin, RelationMixin +from orgs.utils import current_org from ..utils import set_to_root_org, filter_org_queryset from ..models import Organization @@ -80,3 +81,12 @@ class OrgMembershipModelViewSetMixin: def get_queryset(self): queryset = self.membership_class.objects.filter(organization=self.org) return queryset + + +class OrgRelationMixin(RelationMixin): + def get_queryset(self): + queryset = super().get_queryset() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(**{f'{self.from_field}__org_id': org_id}) + return queryset diff --git a/apps/perms/api/__init__.py b/apps/perms/api/__init__.py index 61cbd7d58..f12a8cc38 100644 --- a/apps/perms/api/__init__.py +++ b/apps/perms/api/__init__.py @@ -6,6 +6,7 @@ from .user_permission import * from .asset_permission_relation import * from .user_group_permission import * from .remote_app_permission import * +from .remote_app_permission_relation import * from .user_remote_app_permission import * from .database_app_permission import * from .database_app_permission_relation import * diff --git a/apps/perms/api/base.py b/apps/perms/api/base.py new file mode 100644 index 000000000..d4ffc9246 --- /dev/null +++ b/apps/perms/api/base.py @@ -0,0 +1,15 @@ +from django.db.models import F +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.mixins.api import OrgRelationMixin + + +__all__ = [ + 'RelationViewSet' +] + + +class RelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(**{f'{self.from_field}_display': F(f'{self.from_field}__name')}) + return queryset diff --git a/apps/perms/api/database_app_permission_relation.py b/apps/perms/api/database_app_permission_relation.py index 32ab34355..887b723dd 100644 --- a/apps/perms/api/database_app_permission_relation.py +++ b/apps/perms/api/database_app_permission_relation.py @@ -1,14 +1,12 @@ # coding: utf-8 # - from rest_framework import generics from django.db.models import F, Value from django.db.models.functions import Concat from django.shortcuts import get_object_or_404 -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.utils import current_org from common.permissions import IsOrgAdmin +from .base import RelationViewSet from .. import models, serializers __all__ = [ @@ -21,19 +19,9 @@ __all__ = [ ] -class RelationMixin(OrgBulkModelViewSet): - def get_queryset(self): - queryset = self.model.objects.all() - org_id = current_org.org_id() - if org_id is not None: - queryset = queryset.filter(databaseapppermission__org_id=org_id) - queryset = queryset.annotate(databaseapppermission_display=F('databaseapppermission__name')) - return queryset - - -class DatabaseAppPermissionUserRelationViewSet(RelationMixin): +class DatabaseAppPermissionUserRelationViewSet(RelationViewSet): serializer_class = serializers.DatabaseAppPermissionUserRelationSerializer - model = models.DatabaseAppPermission.users.through + m2m_field = models.DatabaseAppPermission.users.field permission_classes = (IsOrgAdmin,) filterset_fields = [ 'id', 'user', 'databaseapppermission' @@ -46,9 +34,9 @@ class DatabaseAppPermissionUserRelationViewSet(RelationMixin): return queryset -class DatabaseAppPermissionUserGroupRelationViewSet(RelationMixin): +class DatabaseAppPermissionUserGroupRelationViewSet(RelationViewSet): serializer_class = serializers.DatabaseAppPermissionUserGroupRelationSerializer - model = models.DatabaseAppPermission.user_groups.through + m2m_field = models.DatabaseAppPermission.user_groups.field permission_classes = (IsOrgAdmin,) filterset_fields = [ 'id', "usergroup", "databaseapppermission" @@ -77,9 +65,9 @@ class DatabaseAppPermissionAllUserListApi(generics.ListAPIView): return users -class DatabaseAppPermissionDatabaseAppRelationViewSet(RelationMixin): +class DatabaseAppPermissionDatabaseAppRelationViewSet(RelationViewSet): serializer_class = serializers.DatabaseAppPermissionDatabaseAppRelationSerializer - model = models.DatabaseAppPermission.database_apps.through + m2m_field = models.DatabaseAppPermission.database_apps.field permission_classes = (IsOrgAdmin,) filterset_fields = [ 'id', 'databaseapp', 'databaseapppermission', @@ -110,9 +98,9 @@ class DatabaseAppPermissionAllDatabaseAppListApi(generics.ListAPIView): return database_apps -class DatabaseAppPermissionSystemUserRelationViewSet(RelationMixin): +class DatabaseAppPermissionSystemUserRelationViewSet(RelationViewSet): serializer_class = serializers.DatabaseAppPermissionSystemUserRelationSerializer - model = models.DatabaseAppPermission.system_users.through + m2m_field = models.DatabaseAppPermission.system_users.field permission_classes = (IsOrgAdmin,) filterset_fields = [ 'id', 'systemuser', 'databaseapppermission' diff --git a/apps/perms/api/remote_app_permission.py b/apps/perms/api/remote_app_permission.py index 6ced7f0ae..cb1998675 100644 --- a/apps/perms/api/remote_app_permission.py +++ b/apps/perms/api/remote_app_permission.py @@ -13,7 +13,6 @@ from ..serializers import ( RemoteAppPermissionUpdateRemoteAppSerializer, ) - __all__ = [ 'RemoteAppPermissionViewSet', 'RemoteAppPermissionAddUserApi', 'RemoteAppPermissionAddRemoteAppApi', diff --git a/apps/perms/api/remote_app_permission_relation.py b/apps/perms/api/remote_app_permission_relation.py new file mode 100644 index 000000000..5cbb7dbf9 --- /dev/null +++ b/apps/perms/api/remote_app_permission_relation.py @@ -0,0 +1,79 @@ +# coding: utf-8 +# +from perms.api.base import RelationViewSet +from rest_framework import generics +from django.db.models import F +from django.shortcuts import get_object_or_404 + +from common.permissions import IsOrgAdmin +from .. import models, serializers + +__all__ = [ + 'RemoteAppPermissionUserRelationViewSet', + 'RemoteAppPermissionRemoteAppRelationViewSet', + 'RemoteAppPermissionAllRemoteAppListApi', + 'RemoteAppPermissionAllUserListApi', +] + + +class RemoteAppPermissionAllUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.PermissionAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.RemoteAppPermission, pk=pk) + users = perm.all_users.only( + *self.serializer_class.Meta.only_fields + ) + return users + + +class RemoteAppPermissionUserRelationViewSet(RelationViewSet): + serializer_class = serializers.RemoteAppPermissionUserRelationSerializer + m2m_field = models.RemoteAppPermission.users.field + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'user', 'remoteapppermission' + ] + search_fields = ('user__name', 'user__username', 'remoteapppermission__name') + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(user_display=F('user__name')) + return queryset + + +class RemoteAppPermissionRemoteAppRelationViewSet(RelationViewSet): + serializer_class = serializers.RemoteAppPermissionRemoteAppRelationSerializer + m2m_field = models.RemoteAppPermission.remote_apps.field + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'remoteapp', 'remoteapppermission', + ] + search_fields = [ + "id", "remoteapp__name", "remoteapppermission__name" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(remoteapp_display=F('remoteapp__name')) + return queryset + + +class RemoteAppPermissionAllRemoteAppListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.RemoteAppPermissionAllRemoteAppSerializer + filter_fields = ("name",) + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.RemoteAppPermission, pk=pk) + remote_apps = perm.all_remote_apps.only( + *self.serializer_class.Meta.only_fields + ) + return remote_apps diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 2467a31b8..4ad52b2ce 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -79,6 +79,23 @@ class BasePermission(OrgModelMixin): return True return False + @property + def all_users(self): + from users.models import User + + users_query = self._meta.get_field('users').related_query_name() + user_groups_query = self._meta.get_field('user_groups').related_query_name() + + users_q = Q(**{ + f'{users_query}': self + }) + + user_groups_q = Q(**{ + f'groups__{user_groups_query}': self + }) + + return User.objects.filter(users_q | user_groups_q).distinct() + def get_all_users(self): from users.models import User users_id = self.users.all().values_list('id', flat=True) diff --git a/apps/perms/models/database_app_permission.py b/apps/perms/models/database_app_permission.py index de2693274..91b989128 100644 --- a/apps/perms/models/database_app_permission.py +++ b/apps/perms/models/database_app_permission.py @@ -4,6 +4,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.utils import lazyproperty from .base import BasePermission __all__ = [ @@ -28,3 +29,11 @@ class DatabaseAppPermission(BasePermission): def get_all_database_apps(self): return self.database_apps.all() + + @lazyproperty + def database_apps_amount(self): + return self.database_apps.count() + + @lazyproperty + def system_users_amount(self): + return self.system_users.count() diff --git a/apps/perms/models/remote_app_permission.py b/apps/perms/models/remote_app_permission.py index c9a7e4463..40114875c 100644 --- a/apps/perms/models/remote_app_permission.py +++ b/apps/perms/models/remote_app_permission.py @@ -23,6 +23,10 @@ class RemoteAppPermission(BasePermission): def get_all_remote_apps(self): return set(self.remote_apps.all()) + @property + def all_remote_apps(self): + return self.remote_apps.all() + @lazyproperty def remote_apps_amount(self): return self.remote_apps.count() diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 7f83bae9b..7b8945827 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -4,6 +4,8 @@ from .asset_permission import * from .user_permission import * from .remote_app_permission import * +from .remote_app_permission_relation import * from .asset_permission_relation import * from .database_app_permission import * from .database_app_permission_relation import * +from .base import * diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py new file mode 100644 index 000000000..33de4980b --- /dev/null +++ b/apps/perms/serializers/base.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + + +class PermissionAllUserSerializer(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) diff --git a/apps/perms/serializers/database_app_permission.py b/apps/perms/serializers/database_app_permission.py index a8b8bafcd..a8813d8a6 100644 --- a/apps/perms/serializers/database_app_permission.py +++ b/apps/perms/serializers/database_app_permission.py @@ -1,6 +1,6 @@ # coding: utf-8 # - +from django.db.models import Count from rest_framework import serializers from common.fields import StringManyToManyField @@ -27,13 +27,22 @@ class DatabaseAppPermissionSerializer(BulkOrgResourceModelSerializer): class DatabaseAppPermissionListSerializer(BulkOrgResourceModelSerializer): - users = StringManyToManyField(many=True, read_only=True) - user_groups = StringManyToManyField(many=True, read_only=True) - database_apps = StringManyToManyField(many=True, read_only=True) - system_users = StringManyToManyField(many=True, read_only=True) - is_valid = serializers.BooleanField() is_expired = serializers.BooleanField() class Meta: model = models.DatabaseAppPermission - fields = '__all__' + fields = [ + 'id', 'name', 'comment', 'is_active', 'users_amount', 'user_groups_amount', + 'date_start', 'date_expired', 'is_valid', 'database_apps_amount', 'system_users_amount', + 'created_by', 'date_created', 'is_expired' + ] + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.annotate( + users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True), + database_apps_amount=Count('database_apps', distinct=True), + system_users_amount=Count('system_users', distinct=True) + ) + return queryset diff --git a/apps/perms/serializers/database_app_permission_relation.py b/apps/perms/serializers/database_app_permission_relation.py index 1a8263cda..deb761853 100644 --- a/apps/perms/serializers/database_app_permission_relation.py +++ b/apps/perms/serializers/database_app_permission_relation.py @@ -1,8 +1,8 @@ # coding: utf-8 # +from perms.serializers.base import PermissionAllUserSerializer from rest_framework import serializers -from applications.models import DatabaseApp from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer @@ -50,16 +50,9 @@ class DatabaseAppPermissionUserGroupRelationSerializer(RelationMixin, serializer ] -class DatabaseAppPermissionAllUserSerializer(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) +class DatabaseAppPermissionAllUserSerializer(PermissionAllUserSerializer): + class Meta(PermissionAllUserSerializer.Meta): + pass class DatabaseAppPermissionDatabaseAppRelationSerializer(RelationMixin, serializers.ModelSerializer): diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py index 2c347386c..a0bd7c410 100644 --- a/apps/perms/serializers/remote_app_permission.py +++ b/apps/perms/serializers/remote_app_permission.py @@ -36,8 +36,8 @@ class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ queryset = queryset.annotate( - users_amount=Count('users'), user_groups_amount=Count('user_groups'), - remote_apps_amount=Count('remote_apps'), system_users_amount=Count('system_users') + users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True), + remote_apps_amount=Count('remote_apps', distinct=True), system_users_amount=Count('system_users', distinct=True) ) return queryset diff --git a/apps/perms/serializers/remote_app_permission_relation.py b/apps/perms/serializers/remote_app_permission_relation.py new file mode 100644 index 000000000..05d06a9da --- /dev/null +++ b/apps/perms/serializers/remote_app_permission_relation.py @@ -0,0 +1,49 @@ +# coding: utf-8 +# +from rest_framework import serializers + +from common.serializers import AdaptedBulkListSerializer +from ..models import RemoteAppPermission + + +__all__ = [ + 'RemoteAppPermissionRemoteAppRelationSerializer', + 'RemoteAppPermissionAllRemoteAppSerializer', + 'RemoteAppPermissionUserRelationSerializer', +] + + +class RemoteAppPermissionRemoteAppRelationSerializer(serializers.ModelSerializer): + remoteapp_display = serializers.ReadOnlyField() + remoteapppermission_display = serializers.ReadOnlyField() + + class Meta: + model = RemoteAppPermission.remote_apps.through + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'remoteapp', 'remoteapp_display', 'remoteapppermission', 'remoteapppermission_display' + ] + + +class RemoteAppPermissionAllRemoteAppSerializer(serializers.Serializer): + remoteapp = serializers.UUIDField(read_only=True, source='id') + remoteapp_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'name'] + + @staticmethod + def get_remoteapp_display(obj): + return str(obj) + + +class RemoteAppPermissionUserRelationSerializer(serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + remoteapppermission_display = serializers.ReadOnlyField() + + class Meta: + model = RemoteAppPermission.users.through + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'user', 'user_display', 'remoteapppermission', 'remoteapppermission_display' + ] diff --git a/apps/perms/urls/remote_app_permission.py b/apps/perms/urls/remote_app_permission.py index 8f83d72d0..798ca9639 100644 --- a/apps/perms/urls/remote_app_permission.py +++ b/apps/perms/urls/remote_app_permission.py @@ -7,6 +7,9 @@ from .. import api router = BulkRouter() router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') +router.register('remote-app-permissions-users-relations', api.RemoteAppPermissionUserRelationViewSet, 'remote-app-permissions-users-relation') +router.register('remote-app-permissions-remote-apps-relations', api.RemoteAppPermissionRemoteAppRelationViewSet, 'remote-app-permissions-remote-apps-relation') + remote_app_permission_urlpatterns = [ # 查询用户授权的RemoteApp @@ -32,7 +35,9 @@ remote_app_permission_urlpatterns = [ path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), + + path('remote-app-permissions//remote-apps/all/', api.RemoteAppPermissionAllRemoteAppListApi.as_view(), name='remote-app-permission-all-remote-apps'), + path('remote-app-permissions//users/all/', api.RemoteAppPermissionAllUserListApi.as_view(), name='remote-app-permission-all-users'), ] remote_app_permission_urlpatterns += router.urls -