diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 009caa1ce..e44679995 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -3,6 +3,8 @@ from django.core.cache import cache from rest_framework import serializers +from common.serializers import AdaptedBulkListSerializer + from ..models import Node, AdminUser from ..const import ADMIN_USER_CONN_CACHE_KEY @@ -18,6 +20,7 @@ class AdminUserSerializer(serializers.ModelSerializer): reachable_amount = serializers.SerializerMethodField() class Meta: + list_serializer_class = AdaptedBulkListSerializer model = AdminUser fields = '__all__' diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 9640aff7f..c0f435adc 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from rest_framework_bulk.serializers import BulkListSerializer from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer from ..models import Asset from .system_user import AssetSystemUserSerializer @@ -19,7 +19,7 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): """ class Meta: model = Asset - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' validators = [] diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index e49f89bcf..26040f6aa 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -3,6 +3,7 @@ from rest_framework import serializers from common.fields import ChoiceDisplayField +from common.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser @@ -12,6 +13,7 @@ class CommandFilterSerializer(serializers.ModelSerializer): class Meta: model = CommandFilter + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' @@ -21,3 +23,4 @@ class CommandFilterRuleSerializer(serializers.ModelSerializer): class Meta: model = CommandFilterRule fields = '__all__' + list_serializer_class = AdaptedBulkListSerializer diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 9cddf0c49..553911eb8 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -2,6 +2,8 @@ # from rest_framework import serializers +from common.serializers import AdaptedBulkListSerializer + from ..models import Domain, Gateway @@ -12,6 +14,7 @@ class DomainSerializer(serializers.ModelSerializer): class Meta: model = Domain fields = '__all__' + list_serializer_class = AdaptedBulkListSerializer @staticmethod def get_asset_count(obj): @@ -25,6 +28,7 @@ class DomainSerializer(serializers.ModelSerializer): class GatewaySerializer(serializers.ModelSerializer): class Meta: model = Gateway + list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'ip', 'port', 'protocol', 'username', 'domain', 'is_active', 'date_created', 'date_updated', diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index b45e1e508..9fbc9e804 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from rest_framework_bulk.serializers import BulkListSerializer + +from common.serializers import AdaptedBulkListSerializer from ..models import Label @@ -12,7 +13,7 @@ class LabelSerializer(serializers.ModelSerializer): class Meta: model = Label fields = '__all__' - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer @staticmethod def get_asset_count(obj): diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index b4387d1ff..c737f8cbe 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from common.serializers import AdaptedBulkListSerializer + from ..models import SystemUser, Asset from .base import AuthSerializer @@ -17,6 +19,7 @@ class SystemUserSerializer(serializers.ModelSerializer): class Meta: model = SystemUser exclude = ('_password', '_private_key', '_public_key') + list_serializer_class = AdaptedBulkListSerializer def get_field_names(self, declared_fields, info): fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) diff --git a/apps/authentication/backends/openid/middleware.py b/apps/authentication/backends/openid/middleware.py index 75a752708..b1556ff04 100644 --- a/apps/authentication/backends/openid/middleware.py +++ b/apps/authentication/backends/openid/middleware.py @@ -23,15 +23,15 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): def process_request(self, request): # Don't need openid auth if AUTH_OPENID is False if not settings.AUTH_OPENID: - logger.info("Not settings.AUTH_OPENID") + logger.debug("Not settings.AUTH_OPENID") return # Don't need check single logout if user not authenticated if not request.user.is_authenticated: - logger.info("User is not authenticated") + logger.debug("User is not authenticated") return elif not request.session[BACKEND_SESSION_KEY].endswith( BACKEND_OPENID_AUTH_CODE): - logger.info("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE") + logger.debug("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE") return # Check openid user single logout or not with access_token @@ -40,7 +40,6 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): client.openid_connect_client.userinfo( token=request.session.get(OIDT_ACCESS_TOKEN) ) - except Exception as e: logout(request) logger.error(e) diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 0a7d15fef..a5e9a58d3 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -4,6 +4,10 @@ from django.db import models from django.http import JsonResponse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from rest_framework.utils import html +from rest_framework.settings import api_settings +from rest_framework.exceptions import ValidationError +from rest_framework.fields import SkipField class NoDeleteQuerySet(models.query.QuerySet): @@ -89,6 +93,60 @@ class BulkSerializerMixin(object): return ret +class BulkListSerializerMixin(object): + """ + Become rest_framework_bulk doing bulk update raise Exception: + 'QuerySet' object has no attribute 'pk' when doing bulk update + so rewrite it . + https://github.com/miki725/django-rest-framework-bulk/issues/68 + """ + + def to_internal_value(self, data): + """ + List of dicts of native values <- List of dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_list(data) + + if not isinstance(data, list): + message = self.error_messages['not_a_list'].format( + input_type=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='not_a_list') + + if not self.allow_empty and len(data) == 0: + if self.parent and self.partial: + raise SkipField() + + message = self.error_messages['empty'] + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='empty') + + ret = [] + errors = [] + + for item in data: + try: + # prepare child serializer to only handle one instance + self.child.instance = self.instance.get(id=item['id']) if self.instance else None + self.child.initial_data = item + # raw + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret + + class DatetimeSearchMixin: date_format = '%Y-%m-%d' date_from = date_to = None diff --git a/apps/common/serializers.py b/apps/common/serializers.py new file mode 100644 index 000000000..7f2abce92 --- /dev/null +++ b/apps/common/serializers.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# + +from .mixins import BulkListSerializerMixin +from rest_framework_bulk.serializers import BulkListSerializer + + +class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): + pass diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index db3b56171..1c93cbf91 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -1,11 +1,11 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers -from rest_framework_bulk import BulkListSerializer from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission +from common.serializers import AdaptedBulkListSerializer from .utils import set_current_org, get_current_org from .models import Organization from .mixins import OrgMembershipSerializerMixin @@ -14,7 +14,7 @@ from .mixins import OrgMembershipSerializerMixin class OrgSerializer(ModelSerializer): class Meta: model = Organization - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' read_only_fields = ['created_by', 'date_created'] @@ -70,12 +70,12 @@ class OrgReadSerializer(ModelSerializer): class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: model = Organization.admins.through - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: model = Organization.users.through - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' diff --git a/apps/terminal/serializers/v1.py b/apps/terminal/serializers/v1.py index adf75c936..27b12b77a 100644 --- a/apps/terminal/serializers/v1.py +++ b/apps/terminal/serializers/v1.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from rest_framework_bulk.serializers import BulkListSerializer from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer from ..models import Terminal, Status, Session, Task @@ -29,7 +29,7 @@ class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: model = Session - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' @@ -44,7 +44,7 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: fields = '__all__' model = Task - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer class ReplaySerializer(serializers.Serializer): diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 33874deb7..63c3dab12 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -5,6 +5,7 @@ from django.core.cache import cache from django.contrib.auth import logout from django.utils.translation import ugettext as _ +from rest_framework import status from rest_framework import generics from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated @@ -52,9 +53,72 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): self.permission_classes = (IsOrgAdminOrAppUser,) return super().get_permissions() + def _deny_permission(self, instance): + """ + check current user has permission to handle instance + (update, destroy, bulk_update, bulk destroy) + """ + return not self.request.user.is_superuser and instance.is_superuser + + def destroy(self, request, *args, **kwargs): + """ + rewrite because limit org_admin destroy superuser + """ + instance = self.get_object() + if self._deny_permission(instance): + data = {'msg': _("You do not have permission.")} + return Response(data=data, status=status.HTTP_403_FORBIDDEN) + + return super().destroy(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + """ + rewrite because limit org_admin update superuser + """ + instance = self.get_object() + if self._deny_permission(instance): + data = {'msg': _("You do not have permission.")} + return Response(data=data, status=status.HTTP_403_FORBIDDEN) + + return super().update(request, *args, **kwargs) + + def _bulk_deny_permission(self, instances): + deny_instances = [i for i in instances if self._deny_permission(i)] + if len(deny_instances) > 0: + return True + else: + return False + def allow_bulk_destroy(self, qs, filtered): + if self._bulk_deny_permission(filtered): + return False return qs.count() != filtered.count() + def bulk_update(self, request, *args, **kwargs): + """ + rewrite because limit org_admin update superuser + """ + partial = kwargs.pop('partial', False) + + # restrict the update to the filtered queryset + queryset = self.filter_queryset(self.get_queryset()) + if self._bulk_deny_permission(queryset): + data = {'msg': _("You do not have permission.")} + return Response(data=data, status=status.HTTP_403_FORBIDDEN) + + serializer = self.get_serializer( + queryset, data=request.data, many=True, partial=partial, + ) + + try: + serializer.is_valid(raise_exception=True) + except Exception as e: + data = {'error': str(e)} + return Response(data=data, status=status.HTTP_400_BAD_REQUEST) + + self.perform_bulk_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + class UserChangePasswordApi(generics.RetrieveUpdateAPIView): permission_classes = (IsOrgAdmin,) diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/v1.py index 160df7c62..b8c91417d 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/v1.py @@ -3,10 +3,10 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework_bulk import BulkListSerializer from common.utils import get_signer, validate_ssh_public_key from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer from ..models import User, UserGroup signer = get_signer() @@ -16,7 +16,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: model = User - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'username', 'email', 'groups', 'groups_display', 'role', 'role_display', 'avatar_url', 'wechat', 'phone', @@ -52,7 +52,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: model = UserGroup - list_serializer_class = BulkListSerializer + list_serializer_class = AdaptedBulkListSerializer fields = '__all__' read_only_fields = ['created_by'] diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index deb96eb68..351823a9e 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -22,11 +22,11 @@ {% trans 'Asset granted' %}