mirror of https://github.com/jumpserver/jumpserver
[Update] 控制组织管理员不允许更新、删除超级用户;修复ViewSet API批量更新的bug (#2629)
* [Update] 控制组织管理员不允许编辑(更新、删除)超级用户 - 待续(控制批量更新API) * [Update] 修改方法名称 * [Update] 控制组织管理员不允许批量更新包含超级用户的用户列表 * [Bugfix] 修复所有ViewSet API进行批量更新时rest_framework_bulk库内部的bug * [Update] 修改 OpenID Middleware 日志输出模式 info => debugpull/2632/head^2
parent
aabcf7f31c
commit
caa5060ecd
|
@ -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__'
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .mixins import BulkListSerializerMixin
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
|
||||
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
|
||||
pass
|
|
@ -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__'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,)
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
<a href="{% url 'users:user-granted-asset' pk=user_object.id %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Asset granted' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'users:user-update' pk=user_object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
<a class="btn btn-outline {% if user_object.is_superuser and not request.user.is_superuser %} disabled {% else %} btn-default {% endif %}" href="{% url 'users:user-update' pk=user_object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline {% if request.user != user_object and user_object.username != "admin" %} btn-danger btn-delete-user {% else %} disabled {% endif %}">
|
||||
<a class="btn btn-outline {% if request.user == user_object or user_object.username == "admin" or user_object.is_superuser and not request.user.is_superuser %} disabled {% else %} btn-danger btn-delete-user {% endif %}">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -77,10 +77,16 @@ function initTable() {
|
|||
}
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
|
||||
var update_btn = "";
|
||||
if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) {
|
||||
update_btn = '<a class="btn btn-xs disabled btn-info">{% trans "Update" %}</a>';
|
||||
}
|
||||
else{
|
||||
update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
|
||||
}
|
||||
|
||||
var del_btn = "";
|
||||
if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}") {
|
||||
if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) {
|
||||
del_btn = '<a class="btn btn-xs btn-danger m-l-xs" disabled>{% trans "Delete" %}</a>'
|
||||
.replace('{{ DEFAULT_PK }}', cellData)
|
||||
.replace('99991938', rowData.name);
|
||||
|
|
|
@ -107,6 +107,15 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
|
|||
success_url = reverse_lazy('users:user-list')
|
||||
success_message = update_success_msg
|
||||
|
||||
def _deny_permission(self):
|
||||
obj = self.get_object()
|
||||
return not self.request.user.is_superuser and obj.is_superuser
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self._deny_permission():
|
||||
return redirect(self.success_url)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
check_rules = get_password_check_rules()
|
||||
context = {
|
||||
|
|
Loading…
Reference in New Issue