diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index f9f67b849..71483a4df 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -9,8 +9,6 @@ urlpatterns = [ path('', views.AssetListView.as_view(), name='asset-index'), path('asset/', views.AssetListView.as_view(), name='asset-list'), path('asset/create/', views.AssetCreateView.as_view(), name='asset-create'), - path('asset/export/', views.AssetExportView.as_view(), name='asset-export'), - path('asset/import/', views.BulkImportAssetView.as_view(), name='asset-import'), path('asset//', views.AssetDetailView.as_view(), name='asset-detail'), path('asset//update/', views.AssetUpdateView.as_view(), name='asset-update'), path('asset//delete/', views.AssetDeleteView.as_view(), name='asset-delete'), diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index aef420171..984e2052e 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -37,7 +37,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain __all__ = [ 'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView', 'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView', - 'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView', + 'AssetDeleteView', ] logger = get_logger(__file__) @@ -229,150 +229,3 @@ class AssetDetailView(PermissionsMixin, DetailView): } kwargs.update(context) return super().get_context_data(**kwargs) - - -@method_decorator(csrf_exempt, name='dispatch') -class AssetExportView(PermissionsMixin, View): - permission_classes = [IsValidUser] - - def get(self, request): - spm = request.GET.get('spm', '') - assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else [] - assets_id = cache.get(spm, assets_id_default) - fields = [ - field for field in Asset._meta.fields - if field.name not in [ - 'date_created', 'org_id' - ] - ] - filename = 'assets-{}.csv'.format( - timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S') - ) - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % filename - response.write(codecs.BOM_UTF8) - assets = Asset.objects.filter(id__in=assets_id) - writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL) - - header = [field.verbose_name for field in fields] - writer.writerow(header) - - for asset in assets: - data = [getattr(asset, field.name) for field in fields] - writer.writerow(data) - return response - - def post(self, request, *args, **kwargs): - try: - assets_id = json.loads(request.body).get('assets_id', []) - node_id = json.loads(request.body).get('node_id', None) - except ValueError: - return HttpResponse('Json object not valid', status=400) - - if not assets_id: - node = get_object_or_none(Node, id=node_id) if node_id else Node.root() - assets = node.get_all_assets() - for asset in assets: - assets_id.append(asset.id) - - spm = uuid.uuid4().hex - cache.set(spm, assets_id, 300) - url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm - return JsonResponse({'redirect': url}) - - -class BulkImportAssetView(PermissionsMixin, JSONResponseMixin, FormView): - form_class = forms.FileForm - permission_classes = [IsOrgAdmin] - - def form_valid(self, form): - node_id = self.request.GET.get("node_id") - node = get_object_or_none(Node, id=node_id) if node_id else Node.root() - f = form.cleaned_data['file'] - det_result = chardet.detect(f.read()) - f.seek(0) # reset file seek index - - file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode()) - csv_file = StringIO(file_data) - reader = csv.reader(csv_file) - csv_data = [row for row in reader] - fields = [ - field for field in Asset._meta.fields - if field.name not in [ - 'date_created' - ] - ] - header_ = csv_data[0] - mapping_reverse = {field.verbose_name: field.name for field in fields} - attr = [mapping_reverse.get(n, None) for n in header_] - if None in attr: - data = {'valid': False, - 'msg': 'Must be same format as ' - 'template or export file'} - return self.render_json_response(data) - - created, updated, failed = [], [], [] - assets = [] - for row in csv_data[1:]: - if set(row) == {''}: - continue - - asset_dict_raw = dict(zip(attr, row)) - asset_dict = dict() - for k, v in asset_dict_raw.items(): - v = v.strip() - if k == 'is_active': - v = False if v in ['False', 0, 'false'] else True - elif k == 'admin_user': - v = get_object_or_none(AdminUser, name=v) - elif k in ['port', 'cpu_count', 'cpu_cores']: - try: - v = int(v) - except ValueError: - v = '' - elif k == 'domain': - v = get_object_or_none(Domain, name=v) - elif k == 'platform': - v = v.lower().capitalize() - if v != '': - asset_dict[k] = v - - asset = None - asset_id = asset_dict.pop('id', None) - if asset_id: - asset = get_object_or_none(Asset, id=asset_id) - if not asset: - try: - if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))): - raise Exception(_('already exists')) - with transaction.atomic(): - asset = Asset.objects.create(**asset_dict) - if node: - asset.nodes.set([node]) - created.append(asset_dict['hostname']) - assets.append(asset) - except Exception as e: - failed.append('%s: %s' % (asset_dict['hostname'], str(e))) - else: - for k, v in asset_dict.items(): - if v != '': - setattr(asset, k, v) - try: - asset.save() - updated.append(asset_dict['hostname']) - except Exception as e: - failed.append('%s: %s' % (asset_dict['hostname'], str(e))) - - data = { - 'created': created, - 'created_info': 'Created {}'.format(len(created)), - 'updated': updated, - 'updated_info': 'Updated {}'.format(len(updated)), - 'failed': failed, - 'failed_info': 'Failed {}'.format(len(failed)), - 'valid': True, - 'msg': 'Created: {}. Updated: {}, Error: {}'.format( - len(created), len(updated), len(failed)) - } - return self.render_json_response(data) - diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 776c50e4d..a2f4c9286 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -140,3 +140,14 @@ class NeedMFAVerify(permissions.BasePermission): if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL: return True return False + + +class CanUpdateSuperUser(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in ['GET', 'OPTIONS']: + return True + if request.user.is_superuser: + return True + if hasattr(obj, 'is_superuser') and obj.is_superuser: + return False + return True diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 8368cb993..4730ada43 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -373,7 +373,7 @@ defaults = { 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, 'LOGIN_LOG_KEEP_DAYS': 90, - 'ASSETS_PERM_CACHE_TIME': 3600, + 'ASSETS_PERM_CACHE_TIME': 3600*24, 'SECURITY_MFA_VERIFY_TTL': 3600, } diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index 92a76615b..db4aed12c 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -21,9 +21,7 @@ from ..utils import ( ) from ..hands import User, Asset, Node, SystemUser, NodeSerializer from .. import serializers, const -from ..mixins import ( - AssetsFilterMixin, -) +from ..mixins import AssetsFilterMixin from ..models import Action logger = get_logger(__name__) @@ -155,13 +153,13 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV user = self.get_object() util = AssetPermissionUtil(user, cache_policy=self.cache_policy) assets = util.get_assets() - for k, v in assets.items(): + for asset, system_users in assets.items(): system_users_granted = [] - for system_user, actions in v.items(): + for system_user, actions in system_users.items(): system_user.actions = actions system_users_granted.append(system_user) - k.system_users_granted = system_users_granted - queryset.append(k) + asset.system_users_granted = system_users_granted + queryset.append(system_users_granted) return queryset def get_permissions(self): diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 52b79be4d..f3de9bcb7 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -224,6 +224,8 @@ class AssetPermissionCacheMixin: CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh')) cache_policy = '1' + obj_id = '' + _filter_id = None @classmethod def is_not_using_cache(cls, cache_policy): @@ -270,7 +272,6 @@ class AssetPermissionCacheMixin: def get_assets_from_cache(self): cached = cache.get(self.asset_key) if not cached: - print("Refresh cache") self.update_cache() cached = cache.get(self.asset_key) return cached @@ -320,7 +321,7 @@ class AssetPermissionCacheMixin: def get_meta_cache_key(self): cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_{filter_id}' key = cache_key.format( - obj_id=str(self.object.id), filter_id=self._filter_id + obj_id=self.obj_id, filter_id=self._filter_id ) return key @@ -345,7 +346,7 @@ class AssetPermissionCacheMixin: def expire_cache_meta(self): cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_*' - key = cache_key.format(obj_id=str(self.object.id)) + key = cache_key.format(obj_id=self.obj_id) cache.delete_pattern(key) def update_cache(self): @@ -378,6 +379,15 @@ class AssetPermissionCacheMixin: key = cls.CACHE_KEY_PREFIX + '*' cache.delete_pattern(key) + def get_assets_without_cache(self): + raise NotImplementedError() + + def get_nodes_with_assets_without_cache(self): + raise NotImplementedError() + + def get_system_user_without_cache(self): + raise NotImplementedError() + class AssetPermissionUtil(AssetPermissionCacheMixin): get_permissions_map = { @@ -472,8 +482,6 @@ class AssetPermissionUtil(AssetPermissionCacheMixin): for node in nodes: pattern.add(r'^{0}$|^{0}:'.format(node.key)) pattern = '|'.join(list(pattern)) - now = time.time() - print("Get node assets start") if pattern: assets = Asset.objects.filter(nodes__key__regex=pattern)\ .prefetch_related('nodes', "protocols")\ @@ -481,7 +489,6 @@ class AssetPermissionUtil(AssetPermissionCacheMixin): .distinct() else: assets = [] - print("Get node assets end, using: {}".format(time.time() - now)) self.tree.add_assets_without_system_users(assets) assets = self.tree.get_assets() self._assets = assets diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 1ccc9e3c3..8e6b59a3e 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -13,7 +13,8 @@ from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination from common.permissions import ( - IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser + IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser, + CanUpdateSuperUser, ) from common.mixins import IDInCacheFilterMixin from common.utils import get_logger @@ -37,7 +38,7 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): search_fields = filter_fields queryset = User.objects.exclude(role=User.ROLE_APP) serializer_class = UserSerializer - permission_classes = (IsOrgAdmin,) + permission_classes = (IsOrgAdmin, CanUpdateSuperUser) pagination_class = LimitOffsetPagination def send_created_signal(self, users): @@ -70,28 +71,6 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): """ 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: @@ -108,26 +87,12 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): """ 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) + return super().bulk_update(request, *args, **kwargs) class UserChangePasswordApi(generics.RetrieveUpdateAPIView): diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/v1.py index 2060e45e8..126c40f5d 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/v1.py @@ -39,6 +39,14 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): 'created_by': {'read_only': True}, 'source': {'read_only': True} } + def validate_role(self, value): + request = self.context.get('request') + if not request.user.is_superuser and value != User.ROLE_USER: + role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER] + msg = _("Role limit to {}".format(role_display)) + raise serializers.ValidationError(msg) + return value + @staticmethod def validate_password(value): from ..utils import check_password_rules diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index cf2a764b4..79203cec5 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -2,27 +2,27 @@ {% load i18n static %} {% block table_search %}
- -
+ + {% endblock %} {% block table_container %} @@ -92,23 +92,23 @@ function initTable() { }}, {targets: 7, createdCell: function (td, cellData, rowData) { var update_btn = ""; - if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) { - update_btn = '{% trans "Update" %}'; - } - else{ + {#if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) {#} + {# update_btn = '{% trans "Update" %}';#} + {#}#} + {#else{#} update_btn = '{% trans "Update" %}'.replace('00000000-0000-0000-0000-000000000000', cellData); - } + {#}#} var del_btn = ""; - if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) { - del_btn = '{% trans "Delete" %}' - .replace('{{ DEFAULT_PK }}', cellData) - .replace('99991938', rowData.name); - } else { + {#if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) {#} + {# del_btn = '{% trans "Delete" %}'#} + {# .replace('{{ DEFAULT_PK }}', cellData)#} + {# .replace('99991938', rowData.name);#} + {#} else {#} del_btn = '{% trans "Delete" %}' .replace('{{ DEFAULT_PK }}', cellData) .replace('99991938', rowData.name); - } + {#}#} $(td).html(update_btn + del_btn) }}], ajax_url: '{% url "api-users:user-list" %}', diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index f26300a04..dbed09888 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -29,9 +29,7 @@ urlpatterns = [ # User view path('user/', views.UserListView.as_view(), name='user-list'), - path('user/export/', views.UserExportView.as_view(), name='user-export'), path('first-login/', views.UserFirstLoginView.as_view(), name='user-first-login'), - path('user/import/', views.UserBulkImportView.as_view(), name='user-import'), path('user/create/', views.UserCreateView.as_view(), name='user-create'), path('user//update/', views.UserUpdateView.as_view(), name='user-update'), path('user/update/', views.UserBulkUpdateView.as_view(), name='user-bulk-update'), diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 74ee0bd02..ff9e03530 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -46,9 +46,7 @@ from ..signals import post_user_create __all__ = [ 'UserListView', 'UserCreateView', 'UserDetailView', - 'UserUpdateView', - 'UserGrantedAssetView', - 'UserExportView', 'UserBulkImportView', 'UserProfileView', + 'UserUpdateView', 'UserGrantedAssetView', 'UserProfileView', 'UserProfileUpdateView', 'UserPasswordUpdateView', 'UserPublicKeyUpdateView', 'UserBulkUpdateView', 'UserPublicKeyGenerateView', @@ -223,147 +221,6 @@ class UserDetailView(PermissionsMixin, DetailView): return queryset -@method_decorator(csrf_exempt, name='dispatch') -class UserExportView(View): - def get(self, request): - fields = [ - User._meta.get_field(name) - for name in [ - 'id', 'name', 'username', 'email', 'role', - 'wechat', 'phone', 'is_active', 'comment', - ] - ] - spm = request.GET.get('spm', '') - users_id = cache.get(spm, []) - filename = 'users-{}.csv'.format( - timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S') - ) - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % filename - response.write(codecs.BOM_UTF8) - users = User.objects.filter(id__in=users_id) - writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL) - - header = [field.verbose_name for field in fields] - header.append(_('User groups')) - writer.writerow(header) - - for user in users: - groups = ','.join([group.name for group in user.groups.all()]) - data = [getattr(user, field.name) for field in fields] - data.append(groups) - writer.writerow(data) - - return response - - def post(self, request): - try: - users_id = json.loads(request.body).get('users_id', []) - except ValueError: - return HttpResponse('Json object not valid', status=400) - spm = uuid.uuid4().hex - cache.set(spm, users_id, 300) - url = reverse('users:user-export') + '?spm=%s' % spm - return JsonResponse({'redirect': url}) - - -class UserBulkImportView(PermissionsMixin, JSONResponseMixin, FormView): - form_class = forms.FileForm - permission_classes = [IsOrgAdmin] - - def form_invalid(self, form): - try: - error = form.errors.values()[-1][-1] - except Exception as e: - error = _('Invalid file.') - data = { - 'success': False, - 'msg': error - } - return self.render_json_response(data) - - # todo: need be patch, method to long - def form_valid(self, form): - f = form.cleaned_data['file'] - det_result = chardet.detect(f.read()) - f.seek(0) # reset file seek index - data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode()) - csv_file = StringIO(data) - reader = csv.reader(csv_file) - csv_data = [row for row in reader] - header_ = csv_data[0] - fields = [ - User._meta.get_field(name) - for name in [ - 'id', 'name', 'username', 'email', 'role', - 'wechat', 'phone', 'is_active', 'comment', - ] - ] - mapping_reverse = {field.verbose_name: field.name for field in fields} - mapping_reverse[_('User groups')] = 'groups' - attr = [mapping_reverse.get(n, None) for n in header_] - if None in attr: - data = {'valid': False, - 'msg': 'Must be same format as ' - 'template or export file'} - return self.render_json_response(data) - - created, updated, failed = [], [], [] - for row in csv_data[1:]: - if set(row) == {''}: - continue - user_dict = dict(zip(attr, row)) - id_ = user_dict.pop('id') - for k, v in user_dict.items(): - if k in ['is_active']: - if v.lower() == 'false': - v = False - else: - v = bool(v) - elif k == 'groups': - groups_name = v.split(',') - v = UserGroup.objects.filter(name__in=groups_name) - else: - continue - user_dict[k] = v - user = get_object_or_none(User, id=id_) if id_ and is_uuid(id_) else None - if not user: - try: - with transaction.atomic(): - groups = user_dict.pop('groups') - user = User.objects.create(**user_dict) - user.groups.set(groups) - created.append(user_dict['username']) - post_user_create.send(self.__class__, user=user) - except Exception as e: - failed.append('%s: %s' % (user_dict['username'], str(e))) - else: - for k, v in user_dict.items(): - if k == 'groups': - user.groups.set(v) - continue - if v: - setattr(user, k, v) - try: - user.save() - updated.append(user_dict['username']) - except Exception as e: - failed.append('%s: %s' % (user_dict['username'], str(e))) - - data = { - 'created': created, - 'created_info': 'Created {}'.format(len(created)), - 'updated': updated, - 'updated_info': 'Updated {}'.format(len(updated)), - 'failed': failed, - 'failed_info': 'Failed {}'.format(len(failed)), - 'valid': True, - 'msg': 'Created: {}. Updated: {}, Error: {}'.format( - len(created), len(updated), len(failed)) - } - return self.render_json_response(data) - - class UserGrantedAssetView(PermissionsMixin, DetailView): model = User template_name = 'users/user_granted_asset.html'