diff --git a/apps/applications/api/remote_app.py b/apps/applications/api/remote_app.py index 79beef8fc..4bd9109fb 100644 --- a/apps/applications/api/remote_app.py +++ b/apps/applications/api/remote_app.py @@ -15,7 +15,7 @@ __all__ = [ class RemoteAppViewSet(OrgBulkModelViewSet): model = RemoteApp - filter_fields = ('name',) + filter_fields = ('name', 'type', 'comment') search_fields = filter_fields permission_classes = (IsOrgAdmin,) serializer_class = RemoteAppSerializer diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index c77da1ae4..58fc78f71 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -1,17 +1,4 @@ -# ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. + from django.db import transaction from django.db.models import Count @@ -49,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet): def get_queryset(self): queryset = super().get_queryset() - queryset = queryset.annotate(_assets_amount=Count('assets')) + queryset = queryset.annotate(assets_amount=Count('assets')) return queryset def destroy(self, request, *args, **kwargs): diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 6eb8ebeaf..0d7a42454 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -33,7 +33,7 @@ class AssetViewSet(OrgBulkModelViewSet): API endpoint that allows Asset to be viewed or edited. """ model = Asset - filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") + filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id", "platform__base") search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") serializer_classes = { diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index c0d834737..6e6af72ac 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -84,12 +84,15 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): def get_object(self): pk = self.kwargs.get("pk") + if pk is None: + return queryset = self.get_queryset() obj = queryset.get(id=pk) return obj def get_exception_handler(self): def handler(e, context): + logger.error(e, exc_info=True) return Response({"error": str(e)}, status=400) return handler diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 82d6964a4..3f9a3eb84 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from django.db.models import Prefetch, F +from django.db.models import Prefetch, F, Count from django.utils.translation import ugettext_lazy as _ @@ -73,21 +73,35 @@ class AssetSerializer(BulkOrgResourceModelSerializer): class Meta: model = Asset list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'ip', 'hostname', 'protocol', 'port', - 'protocols', 'platform', 'is_active', 'public_ip', 'domain', - 'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', - 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', - 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', - 'hostname_raw', 'comment', 'created_by', 'date_created', - 'hardware_info', + fields_mini = ['id', 'hostname', 'ip'] + fields_small = fields_mini + [ + 'protocol', 'port', 'protocols', 'is_active', 'public_ip', + 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', + 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', + 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', + 'created_by', 'date_created', 'hardware_info', ] - read_only_fields = ( + fields_fk = [ + 'admin_user', 'domain', 'platform' + ] + fk_only_fields = { + 'platform': ['name'] + } + fields_m2m = [ + 'nodes', 'labels', + ] + annotates_fields = { + # 'admin_user_display': 'admin_user__name' + } + fields_as = list(annotates_fields.keys()) + fields = fields_small + fields_fk + fields_m2m + fields_as + read_only_fields = [ 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'created_by', 'date_created', - ) + ] + fields_as + extra_kwargs = { 'protocol': {'write_only': True}, 'port': {'write_only': True}, @@ -98,11 +112,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - Prefetch('nodes', queryset=Node.objects.all().only('id')), - Prefetch('labels', queryset=Label.objects.all().only('id')), - ).select_related('admin_user', 'domain', 'platform') \ - .annotate(platform_base=F('platform__base')) + queryset = queryset.select_related('admin_user', 'domain', 'platform') return queryset def compatible_with_old_protocol(self, validated_data): diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py index c055e25bd..2629a8327 100644 --- a/apps/assets/serializers/gathered_user.py +++ b/apps/assets/serializers/gathered_user.py @@ -16,7 +16,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin): 'present', 'date_created', 'date_updated' ] read_only_fields = fields - labels = { - 'hostname': _("Hostname"), - 'ip': "IP" + extra_kwargs = { + 'hostname': {'label': _("Hostname")}, + 'ip': {'label': 'IP'}, } diff --git a/apps/audits/api.py b/apps/audits/api.py index 3677a8e8e..7deee860d 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -1,14 +1,97 @@ # -*- coding: utf-8 -*- # +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin -from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor -from orgs.mixins.api import OrgModelViewSet -from .models import FTPLog -from .serializers import FTPLogSerializer +from common.mixins.api import CommonApiMixin +from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin +from common.drf.filters import DatetimeRangeFilter, current_user_filter +from common.api import CommonGenericViewSet +from orgs.mixins.api import OrgGenericViewSet +from orgs.utils import current_org +from ops.models import CommandExecution +from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog +from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer +from .serializers import OperateLogSerializer, PasswordChangeLogSerializer +from .filters import CurrentOrgMembersFilter -class FTPLogViewSet(OrgModelViewSet): +class FTPLogViewSet(ListModelMixin, OrgGenericViewSet): model = FTPLog serializer_class = FTPLogSerializer permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) - http_method_names = ['get', 'post', 'head', 'options'] + extra_filter_backends = [DatetimeRangeFilter] + date_range_filter_fields = [ + ('date_start', ('date_from', 'date_to')) + ] + filterset_fields = ['user', 'asset', 'system_user'] + search_fields = ['filename'] + + +class UserLoginLogViewSet(ListModelMixin, + CommonGenericViewSet): + queryset = UserLoginLog.objects.all() + permission_classes = [IsOrgAdmin | IsOrgAuditor] + serializer_class = UserLoginLogSerializer + extra_filter_backends = [DatetimeRangeFilter] + date_range_filter_fields = [ + ('datetime', ('date_from', 'date_to')) + ] + filterset_fields = ['username'] + search_fields = ['ip', 'city', 'username'] + + @staticmethod + def get_org_members(): + users = current_org.get_org_members().values_list('username', flat=True) + return users + + def get_queryset(self): + queryset = super().get_queryset() + if not current_org.is_default(): + users = self.get_org_members() + queryset = queryset.filter(username__in=users) + return queryset + + +class OperateLogViewSet(ListModelMixin, OrgGenericViewSet): + model = OperateLog + serializer_class = OperateLogSerializer + permission_classes = [IsOrgAdmin | IsOrgAuditor] + extra_filter_backends = [DatetimeRangeFilter] + date_range_filter_fields = [ + ('datetime', ('date_from', 'date_to')) + ] + filterset_fields = ['user', 'action', 'resource_type'] + search_fields = ['filename'] + ordering_fields = ['-datetime'] + + +class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): + queryset = PasswordChangeLog.objects.all() + permission_classes = [IsOrgAdmin | IsOrgAuditor] + serializer_class = PasswordChangeLogSerializer + extra_filter_backends = [DatetimeRangeFilter] + date_range_filter_fields = [ + ('datetime', ('date_from', 'date_to')) + ] + filterset_fields = ['user'] + ordering_fields = ['-datetime'] + + def get_queryset(self): + users = current_org.get_org_members() + queryset = super().get_queryset().filter( + user__in=[user.__str__() for user in users] + ) + return queryset + + +class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): + model = CommandExecution + serializer_class = CommandExecutionSerializer + permission_classes = [IsOrgAdmin | IsOrgAuditor] + extra_filter_backends = [DatetimeRangeFilter, current_user_filter(), CurrentOrgMembersFilter] + date_range_filter_fields = [ + ('date_start', ('date_from', 'date_to')) + ] + search_fields = ['command'] + ordering_fields = ['-date_created'] diff --git a/apps/audits/filters.py b/apps/audits/filters.py new file mode 100644 index 000000000..6db2d9b21 --- /dev/null +++ b/apps/audits/filters.py @@ -0,0 +1,32 @@ +from rest_framework import filters +from rest_framework.compat import coreapi, coreschema + +from orgs.utils import current_org + + +__all__ = ['CurrentOrgMembersFilter'] + + +class CurrentOrgMembersFilter(filters.BaseFilterBackend): + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='user', location='query', required=False, type='string', + schema=coreschema.String( + title='user', + description='user' + ) + ) + ] + + def _get_user_list(self): + users = current_org.get_org_members(exclude=('Auditor',)) + return users + + def filter_queryset(self, request, queryset, view): + user_id = request.GET.get('user') + if user_id: + queryset = queryset.filter(user=user_id) + else: + queryset = queryset.filter(user__in=self._get_user_list()) + return queryset diff --git a/apps/audits/migrations/0008_auto_20200508_2105.py b/apps/audits/migrations/0008_auto_20200508_2105.py new file mode 100644 index 000000000..bde6687ba --- /dev/null +++ b/apps/audits/migrations/0008_auto_20200508_2105.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.10 on 2020-05-08 13:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0007_auto_20191202_1010'), + ] + + operations = [ + migrations.AlterField( + model_name='ftplog', + name='date_start', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date start'), + ), + migrations.AlterField( + model_name='operatelog', + name='datetime', + field=models.DateTimeField(auto_now=True, verbose_name='Datetime'), + ), + migrations.AlterField( + model_name='passwordchangelog', + name='datetime', + field=models.DateTimeField(auto_now=True, verbose_name='Datetime'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 81866bb1d..406d0aa44 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -22,7 +22,7 @@ class FTPLog(OrgModelMixin): operate = models.CharField(max_length=16, verbose_name=_("Operate")) filename = models.CharField(max_length=1024, verbose_name=_("Filename")) is_success = models.BooleanField(default=True, verbose_name=_("Success")) - date_start = models.DateTimeField(auto_now_add=True) + date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start')) class OperateLog(OrgModelMixin): @@ -40,7 +40,7 @@ class OperateLog(OrgModelMixin): resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - datetime = models.DateTimeField(auto_now=True) + datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -51,7 +51,7 @@ class PasswordChangeLog(models.Model): user = models.CharField(max_length=128, verbose_name=_('User')) change_by = models.CharField(max_length=128, verbose_name=_("Change by")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - datetime = models.DateTimeField(auto_now=True) + datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) def __str__(self): return "{} change {}'s password".format(self.change_by, self.user) diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 7dcd94d73..a23efe534 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # - +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from terminal.models import Session +from ops.models import CommandExecution from . import models @@ -11,25 +12,40 @@ class FTPLogSerializer(serializers.ModelSerializer): class Meta: model = models.FTPLog - fields = '__all__' + fields = ( + 'user', 'remote_addr', 'asset', 'system_user', + 'operate', 'filename', 'is_success', 'date_start' + ) -class LoginLogSerializer(serializers.ModelSerializer): +class UserLoginLogSerializer(serializers.ModelSerializer): + type_display = serializers.ReadOnlyField(source='get_type_display') + status_display = serializers.ReadOnlyField(source='get_status_display') + mfa_display = serializers.ReadOnlyField(source='get_mfa_display') + class Meta: model = models.UserLoginLog - fields = '__all__' + fields = ( + 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', + 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display' + ) class OperateLogSerializer(serializers.ModelSerializer): class Meta: model = models.OperateLog - fields = '__all__' + fields = ( + 'user', 'action', 'resource_type', 'resource', + 'remote_addr', 'datetime' + ) class PasswordChangeLogSerializer(serializers.ModelSerializer): class Meta: model = models.PasswordChangeLog - fields = '__all__' + fields = ( + 'user', 'change_by', 'remote_addr', 'datetime' + ) class SessionAuditSerializer(serializers.ModelSerializer): @@ -37,3 +53,18 @@ class SessionAuditSerializer(serializers.ModelSerializer): model = Session fields = '__all__' + +class CommandExecutionSerializer(serializers.ModelSerializer): + class Meta: + model = CommandExecution + fields = ( + 'hosts', 'run_as', 'command', 'user', 'is_finished', + 'date_start', 'result', 'is_success' + ) + extra_kwargs = { + 'result': {'label': _('Result')}, # model 上的方法,只能在这修改 + 'is_success': {'label': _('Is success')}, + 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 + 'run_as': {'label': _('Run as')}, + 'user': {'label': _('User')}, + } diff --git a/apps/audits/urls/api_urls.py b/apps/audits/urls/api_urls.py index 249843f24..be622a357 100644 --- a/apps/audits/urls/api_urls.py +++ b/apps/audits/urls/api_urls.py @@ -12,6 +12,10 @@ app_name = "audits" router = DefaultRouter() router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log') +router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log') +router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log') +router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log') +router.register(r'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log') urlpatterns = [ ] diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 599351d0a..b61798695 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -106,6 +106,9 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): raise exceptions.AuthenticationFailed(_('User disabled.')) return access_key.user, None + def authenticate_header(self, request): + return 'Sign access_key_id:Signature' + class AccessTokenAuthentication(authentication.BaseAuthentication): keyword = 'Bearer' @@ -143,6 +146,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): raise exceptions.AuthenticationFailed(msg) return user, None + def authenticate_header(self, request): + return self.keyword + class PrivateTokenAuthentication(authentication.TokenAuthentication): model = PrivateToken diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index bee5f8517..62283f1f5 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -18,4 +18,5 @@ urlpatterns = [ # openid path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')), + path('captcha/', include('captcha.urls')), ] diff --git a/apps/common/api.py b/apps/common/api.py index d69540cfd..7ecd77122 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -9,10 +9,12 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, serializers +from rest_framework.viewsets import GenericViewSet from .http import HttpResponseTemporaryRedirect from .const import KEY_CACHE_RESOURCES_ID from .utils import get_logger +from .mixins import CommonApiMixin __all__ = [ 'LogTailApi', 'ResourcesIDCacheApi', @@ -100,3 +102,7 @@ def redirect_plural_name_api(request, *args, **kwargs): full_path = org_full_path.replace(resource, resource+"s", 1) logger.debug("Redirect {} => {}".format(org_full_path, full_path)) return HttpResponseTemporaryRedirect(full_path) + + +class CommonGenericViewSet(CommonApiMixin, GenericViewSet): + pass 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/drf/filters.py b/apps/common/drf/filters.py index c11a7864d..8d091b7d8 100644 --- a/apps/common/drf/filters.py +++ b/apps/common/drf/filters.py @@ -1,27 +1,61 @@ # -*- coding: utf-8 -*- # -import coreapi from rest_framework import filters from rest_framework.fields import DateTimeField from rest_framework.serializers import ValidationError +from rest_framework.compat import coreapi, coreschema from django.core.cache import cache +from django.core.exceptions import ImproperlyConfigured import logging from common import const -__all__ = ["DatetimeRangeFilter", "IDSpmFilter", "CustomFilter"] +__all__ = ["DatetimeRangeFilter", "IDSpmFilter", 'IDInFilter', "CustomFilter"] class DatetimeRangeFilter(filters.BaseFilterBackend): - def filter_queryset(self, request, queryset, view): + def get_schema_fields(self, view): + ret = [] + fields = self._get_date_range_filter_fields(view) + + for attr, date_range_keyword in fields.items(): + if len(date_range_keyword) != 2: + continue + for v in date_range_keyword: + ret.append( + coreapi.Field( + name=v, location='query', required=False, type='string', + schema=coreschema.String( + title=v, + description='%s %s' % (attr, v) + ) + ) + ) + + return ret + + def _get_date_range_filter_fields(self, view): if not hasattr(view, 'date_range_filter_fields'): - return queryset + return {} try: - fields = dict(view.date_range_filter_fields) + return dict(view.date_range_filter_fields) except ValueError: - msg = "View {} datetime_filter_fields set is error".format(view.name) + msg = """ + View {} `date_range_filter_fields` set is improperly. + For example: + ``` + class ExampleView: + date_range_filter_fields = [ + ('db column', ('query param date from', 'query param date to')) + ] + ``` + """.format(view.name) logging.error(msg) - return queryset + raise ImproperlyConfigured(msg) + + def filter_queryset(self, request, queryset, view): + fields = self._get_date_range_filter_fields(view) + kwargs = {} for attr, date_range_keyword in fields.items(): if len(date_range_keyword) != 2: @@ -68,6 +102,25 @@ class IDSpmFilter(filters.BaseFilterBackend): return queryset +class IDInFilter(filters.BaseFilterBackend): + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='ids', location='query', required=False, + type='string', example='/api/v1/users/users?ids=1,2,3', + description='Filter by id set' + ) + ] + + def filter_queryset(self, request, queryset, view): + ids = request.query_params.get('ids') + if not ids: + return queryset + id_list = [i.strip() for i in ids.split(',')] + queryset = queryset.filter(id__in=id_list) + return queryset + + class CustomFilter(filters.BaseFilterBackend): def get_schema_fields(self, view): @@ -92,3 +145,10 @@ class CustomFilter(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): return queryset + + +def current_user_filter(user_field='user'): + class CurrentUserFilter(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + return queryset.filter(**{user_field: request.user}) + return CurrentUserFilter diff --git a/apps/common/drf/renders/csv.py b/apps/common/drf/renders/csv.py index d4ae9e6b8..fd862460f 100644 --- a/apps/common/drf/renders/csv.py +++ b/apps/common/drf/renders/csv.py @@ -21,7 +21,9 @@ class JMSCSVRender(BaseRenderer): @staticmethod def _get_show_fields(fields, template): - if template in ('import', 'update'): + if template == 'import': + return [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id'] + elif template == 'update': return [v for k, v in fields.items() if not v.read_only and k != "org_id"] else: return [v for k, v in fields.items() if not v.write_only and k != "org_id"] diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 6440cdd10..0b7b5aed6 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -3,18 +3,20 @@ 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 from rest_framework.settings import api_settings -from common.drf.filters import IDSpmFilter, CustomFilter +from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter from ..utils import lazyproperty __all__ = [ "JSONResponseMixin", "CommonApiMixin", - "IDSpmFilterMixin", 'AsyncApiMixin', + 'AsyncApiMixin', 'RelationMixin' ] @@ -25,19 +27,11 @@ class JSONResponseMixin(object): return JsonResponse(context) -class IDSpmFilterMixin: - def get_filter_backends(self): - backends = super().get_filter_backends() - backends.append(IDSpmFilter) - return backends - - class SerializerMixin: def get_serializer_class(self): serializer_class = None - if hasattr(self, 'serializer_classes') and \ - isinstance(self.serializer_classes, dict): - if self.action == 'list' and self.request.query_params.get('draw'): + if hasattr(self, 'serializer_classes') and isinstance(self.serializer_classes, dict): + if self.action in ['list', 'metadata'] and self.request.query_params.get('draw'): serializer_class = self.serializer_classes.get('display') if serializer_class is None: serializer_class = self.serializer_classes.get( @@ -49,7 +43,10 @@ class SerializerMixin: class ExtraFilterFieldsMixin: - default_added_filters = [CustomFilter, IDSpmFilter] + """ + 额外的 api filter + """ + default_added_filters = [CustomFilter, IDSpmFilter, IDInFilter] filter_backends = api_settings.DEFAULT_FILTER_BACKENDS extra_filter_fields = [] extra_filter_backends = [] @@ -57,9 +54,10 @@ class ExtraFilterFieldsMixin: def get_filter_backends(self): if self.filter_backends != self.__class__.filter_backends: return self.filter_backends - return list(self.filter_backends) + \ - self.default_added_filters + \ - list(self.extra_filter_backends) + backends = list(self.filter_backends) + \ + list(self.default_added_filters) + \ + list(self.extra_filter_backends) + return backends def filter_queryset(self, queryset): for backend in self.get_filter_backends(): @@ -72,6 +70,9 @@ class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): class InterceptMixin: + """ + Hack默认的dispatch, 让用户可以实现 self.do + """ def dispatch(self, request, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -188,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/common/mixins/serializers.py b/apps/common/mixins/serializers.py index 492a7cf98..5c3a243cf 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/mixins/serializers.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # +from collections import Iterable +from django.db.models import Prefetch, F from django.core.exceptions import ObjectDoesNotExist 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, empty -__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin'] +__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin'] class BulkSerializerMixin(object): @@ -113,3 +115,126 @@ class BulkListSerializerMixin(object): raise ValidationError(errors) return ret + + +class BaseDynamicFieldsPlugin: + def __init__(self, serializer): + self.serializer = serializer + + def can_dynamic(self): + try: + request = self.serializer.context['request'] + method = request.method + except (AttributeError, TypeError, KeyError): + # The serializer was not initialized with request context. + return False + + if method != 'GET': + return False + return True + + def get_request(self): + return self.serializer.context['request'] + + def get_query_params(self): + request = self.get_request() + try: + query_params = request.query_params + except AttributeError: + # DRF 2 + query_params = getattr(request, 'QUERY_PARAMS', request.GET) + return query_params + + def get_exclude_field_names(self): + return set() + + +class QueryFieldsMixin(BaseDynamicFieldsPlugin): + # https://github.com/wimglenn/djangorestframework-queryfields/ + + # If using Django filters in the API, these labels mustn't conflict with any model field names. + include_arg_name = 'fields' + exclude_arg_name = 'fields!' + + # Split field names by this string. It doesn't necessarily have to be a single character. + # Avoid RFC 1738 reserved characters i.e. ';', '/', '?', ':', '@', '=' and '&' + delimiter = ',' + + def get_exclude_field_names(self): + query_params = self.get_query_params() + includes = query_params.getlist(self.include_arg_name) + include_field_names = {name for names in includes for name in names.split(self.delimiter) if name} + + excludes = query_params.getlist(self.exclude_arg_name) + exclude_field_names = {name for names in excludes for name in names.split(self.delimiter) if name} + + if not include_field_names and not exclude_field_names: + # No user fields filtering was requested, we have nothing to do here. + return [] + + serializer_field_names = set(self.serializer.fields) + fields_to_drop = serializer_field_names & exclude_field_names + + if include_field_names: + fields_to_drop |= serializer_field_names - include_field_names + return fields_to_drop + + +class SizedModelFieldsMixin(BaseDynamicFieldsPlugin): + arg_name = 'fields_size' + + def can_dynamic(self): + if not hasattr(self.serializer, 'Meta'): + return False + can = super().can_dynamic() + return can + + def get_exclude_field_names(self): + query_params = self.get_query_params() + size = query_params.get(self.arg_name) + if not size: + return [] + if size not in ['mini', 'small']: + return [] + size_fields = getattr(self.serializer.Meta, 'fields_{}'.format(size), None) + if not size_fields or not isinstance(size_fields, Iterable): + return [] + serializer_field_names = set(self.serializer.fields) + fields_to_drop = serializer_field_names - set(size_fields) + return fields_to_drop + + +class DynamicFieldsMixin: + dynamic_fields_plugins = [QueryFieldsMixin, SizedModelFieldsMixin] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + exclude_field_names = set() + for cls in self.dynamic_fields_plugins: + plugin = cls(self) + if not plugin.can_dynamic(): + continue + exclude_field_names |= set(plugin.get_exclude_field_names()) + + for field in exclude_field_names or []: + self.fields.pop(field, None) + + +class EagerLoadQuerySetFields: + def setup_eager_loading(self, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related( + Prefetch('nodes'), + Prefetch('labels'), + ).select_related('admin_user', 'domain', 'platform') \ + .annotate(platform_base=F('platform__base')) + return queryset + + +class CommonSerializerMixin(DynamicFieldsMixin): + pass + + +class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): + pass diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index a00535b5b..b6685def5 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -36,5 +36,3 @@ class DatetimeSearchMixin: def get(self, request, *args, **kwargs): self.get_date_range() return super().get(request, *args, **kwargs) - - diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index fd580093e..716e625b0 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -16,27 +16,36 @@ from common.utils import lazyproperty __all__ = ['IndexApi'] -class MonthLoginMetricMixin: +class DatesLoginMetricMixin: + @lazyproperty + def days(self): + query_params = self.request.query_params + if query_params.get('monthly'): + return 30 + return 7 @lazyproperty - def session_month(self): - month_ago = timezone.now() - timezone.timedelta(days=30) - session_month = Session.objects.filter(date_start__gt=month_ago) - return session_month + def sessions_queryset(self): + days = timezone.now() - timezone.timedelta(days=self.days) + sessions_queryset = Session.objects.filter(date_start__gt=days) + return sessions_queryset @lazyproperty - def session_month_dates(self): - dates = self.session_month.dates('date_start', 'day') + def session_dates_list(self): + now = timezone.now() + dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)] + dates.reverse() + # dates = self.sessions_queryset.dates('date_start', 'day') return dates - def get_month_metrics_date(self): - month_metrics_date = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0'] - return month_metrics_date + def get_dates_metrics_date(self): + dates_metrics_date = [d.strftime('%m-%d') for d in self.session_dates_list] or ['0'] + return dates_metrics_date @staticmethod def get_cache_key(date, tp): date_str = date.strftime("%Y%m%d") - key = "SESSION_MONTH_{}_{}_{}".format(current_org.id, tp, date_str) + key = "SESSION_DATE_{}_{}_{}".format(current_org.id, tp, date_str) return key def __get_data_from_cache(self, date, tp): @@ -69,9 +78,9 @@ class MonthLoginMetricMixin: self.__set_data_to_cache(date, tp, count) return count - def get_month_metrics_total_count_login(self): + def get_dates_metrics_total_count_login(self): data = [] - for d in self.session_month_dates: + for d in self.session_dates_list: count = self.get_date_login_count(d) data.append(count) if len(data) == 0: @@ -88,9 +97,9 @@ class MonthLoginMetricMixin: self.__set_data_to_cache(date, tp, count) return count - def get_month_metrics_total_count_active_users(self): + def get_dates_metrics_total_count_active_users(self): data = [] - for d in self.session_month_dates: + for d in self.session_dates_list: count = self.get_date_user_count(d) data.append(count) return data @@ -105,90 +114,81 @@ class MonthLoginMetricMixin: self.__set_data_to_cache(date, tp, count) return count - def get_month_metrics_total_count_active_assets(self): + def get_dates_metrics_total_count_active_assets(self): data = [] - for d in self.session_month_dates: + for d in self.session_dates_list: count = self.get_date_asset_count(d) data.append(count) return data @lazyproperty - def month_total_count_active_users(self): - count = len(set(self.session_month.values_list('user', flat=True))) + def dates_total_count_active_users(self): + count = len(set(self.sessions_queryset.values_list('user', flat=True))) return count @lazyproperty - def month_total_count_inactive_users(self): + def dates_total_count_inactive_users(self): total = current_org.get_org_members().count() - active = self.month_total_count_active_users + active = self.dates_total_count_active_users count = total - active if count < 0: count = 0 return count @lazyproperty - def month_total_count_disabled_users(self): + def dates_total_count_disabled_users(self): return current_org.get_org_members().filter(is_active=False).count() @lazyproperty - def month_total_count_active_assets(self): - return len(set(self.session_month.values_list('asset', flat=True))) + def dates_total_count_active_assets(self): + return len(set(self.sessions_queryset.values_list('asset', flat=True))) @lazyproperty - def month_total_count_inactive_assets(self): + def dates_total_count_inactive_assets(self): total = Asset.objects.all().count() - active = self.month_total_count_active_assets + active = self.dates_total_count_active_assets count = total - active if count < 0: count = 0 return count @lazyproperty - def month_total_count_disabled_assets(self): + def dates_total_count_disabled_assets(self): return Asset.objects.filter(is_active=False).count() - - -class WeekSessionMetricMixin: - session_week = None - - @lazyproperty - def session_week(self): - week_ago = timezone.now() - timezone.timedelta(weeks=1) - session_week = Session.objects.filter(date_start__gt=week_ago) - return session_week - - def get_week_login_times_top5_users(self): - users = self.session_week.values_list('user', flat=True) + + # 以下是从week中而来 + def get_dates_login_times_top5_users(self): + users = self.sessions_queryset.values_list('user', flat=True) users = [ {'user': user, 'total': total} for user, total in Counter(users).most_common(5) ] return users - def get_week_total_count_login_users(self): - return len(set(self.session_week.values_list('user', flat=True))) + def get_dates_total_count_login_users(self): + return len(set(self.sessions_queryset.values_list('user', flat=True))) - def get_week_total_count_login_times(self): - return self.session_week.count() + def get_dates_total_count_login_times(self): + return self.sessions_queryset.count() - def get_week_login_times_top10_assets(self): - assets = self.session_week.values("asset")\ - .annotate(total=Count("asset"))\ - .annotate(last=Max("date_start")).order_by("-total")[:10] + def get_dates_login_times_top10_assets(self): + assets = self.sessions_queryset.values("asset") \ + .annotate(total=Count("asset")) \ + .annotate(last=Max("date_start")).order_by("-total")[:10] for asset in assets: asset['last'] = str(asset['last']) return list(assets) - def get_week_login_times_top10_users(self): - users = self.session_week.values("user") \ - .annotate(total=Count("user")) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] + def get_dates_login_times_top10_users(self): + users = self.sessions_queryset.values("user") \ + .annotate(total=Count("user")) \ + .annotate(last=Max("date_start")).order_by("-total")[:10] for user in users: user['last'] = str(user['last']) return list(users) - def get_week_login_record_top10_sessions(self): - sessions = self.session_week.order_by('-date_start')[:10] + def get_dates_login_record_top10_sessions(self): + sessions = self.sessions_queryset.order_by('-date_start')[:10] for session in sessions: session.avatar_url = User.get_avatar_url("") sessions = [ @@ -223,7 +223,7 @@ class TotalCountMixin: return Session.objects.filter(is_finished=False).count() -class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, APIView): +class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): permission_classes = (IsOrgAdmin,) http_method_names = ['get'] @@ -234,60 +234,72 @@ class IndexApi(TotalCountMixin, WeekSessionMetricMixin, MonthLoginMetricMixin, A _all = query_params.get('all') - if _all or query_params.get('total_count'): + if _all or query_params.get('total_count') or query_params.get('total_count_users'): + data.update({ + 'total_count_users': self.get_total_count_users(), + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_assets'): data.update({ 'total_count_assets': self.get_total_count_assets(), - 'total_count_users': self.get_total_count_users(), + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): + data.update({ 'total_count_online_users': self.get_total_count_online_users(), + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'): + data.update({ 'total_count_online_sessions': self.get_total_count_online_sessions(), }) - if _all or query_params.get('month_metrics'): + if _all or query_params.get('dates_metrics'): data.update({ - 'month_metrics_date': self.get_month_metrics_date(), - 'month_metrics_total_count_login': self.get_month_metrics_total_count_login(), - 'month_metrics_total_count_active_users': self.get_month_metrics_total_count_active_users(), - 'month_metrics_total_count_active_assets': self.get_month_metrics_total_count_active_assets(), + 'dates_metrics_date': self.get_dates_metrics_date(), + 'dates_metrics_total_count_login': self.get_dates_metrics_total_count_login(), + 'dates_metrics_total_count_active_users': self.get_dates_metrics_total_count_active_users(), + 'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(), }) - if _all or query_params.get('month_total_count_users'): + if _all or query_params.get('dates_total_count_users'): data.update({ - 'month_total_count_active_users': self.month_total_count_active_users, - 'month_total_count_inactive_users': self.month_total_count_inactive_users, - 'month_total_count_disabled_users': self.month_total_count_disabled_users, + 'dates_total_count_active_users': self.dates_total_count_active_users, + 'dates_total_count_inactive_users': self.dates_total_count_inactive_users, + 'dates_total_count_disabled_users': self.dates_total_count_disabled_users, }) - if _all or query_params.get('month_total_count_assets'): + if _all or query_params.get('dates_total_count_assets'): data.update({ - 'month_total_count_active_assets': self.month_total_count_active_assets, - 'month_total_count_inactive_assets': self.month_total_count_inactive_assets, - 'month_total_count_disabled_assets': self.month_total_count_disabled_assets, + 'dates_total_count_active_assets': self.dates_total_count_active_assets, + 'dates_total_count_inactive_assets': self.dates_total_count_inactive_assets, + 'dates_total_count_disabled_assets': self.dates_total_count_disabled_assets, }) - if _all or query_params.get('week_total_count'): + if _all or query_params.get('dates_total_count'): data.update({ - 'week_total_count_login_users': self.get_week_total_count_login_users(), - 'week_total_count_login_times': self.get_week_total_count_login_times(), + 'dates_total_count_login_users': self.get_dates_total_count_login_users(), + 'dates_total_count_login_times': self.get_dates_total_count_login_times(), }) - if _all or query_params.get('week_login_times_top5_users'): + if _all or query_params.get('dates_login_times_top5_users'): data.update({ - 'week_login_times_top5_users': self.get_week_login_times_top5_users(), + 'dates_login_times_top5_users': self.get_dates_login_times_top5_users(), }) - if _all or query_params.get('week_login_times_top10_assets'): + if _all or query_params.get('dates_login_times_top10_assets'): data.update({ - 'week_login_times_top10_assets': self.get_week_login_times_top10_assets(), + 'dates_login_times_top10_assets': self.get_dates_login_times_top10_assets(), }) - if _all or query_params.get('week_login_times_top10_users'): + if _all or query_params.get('dates_login_times_top10_users'): data.update({ - 'week_login_times_top10_users': self.get_week_login_times_top10_users(), + 'dates_login_times_top10_users': self.get_dates_login_times_top10_users(), }) - if _all or query_params.get('week_login_record_top10_sessions'): + if _all or query_params.get('dates_login_record_top10_sessions'): data.update({ - 'week_login_record_top10_sessions': self.get_week_login_record_top10_sessions() + 'dates_login_record_top10_sessions': self.get_dates_login_record_top10_sessions() }) return JsonResponse(data, status=200) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 5da97cfb7..6a54ec251 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -123,7 +123,7 @@ class Config(dict): # Django Config, Must set before start 'SECRET_KEY': '', 'BOOTSTRAP_TOKEN': '', - 'DEBUG': True, + 'DEBUG': False, 'LOG_LEVEL': 'DEBUG', 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), 'DB_ENGINE': 'mysql', @@ -256,7 +256,9 @@ class Config(dict): 'FORCE_SCRIPT_NAME': '', 'LOGIN_CONFIRM_ENABLE': False, 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, - 'ORG_CHANGE_TO_URL': '' + 'ORG_CHANGE_TO_URL': '', + 'LANGUAGE_CODE': 'zh', + 'TIME_ZONE': 'Asia/Shanghai' } def compatible_auth_openid_of_key(self): @@ -435,6 +437,15 @@ class DynamicConfig: backends.insert(0, 'authentication.backends.radius.RadiusBackend') return backends + def XPACK_LICENSE_IS_VALID(self): + if not HAS_XPACK: + return False + try: + from xpack.plugins.license.models import License + return License.has_valid_license() + except: + return False + def get_from_db(self, item): if self.db_setting is not None: value = self.db_setting.get(item) diff --git a/apps/jumpserver/middleware.py b/apps/jumpserver/middleware.py index 775041b9d..f1589003d 100644 --- a/apps/jumpserver/middleware.py +++ b/apps/jumpserver/middleware.py @@ -15,7 +15,7 @@ class TimezoneMiddleware: self.get_response = get_response def __call__(self, request): - tzname = request.META.get('TZ') + tzname = request.META.get('HTTP_X_TZ') if tzname: timezone.activate(pytz.timezone(tzname)) else: diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index d1fbb5a36..0f8cf98a8 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -175,9 +175,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ # LANGUAGE_CODE = 'en' -LANGUAGE_CODE = 'zh' +LANGUAGE_CODE = CONFIG.LANGUAGE_CODE -TIME_ZONE = 'Asia/Shanghai' +TIME_ZONE = CONFIG.TIME_ZONE USE_I18N = True diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index a55693c55..3bada5469 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -85,3 +85,7 @@ LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD + +# XPACK +XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID + diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index ade854397..a43a9ea6b 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -43,6 +43,11 @@ app_view_patterns = [ path('applications/', include('applications.urls.views_urls', namespace='applications')), path('tickets/', include('tickets.urls.views_urls', namespace='tickets')), re_path(r'flower/(?P.*)', views.celery_flower_view, name='flower-view'), + re_path('luna/.*', views.LunaView.as_view(), name='luna-view'), + re_path('koko/.*', views.KokoView.as_view(), name='koko-view'), + re_path('ws/.*', views.WsView.as_view(), name='ws-view'), + path('i18n//', views.I18NView.as_view(), name='i18n-switch'), + path('settings/', include('settings.urls.view_urls', namespace='settings')), ] @@ -59,32 +64,38 @@ js_i18n_patterns = i18n_patterns( ) +apps = [ + 'users', 'assets', 'perms', 'terminal', 'ops', 'audits', 'orgs', 'auth', + 'applications', 'tickets', 'settings', 'xpack' + 'flower', 'luna', 'koko', 'ws', 'i18n', 'jsi18n', 'docs', 'redocs', + 'zh-hans' +] + + urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('api/v1/', include(api_v1)), path('api/v2/', include(api_v2)), re_path('api/(?P\w+)/(?Pv\d)/.*', views.redirect_format_api), path('api/health/', views.HealthCheckView.as_view(), name="health"), - re_path('luna/.*', views.LunaView.as_view(), name='luna-view'), - re_path('koko/.*', views.KokoView.as_view(), name='koko-view'), - re_path('ws/.*', views.WsView.as_view(), name='ws-view'), - path('i18n//', views.I18NView.as_view(), name='i18n-switch'), - path('settings/', include('settings.urls.view_urls', namespace='settings')), - # External apps url - path('captcha/', include('captcha.urls')), + path('core/auth/captcha/', include('captcha.urls')), + path('core/', include(app_view_patterns)), ] -urlpatterns += app_view_patterns urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += js_i18n_patterns +# 兼容之前的 +old_app_pattern = '|'.join(apps) +urlpatterns += [re_path(old_app_pattern, views.redirect_old_apps_view)] + handler404 = 'jumpserver.views.handler404' handler500 = 'jumpserver.views.handler500' if settings.DEBUG: - urlpatterns += [ + app_view_patterns += [ re_path('^swagger(?P\.json|\.yaml)$', views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), path('docs/', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index a1db94e77..17a7dfd6c 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -3,7 +3,7 @@ import re import time -from django.http import HttpResponseRedirect, JsonResponse +from django.http import HttpResponseRedirect, JsonResponse, Http404 from django.conf import settings from django.views.generic import View from django.utils.translation import ugettext_lazy as _ @@ -16,7 +16,7 @@ from common.http import HttpResponseTemporaryRedirect __all__ = [ 'LunaView', 'I18NView', 'KokoView', 'WsView', 'HealthCheckView', - 'redirect_format_api' + 'redirect_format_api', 'redirect_old_apps_view' ] @@ -51,6 +51,14 @@ def redirect_format_api(request, *args, **kwargs): return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404) +def redirect_old_apps_view(request, *args, **kwargs): + path = request.get_full_path() + if path.find('/core') != -1: + raise Http404() + new_path = '/core{}'.format(path) + return HttpResponseTemporaryRedirect(new_path) + + class HealthCheckView(APIView): permission_classes = () diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c51d13e46..6ea8518e0 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 8207c4b7a..067d402b4 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-20 19:25+0800\n" +"POT-Creation-Date: 2020-05-29 14:51+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -53,7 +53,7 @@ msgstr "自定义" #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 #: xpack/plugins/change_auth_plan/forms.py:74 -#: xpack/plugins/change_auth_plan/models.py:265 +#: xpack/plugins/change_auth_plan/models.py:274 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:40 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 @@ -141,13 +141,13 @@ msgstr "运行参数" #: perms/templates/perms/remote_app_permission_list.html:14 #: perms/templates/perms/remote_app_permission_remote_app.html:49 #: perms/templates/perms/remote_app_permission_user.html:49 -#: settings/models.py:26 +#: settings/models.py:27 #: settings/templates/settings/_ldap_list_users_modal.html:32 #: terminal/models.py:26 terminal/models.py:342 terminal/models.py:374 #: terminal/models.py:411 terminal/templates/terminal/base_storage_list.html:31 #: terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:30 users/forms/profile.py:20 -#: users/models/group.py:15 users/models/user.py:440 +#: users/models/group.py:15 users/models/user.py:462 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -170,9 +170,8 @@ msgstr "运行参数" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:53 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:16 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:51 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:47 #: xpack/plugins/orgs/templates/orgs/org_list.html:12 -#: xpack/plugins/orgs/templates/orgs/org_users.html:46 msgid "Name" msgstr "名称" @@ -250,17 +249,17 @@ msgstr "数据库" #: perms/templates/perms/asset_permission_detail.html:97 #: perms/templates/perms/database_app_permission_detail.html:93 #: perms/templates/perms/remote_app_permission_detail.html:89 -#: settings/models.py:31 terminal/models.py:36 terminal/models.py:381 +#: settings/models.py:32 terminal/models.py:36 terminal/models.py:381 #: terminal/models.py:418 terminal/templates/terminal/base_storage_list.html:33 #: terminal/templates/terminal/terminal_detail.html:63 #: tickets/templates/tickets/ticket_detail.html:104 users/models/group.py:16 -#: users/models/user.py:473 users/templates/users/user_detail.html:115 +#: users/models/user.py:495 users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 #: users/templates/users/user_group_detail.html:62 #: users/templates/users/user_group_list.html:16 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:75 +#: xpack/plugins/change_auth_plan/models.py:76 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:115 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 #: xpack/plugins/cloud/models.py:53 xpack/plugins/cloud/models.py:139 @@ -269,14 +268,14 @@ msgstr "数据库" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:128 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 #: xpack/plugins/gathered_user/models.py:26 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:63 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:59 #: xpack/plugins/orgs/templates/orgs/org_list.html:23 msgid "Comment" msgstr "备注" #: applications/models/database_app.py:41 #: perms/forms/database_app_permission.py:44 -#: perms/models/database_app_permission.py:17 +#: perms/models/database_app_permission.py:18 #: perms/templates/perms/database_app_permission_create_update.html:46 #: perms/templates/perms/database_app_permission_database_app.html:23 #: perms/templates/perms/database_app_permission_database_app.html:53 @@ -322,9 +321,9 @@ msgstr "参数" #: perms/templates/perms/asset_permission_detail.html:93 #: perms/templates/perms/database_app_permission_detail.html:89 #: perms/templates/perms/remote_app_permission_detail.html:85 -#: users/models/user.py:481 users/serializers/group.py:32 +#: users/models/user.py:503 users/serializers/group.py:35 #: users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:79 +#: xpack/plugins/change_auth_plan/models.py:80 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:111 #: xpack/plugins/cloud/models.py:56 xpack/plugins/cloud/models.py:145 #: xpack/plugins/gathered_user/models.py:30 @@ -343,7 +342,7 @@ msgstr "创建者" #: assets/templates/assets/cmd_filter_detail.html:64 #: assets/templates/assets/domain_detail.html:63 #: assets/templates/assets/system_user_detail.html:104 -#: common/mixins/models.py:50 ops/models/adhoc.py:38 +#: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 #: ops/templates/ops/adhoc_detail.html:88 ops/templates/ops/task_detail.html:62 #: orgs/models.py:17 perms/models/base.py:55 #: perms/templates/perms/asset_permission_detail.html:89 @@ -356,7 +355,7 @@ msgstr "创建者" #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:148 #: xpack/plugins/cloud/templates/cloud/account_detail.html:63 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:108 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:59 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:55 msgid "Date created" msgstr "创建日期" @@ -539,7 +538,7 @@ msgstr "详情" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:26 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:60 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:46 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:24 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:93 msgid "Update" msgstr "更新" @@ -591,7 +590,7 @@ msgstr "更新" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:30 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:61 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:47 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:28 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:24 #: xpack/plugins/orgs/templates/orgs/org_list.html:95 msgid "Delete" msgstr "删除" @@ -651,7 +650,6 @@ msgstr "创建数据库应用" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:19 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:24 -#: xpack/plugins/orgs/templates/orgs/org_users.html:47 msgid "Action" msgstr "动作" @@ -679,7 +677,7 @@ msgstr "创建远程应用" msgid "Connect" msgstr "连接" -#: applications/views/database_app.py:26 users/models/user.py:144 +#: applications/views/database_app.py:26 users/models/user.py:156 msgid "Application" msgstr "应用程序" @@ -718,7 +716,7 @@ msgstr "远程应用详情" msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/admin_user.py:59 +#: assets/api/admin_user.py:46 msgid "Deleted failed, There are related assets" msgstr "删除失败,存在关联资产" @@ -743,7 +741,7 @@ msgstr "最新版本的不能被删除" #: assets/templates/assets/asset_detail.html:194 #: assets/templates/assets/system_user_assets.html:118 #: perms/models/asset_permission.py:81 -#: xpack/plugins/change_auth_plan/models.py:54 +#: xpack/plugins/change_auth_plan/models.py:55 #: xpack/plugins/gathered_user/models.py:24 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:17 msgid "Nodes" @@ -855,14 +853,14 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: perms/templates/perms/remote_app_permission_user.html:50 #: settings/templates/settings/_ldap_list_users_modal.html:31 #: settings/templates/settings/_ldap_test_user_login_modal.html:10 -#: users/forms/profile.py:19 users/models/user.py:438 +#: users/forms/profile.py:19 users/models/user.py:460 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:59 -#: xpack/plugins/change_auth_plan/models.py:45 -#: xpack/plugins/change_auth_plan/models.py:261 +#: xpack/plugins/change_auth_plan/models.py:46 +#: xpack/plugins/change_auth_plan/models.py:270 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:63 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 @@ -920,15 +918,15 @@ msgstr "密码或密钥密码" #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:66 -#: xpack/plugins/change_auth_plan/models.py:181 -#: xpack/plugins/change_auth_plan/models.py:268 +#: xpack/plugins/change_auth_plan/models.py:67 +#: xpack/plugins/change_auth_plan/models.py:190 +#: xpack/plugins/change_auth_plan/models.py:277 msgid "Password" msgstr "密码" #: assets/forms/user.py:29 assets/serializers/asset_user.py:79 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:467 +#: users/models/user.py:489 msgid "Private key" msgstr "ssh私钥" @@ -1008,7 +1006,8 @@ msgstr "内部的" #: assets/templates/assets/user_asset_list.html:76 #: audits/templates/audits/login_log_list.html:60 #: perms/templates/perms/asset_permission_list.html:187 -#: settings/forms/terminal.py:16 users/templates/users/_granted_assets.html:26 +#: settings/forms/terminal.py:16 settings/serializers/settings.py:52 +#: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:24 @@ -1025,7 +1024,8 @@ msgstr "IP" #: assets/templates/assets/asset_list.html:24 #: assets/templates/assets/user_asset_list.html:75 #: perms/templates/perms/asset_permission_list.html:188 -#: settings/forms/terminal.py:15 users/templates/users/_granted_assets.html:25 +#: settings/forms/terminal.py:15 settings/serializers/settings.py:51 +#: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:49 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:23 @@ -1146,15 +1146,15 @@ msgstr "版本" msgid "AuthBook" msgstr "" -#: assets/models/base.py:235 xpack/plugins/change_auth_plan/models.py:70 -#: xpack/plugins/change_auth_plan/models.py:188 -#: xpack/plugins/change_auth_plan/models.py:275 +#: assets/models/base.py:235 xpack/plugins/change_auth_plan/models.py:71 +#: xpack/plugins/change_auth_plan/models.py:197 +#: xpack/plugins/change_auth_plan/models.py:284 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:236 xpack/plugins/change_auth_plan/models.py:73 -#: xpack/plugins/change_auth_plan/models.py:184 -#: xpack/plugins/change_auth_plan/models.py:271 +#: assets/models/base.py:236 xpack/plugins/change_auth_plan/models.py:74 +#: xpack/plugins/change_auth_plan/models.py:193 +#: xpack/plugins/change_auth_plan/models.py:280 msgid "SSH public key" msgstr "ssh公钥" @@ -1174,7 +1174,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:459 +#: assets/models/cluster.py:22 users/models/user.py:481 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -1200,7 +1200,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:600 +#: users/models/user.py:622 msgid "System" msgstr "系统" @@ -1308,7 +1308,7 @@ msgstr "默认资产组" #: assets/models/label.py:15 assets/templates/assets/system_user_users.html:63 #: audits/models.py:18 audits/models.py:38 audits/models.py:51 -#: audits/templates/audits/ftp_log_list.html:37 +#: audits/serializers.py:69 audits/templates/audits/ftp_log_list.html:37 #: audits/templates/audits/ftp_log_list.html:74 #: audits/templates/audits/operate_log_list.html:37 #: audits/templates/audits/password_change_log_list.html:37 @@ -1333,7 +1333,7 @@ msgstr "默认资产组" #: tickets/models/ticket.py:128 tickets/templates/tickets/ticket_detail.html:32 #: tickets/templates/tickets/ticket_list.html:34 #: tickets/templates/tickets/ticket_list.html:103 users/forms/group.py:15 -#: users/models/user.py:143 users/models/user.py:159 users/models/user.py:588 +#: users/models/user.py:155 users/models/user.py:171 users/models/user.py:610 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -1341,15 +1341,17 @@ msgstr "默认资产组" #: users/templates/users/user_database_app_permission.html:58 #: users/templates/users/user_group_detail.html:73 #: users/templates/users/user_group_list.html:15 +#: users/templates/users/user_list.html:135 #: users/templates/users/user_remote_app_permission.html:37 #: users/templates/users/user_remote_app_permission.html:58 #: users/views/profile/base.py:46 xpack/plugins/orgs/forms.py:27 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:108 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User" msgstr "用户" #: assets/models/label.py:19 assets/models/node.py:488 -#: assets/templates/assets/label_list.html:15 settings/models.py:27 +#: assets/templates/assets/label_list.html:15 settings/models.py:28 msgid "Value" msgstr "值" @@ -1405,7 +1407,7 @@ msgstr "手动登录" #: assets/views/platform.py:58 assets/views/platform.py:74 #: assets/views/system_user.py:30 assets/views/system_user.py:47 #: assets/views/system_user.py:64 assets/views/system_user.py:80 -#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:50 +#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:51 msgid "Assets" msgstr "资产管理" @@ -1421,7 +1423,6 @@ msgid "Users" msgstr "用户管理" #: assets/models/user.py:112 users/templates/users/user_group_list.html:90 -#: users/templates/users/user_list.html:135 #: users/templates/users/user_profile.html:124 msgid "User groups" msgstr "用户组" @@ -1454,7 +1455,7 @@ msgstr "SFTP根路径" #: audits/templates/audits/ftp_log_list.html:76 #: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 #: perms/models/asset_permission.py:82 -#: perms/models/database_app_permission.py:21 +#: perms/models/database_app_permission.py:22 #: perms/models/remote_app_permission.py:16 #: perms/templates/perms/asset_permission_asset.html:124 #: perms/templates/perms/asset_permission_list.html:37 @@ -1508,15 +1509,15 @@ msgstr "协议格式 {}/{}" msgid "Protocol duplicate: {}" msgstr "协议重复: {}" -#: assets/serializers/asset.py:94 +#: assets/serializers/asset.py:108 msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:95 orgs/mixins/serializers.py:27 +#: assets/serializers/asset.py:109 orgs/mixins/serializers.py:27 msgid "Org name" msgstr "组织名称" -#: assets/serializers/asset.py:134 assets/serializers/asset.py:171 +#: assets/serializers/asset.py:144 assets/serializers/asset.py:181 msgid "Connectivity" msgstr "连接" @@ -1538,7 +1539,7 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:470 users/templates/users/first_login.html:42 +#: users/models/user.py:492 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1779,8 +1780,8 @@ msgstr "获取认证信息错误" msgid "Close" msgstr "关闭" -#: assets/templates/assets/_asset_user_list.html:24 -#: audits/templates/audits/operate_log_list.html:75 +#: assets/templates/assets/_asset_user_list.html:24 audits/models.py:43 +#: audits/models.py:54 audits/templates/audits/operate_log_list.html:75 #: audits/templates/audits/password_change_log_list.html:57 #: ops/templates/ops/task_adhoc.html:61 #: terminal/templates/terminal/command_list.html:34 @@ -2503,6 +2504,24 @@ msgstr "文件名" msgid "Success" msgstr "成功" +#: audits/models.py:25 audits/templates/audits/ftp_log_list.html:81 +#: ops/models/command.py:28 ops/templates/ops/adhoc_history.html:50 +#: ops/templates/ops/adhoc_history_detail.html:59 +#: ops/templates/ops/command_execution_list.html:72 +#: ops/templates/ops/task_history.html:56 perms/models/base.py:52 +#: perms/templates/perms/asset_permission_detail.html:81 +#: perms/templates/perms/database_app_permission_detail.html:77 +#: perms/templates/perms/remote_app_permission_detail.html:73 +#: terminal/models.py:199 terminal/templates/terminal/session_detail.html:72 +#: terminal/templates/terminal/session_list.html:32 +#: xpack/plugins/change_auth_plan/models.py:176 +#: xpack/plugins/change_auth_plan/models.py:299 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 +#: xpack/plugins/gathered_user/models.py:76 +msgid "Date start" +msgstr "开始日期" + #: audits/models.py:33 #: authentication/templates/authentication/_access_key_modal.html:22 #: xpack/plugins/vault/templates/vault/vault.html:7 @@ -2526,7 +2545,7 @@ msgstr "修改者" msgid "Disabled" msgstr "禁用" -#: audits/models.py:72 settings/models.py:30 +#: audits/models.py:72 settings/models.py:31 #: users/templates/users/user_detail.html:82 msgid "Enabled" msgstr "启用" @@ -2559,14 +2578,14 @@ msgstr "Agent" #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 #: settings/forms/security.py:16 users/forms/profile.py:52 -#: users/models/user.py:462 users/templates/users/first_login.html:45 +#: users/models/user.py:484 users/templates/users/first_login.html:45 #: users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" #: audits/models.py:87 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:286 +#: xpack/plugins/change_auth_plan/models.py:295 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:217 msgid "Reason" @@ -2586,29 +2605,36 @@ msgstr "状态" msgid "Date login" msgstr "登录日期" -#: audits/templates/audits/ftp_log_list.html:81 -#: ops/templates/ops/adhoc_history.html:50 -#: ops/templates/ops/adhoc_history_detail.html:59 -#: ops/templates/ops/command_execution_list.html:72 -#: ops/templates/ops/task_history.html:56 perms/models/base.py:52 -#: perms/templates/perms/asset_permission_detail.html:81 -#: perms/templates/perms/database_app_permission_detail.html:77 -#: perms/templates/perms/remote_app_permission_detail.html:73 -#: terminal/models.py:199 terminal/templates/terminal/session_detail.html:72 -#: terminal/templates/terminal/session_list.html:32 -#: xpack/plugins/change_auth_plan/models.py:167 -#: xpack/plugins/change_auth_plan/models.py:290 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 -#: xpack/plugins/gathered_user/models.py:76 -msgid "Date start" -msgstr "开始日期" +#: audits/serializers.py:65 ops/models/command.py:24 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 +#: xpack/plugins/cloud/models.py:212 +msgid "Result" +msgstr "结果" + +#: audits/serializers.py:66 ops/models/adhoc.py:240 +#: ops/templates/ops/adhoc_history.html:54 +#: ops/templates/ops/task_history.html:60 +msgid "Is success" +msgstr "是否成功" + +#: audits/serializers.py:67 ops/templates/ops/adhoc_detail.html:51 +#: ops/templates/ops/command_execution_list.html:65 +#: ops/templates/ops/task_adhoc.html:57 ops/templates/ops/task_list.html:13 +#: terminal/forms/storage.py:151 +msgid "Hosts" +msgstr "主机" + +#: audits/serializers.py:68 ops/templates/ops/adhoc_detail.html:70 +#: ops/templates/ops/adhoc_detail.html:75 +#: ops/templates/ops/command_execution_list.html:68 +#: ops/templates/ops/task_adhoc.html:59 +msgid "Run as" +msgstr "运行用户" #: audits/templates/audits/login_log_list.html:34 #: perms/templates/perms/asset_permission_user.html:74 #: perms/templates/perms/database_app_permission_user.html:74 #: perms/templates/perms/remote_app_permission_user.html:83 -#: xpack/plugins/orgs/templates/orgs/org_users.html:67 msgid "Select user" msgstr "选择用户" @@ -2704,20 +2730,20 @@ msgstr "" msgid "User disabled." msgstr "用户已禁用" -#: authentication/backends/api.py:121 +#: authentication/backends/api.py:124 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication/backends/api.py:124 +#: authentication/backends/api.py:127 msgid "Invalid token header. Sign string should not contain spaces." msgstr "" -#: authentication/backends/api.py:131 +#: authentication/backends/api.py:134 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" -#: authentication/backends/api.py:142 +#: authentication/backends/api.py:145 msgid "Invalid token or cache refreshed." msgstr "" @@ -2833,7 +2859,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:360 users/templates/users/user_profile.html:94 +#: users/models/user.py:382 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 #: users/templates/users/user_verify_mfa.html:32 @@ -2841,7 +2867,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:361 users/templates/users/user_profile.html:92 +#: users/models/user.py:383 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2943,12 +2969,12 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: common/const.py:6 +#: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" msgstr "%(name)s 创建成功" -#: common/const.py:7 +#: common/const/__init__.py:7 #, python-format msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" @@ -3026,11 +3052,11 @@ msgstr "" "
Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
" -#: jumpserver/views/other.py:65 +#: jumpserver/views/other.py:73 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:73 +#: jumpserver/views/other.py:81 msgid "" "
Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
If you see this page, " @@ -3061,6 +3087,7 @@ msgid "Regularly perform" msgstr "定期执行" #: ops/mixin.py:108 ops/mixin.py:147 +#: xpack/plugins/change_auth_plan/serializers.py:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:79 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 @@ -3071,6 +3098,10 @@ msgstr "定期执行" msgid "Periodic perform" msgstr "定时执行" +#: ops/mixin.py:113 +msgid "Interval" +msgstr "间隔" + #: ops/mixin.py:122 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" @@ -3126,7 +3157,7 @@ msgstr "Become" #: ops/models/adhoc.py:150 users/templates/users/user_group_detail.html:54 #: xpack/plugins/cloud/templates/cloud/account_detail.html:59 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:55 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:51 msgid "Create by" msgstr "创建者" @@ -3148,26 +3179,22 @@ msgstr "完成时间" #: ops/models/adhoc.py:238 ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/task_history.html:61 ops/templates/ops/task_list.html:16 -#: xpack/plugins/change_auth_plan/models.py:170 -#: xpack/plugins/change_auth_plan/models.py:293 +#: xpack/plugins/change_auth_plan/models.py:179 +#: xpack/plugins/change_auth_plan/models.py:302 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 #: xpack/plugins/gathered_user/models.py:79 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:239 ops/templates/ops/adhoc_detail.html:104 +#: ops/models/adhoc.py:239 ops/models/command.py:26 +#: ops/templates/ops/adhoc_detail.html:104 #: ops/templates/ops/adhoc_history.html:53 #: ops/templates/ops/adhoc_history_detail.html:67 #: ops/templates/ops/task_detail.html:82 ops/templates/ops/task_history.html:59 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:240 ops/templates/ops/adhoc_history.html:54 -#: ops/templates/ops/task_history.html:60 -msgid "Is success" -msgstr "是否成功" - #: ops/models/adhoc.py:241 msgid "Adhoc raw result" msgstr "结果" @@ -3184,11 +3211,9 @@ msgstr "{} 任务开始: {}" msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/command.py:24 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 -#: xpack/plugins/cloud/models.py:212 -msgid "Result" -msgstr "结果" +#: ops/models/command.py:29 +msgid "Date finished" +msgstr "结束日期" #: ops/models/command.py:64 msgid "Task start" @@ -3220,21 +3245,8 @@ msgstr "版本详情" msgid "Version run execution" msgstr "执行历史" -#: ops/templates/ops/adhoc_detail.html:51 -#: ops/templates/ops/command_execution_list.html:65 -#: ops/templates/ops/task_adhoc.html:57 ops/templates/ops/task_list.html:13 -#: terminal/forms/storage.py:151 -msgid "Hosts" -msgstr "主机" - -#: ops/templates/ops/adhoc_detail.html:70 -#: ops/templates/ops/adhoc_detail.html:75 -#: ops/templates/ops/command_execution_list.html:68 -#: ops/templates/ops/task_adhoc.html:59 -msgid "Run as" -msgstr "运行用户" - #: ops/templates/ops/adhoc_detail.html:92 ops/templates/ops/task_list.html:12 +#: xpack/plugins/change_auth_plan/serializers.py:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:18 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:19 msgid "Run times" @@ -3364,7 +3376,7 @@ msgid "Pending" msgstr "等待" #: ops/templates/ops/command_execution_list.html:70 -#: xpack/plugins/change_auth_plan/models.py:257 +#: xpack/plugins/change_auth_plan/models.py:266 msgid "Finished" msgstr "结束" @@ -3471,7 +3483,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/templates/perms/database_app_permission_list.html:16 #: perms/templates/perms/remote_app_permission_list.html:16 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:446 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:468 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -3493,6 +3505,7 @@ msgid "Asset or group at least one required" msgstr "资产和节点至少选一个" #: perms/models/asset_permission.py:31 settings/forms/terminal.py:19 +#: settings/serializers/settings.py:56 msgid "All" msgstr "全部" @@ -3523,12 +3536,12 @@ msgstr "资产授权" #: perms/templates/perms/asset_permission_detail.html:85 #: perms/templates/perms/database_app_permission_detail.html:81 #: perms/templates/perms/remote_app_permission_detail.html:77 -#: users/models/user.py:478 users/templates/users/user_detail.html:93 +#: users/models/user.py:500 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" -#: perms/models/database_app_permission.py:26 +#: perms/models/database_app_permission.py:27 #: users/templates/users/_user_detail_nav_header.html:61 #: users/views/user.py:277 msgid "DatabaseApp permission" @@ -3579,9 +3592,8 @@ msgstr "添加资产" #: perms/templates/perms/remote_app_permission_user.html:120 #: users/templates/users/user_group_detail.html:87 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:76 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:89 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:123 -#: xpack/plugins/orgs/templates/orgs/org_users.html:73 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:88 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:125 msgid "Add" msgstr "添加" @@ -3652,7 +3664,7 @@ msgstr "刷新授权缓存" #: users/templates/users/user_database_app_permission.html:41 #: users/templates/users/user_list.html:19 #: users/templates/users/user_remote_app_permission.html:41 -#: xpack/plugins/cloud/models.py:50 +#: xpack/plugins/cloud/models.py:50 xpack/plugins/cloud/serializers.py:32 #: xpack/plugins/cloud/templates/cloud/account_detail.html:55 #: xpack/plugins/cloud/templates/cloud/account_list.html:14 msgid "Validity" @@ -3680,7 +3692,6 @@ msgstr "刷新成功" #: perms/templates/perms/asset_permission_user.html:31 #: perms/templates/perms/database_app_permission_user.html:31 #: perms/templates/perms/remote_app_permission_user.html:30 -#: xpack/plugins/orgs/templates/orgs/org_users.html:24 msgid "User list of " msgstr "用户列表" @@ -4101,7 +4112,7 @@ msgid "" "characters" msgstr "开启后,用户密码修改、重置必须包含特殊字符" -#: settings/forms/terminal.py:20 +#: settings/forms/terminal.py:20 settings/serializers/settings.py:57 msgid "Auto" msgstr "自动" @@ -4150,7 +4161,7 @@ msgid "ex: Last\\s*login|success|成功" msgstr "" "登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功 " -#: settings/models.py:95 users/templates/users/reset_password.html:29 +#: settings/models.py:96 users/templates/users/reset_password.html:29 #: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" @@ -4168,7 +4179,7 @@ msgid "Refresh cache" msgstr "刷新缓存" #: settings/templates/settings/_ldap_list_users_modal.html:33 -#: users/forms/profile.py:89 users/models/user.py:442 +#: users/forms/profile.py:89 users/models/user.py:464 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -4363,7 +4374,7 @@ msgstr "系统设置" msgid "Update setting successfully" msgstr "更新设置成功" -#: templates/_base_only_msg_content.html:28 +#: templates/_base_only_msg_content.html:28 xpack/plugins/interface/api.py:17 #: xpack/plugins/interface/models.py:36 msgid "Welcome to the JumpServer open source fortress" msgstr "欢迎使用JumpServer开源堡垒机" @@ -4841,7 +4852,7 @@ msgstr "风险等级" msgid "Container name" msgstr "容器名称" -#: terminal/forms/storage.py:44 +#: terminal/forms/storage.py:44 xpack/plugins/cloud/serializers.py:75 msgid "Account name" msgstr "账户名称" @@ -5312,7 +5323,7 @@ msgstr "工单列表" msgid "Ticket detail" msgstr "工单详情" -#: users/api/user.py:116 +#: users/api/user.py:113 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -5386,11 +5397,11 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:138 +#: users/serializers/user.py:167 users/serializers/user.py:287 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms/user.py:27 users/models/user.py:450 +#: users/forms/user.py:27 users/models/user.py:472 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -5398,7 +5409,7 @@ msgstr "ssh密钥不合法" msgid "Role" msgstr "角色" -#: users/forms/user.py:31 users/models/user.py:485 +#: users/forms/user.py:31 users/models/user.py:507 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -5418,15 +5429,17 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 +#: users/forms/user.py:124 users/serializers/user.py:28 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 +#: users/forms/user.py:125 users/serializers/user.py:29 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 xpack/plugins/change_auth_plan/models.py:59 +#: users/forms/user.py:132 users/serializers/user.py:36 +#: xpack/plugins/change_auth_plan/models.py:60 +#: xpack/plugins/change_auth_plan/serializers.py:30 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:45 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:67 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -5434,88 +5447,87 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:142 users/models/user.py:596 +#: users/models/user.py:154 users/models/user.py:618 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:145 xpack/plugins/orgs/forms.py:29 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:109 +#: users/models/user.py:157 xpack/plugins/orgs/forms.py:29 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:155 +#: users/models/user.py:167 msgid "Org admin" msgstr "组织管理员" -#: users/models/user.py:157 +#: users/models/user.py:169 msgid "Org auditor" msgstr "组织审计员" -#: users/models/user.py:362 users/templates/users/user_profile.html:90 +#: users/models/user.py:384 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:429 +#: users/models/user.py:451 msgid "Local" msgstr "数据库" -#: users/models/user.py:453 +#: users/models/user.py:475 msgid "Avatar" msgstr "头像" -#: users/models/user.py:456 users/templates/users/user_detail.html:68 +#: users/models/user.py:478 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:489 +#: users/models/user.py:511 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:599 +#: users/models/user.py:621 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/group.py:46 +#: users/serializers/group.py:50 msgid "Auditors cannot be join in the user group" msgstr "审计员不能被加入到用户组" -#: users/serializers/user.py:42 +#: users/serializers/user.py:67 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:43 +#: users/serializers/user.py:68 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:44 +#: users/serializers/user.py:69 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:45 +#: users/serializers/user.py:70 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:53 -msgid "Role limit to {}" -msgstr "角色只能为 {}" - -#: users/serializers/user.py:65 -msgid "Password does not match security rules" -msgstr "密码不满足安全规则" - -#: users/serializers/user.py:123 +#: users/serializers/user.py:74 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:124 +#: users/serializers/user.py:75 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:125 +#: users/serializers/user.py:76 msgid "Role name" msgstr "角色名" +#: users/serializers/user.py:95 +msgid "Role limit to {}" +msgstr "角色只能为 {}" + +#: users/serializers/user.py:107 users/serializers/user.py:253 +msgid "Password does not match security rules" +msgstr "密码不满足安全规则" + #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -5774,6 +5786,7 @@ msgid "User group detail" msgstr "用户组详情" #: users/templates/users/user_group_detail.html:81 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:116 msgid "Add user" msgstr "添加用户" @@ -6245,8 +6258,8 @@ msgstr "" "用户不存在,则创建用户。" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:87 -#: xpack/plugins/change_auth_plan/models.py:174 +#: xpack/plugins/change_auth_plan/models.py:88 +#: xpack/plugins/change_auth_plan/models.py:183 #: xpack/plugins/change_auth_plan/views.py:33 #: xpack/plugins/change_auth_plan/views.py:50 #: xpack/plugins/change_auth_plan/views.py:74 @@ -6257,69 +6270,69 @@ msgstr "" msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models.py:39 +#: xpack/plugins/change_auth_plan/models.py:40 msgid "Custom password" msgstr "自定义密码" -#: xpack/plugins/change_auth_plan/models.py:40 +#: xpack/plugins/change_auth_plan/models.py:41 msgid "All assets use the same random password" msgstr "所有资产使用相同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:41 +#: xpack/plugins/change_auth_plan/models.py:42 msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/models.py:64 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:72 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:178 +#: xpack/plugins/change_auth_plan/models.py:187 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:193 -#: xpack/plugins/change_auth_plan/models.py:279 +#: xpack/plugins/change_auth_plan/models.py:202 +#: xpack/plugins/change_auth_plan/models.py:288 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:252 +#: xpack/plugins/change_auth_plan/models.py:261 msgid "Ready" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:253 +#: xpack/plugins/change_auth_plan/models.py:262 msgid "Preflight check" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:254 +#: xpack/plugins/change_auth_plan/models.py:263 msgid "Change auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:255 +#: xpack/plugins/change_auth_plan/models.py:264 msgid "Verify auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:256 +#: xpack/plugins/change_auth_plan/models.py:265 msgid "Keep auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:283 +#: xpack/plugins/change_auth_plan/models.py:292 msgid "Step" msgstr "步骤" -#: xpack/plugins/change_auth_plan/models.py:300 +#: xpack/plugins/change_auth_plan/models.py:309 msgid "Change auth plan task" msgstr "改密计划任务" -#: xpack/plugins/change_auth_plan/serializers.py:68 +#: xpack/plugins/change_auth_plan/serializers.py:70 msgid "* Please enter custom password" msgstr "* 请输入自定义密码" -#: xpack/plugins/change_auth_plan/serializers.py:78 +#: xpack/plugins/change_auth_plan/serializers.py:80 msgid "* Please enter the correct password length" msgstr "* 请输入正确的密码长度" -#: xpack/plugins/change_auth_plan/serializers.py:81 +#: xpack/plugins/change_auth_plan/serializers.py:83 msgid "* Password length range 6-30 bits" msgstr "* 密码长度范围 6-30 位" @@ -6434,7 +6447,7 @@ msgstr "选择管理员" #: xpack/plugins/cloud/forms.py:85 msgid "Tips: The asset information is always covered" -msgstr "提示:资产信息总是被覆盖" +msgstr "" #: xpack/plugins/cloud/meta.py:9 xpack/plugins/cloud/views.py:27 #: xpack/plugins/cloud/views.py:44 xpack/plugins/cloud/views.py:62 @@ -6453,7 +6466,7 @@ msgstr "有效" msgid "Unavailable" msgstr "无效" -#: xpack/plugins/cloud/models.py:39 +#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/serializers.py:31 #: xpack/plugins/cloud/templates/cloud/account_detail.html:51 #: xpack/plugins/cloud/templates/cloud/account_list.html:13 msgid "Provider" @@ -6472,7 +6485,7 @@ msgstr "" msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:122 +#: xpack/plugins/cloud/models.py:122 xpack/plugins/cloud/serializers.py:55 msgid "Regions" msgstr "地域" @@ -6480,10 +6493,10 @@ msgstr "地域" msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:136 +#: xpack/plugins/cloud/models.py:136 xpack/plugins/cloud/serializers.py:77 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 msgid "Covered always" -msgstr "总是覆盖" +msgstr "总是被覆盖" #: xpack/plugins/cloud/models.py:142 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:104 @@ -6530,11 +6543,11 @@ msgstr "同步实例任务历史" msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/providers/aliyun.py:19 +#: xpack/plugins/cloud/providers/aliyun.py:16 msgid "Alibaba Cloud" msgstr "阿里云" -#: xpack/plugins/cloud/providers/aws.py:15 +#: xpack/plugins/cloud/providers/aws.py:14 msgid "AWS (International)" msgstr "AWS (国际)" @@ -6542,66 +6555,70 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/huaweicloud.py:17 +#: xpack/plugins/cloud/providers/huaweicloud.py:13 msgid "Huawei Cloud" msgstr "华为云" +#: xpack/plugins/cloud/providers/huaweicloud.py:16 +msgid "CN North-Beijing4" +msgstr "华北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:17 +msgid "CN East-Shanghai1" +msgstr "华东-上海1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:18 +msgid "CN East-Shanghai2" +msgstr "华东-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:19 +msgid "CN South-Guangzhou" +msgstr "华南-广州" + #: xpack/plugins/cloud/providers/huaweicloud.py:20 -msgid "AF-Johannesburg" -msgstr "非洲-约翰内斯堡" +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳1" #: xpack/plugins/cloud/providers/huaweicloud.py:21 -msgid "AP-Bangkok" -msgstr "亚太-曼谷" +#, fuzzy +#| msgid "AP-Hong Kong" +msgid "AP-Hong-Kong" +msgstr "亚太-香港" #: xpack/plugins/cloud/providers/huaweicloud.py:22 -msgid "AP-Hong Kong" -msgstr "亚太-香港" +msgid "AP-Bangkok" +msgstr "亚太-曼谷" #: xpack/plugins/cloud/providers/huaweicloud.py:23 msgid "AP-Singapore" msgstr "亚太-新加坡" #: xpack/plugins/cloud/providers/huaweicloud.py:24 -msgid "CN East-Shanghai1" -msgstr "华东-上海1" +msgid "AF-Johannesburg" +msgstr "非洲-约翰内斯堡" #: xpack/plugins/cloud/providers/huaweicloud.py:25 -msgid "CN East-Shanghai2" -msgstr "华东-上海2" - -#: xpack/plugins/cloud/providers/huaweicloud.py:26 -msgid "CN North-Beijing1" -msgstr "华北-北京1" - -#: xpack/plugins/cloud/providers/huaweicloud.py:27 -msgid "CN North-Beijing4" -msgstr "华北-北京4" - -#: xpack/plugins/cloud/providers/huaweicloud.py:28 -msgid "CN Northeast-Dalian" -msgstr "东北-大连" - -#: xpack/plugins/cloud/providers/huaweicloud.py:29 -msgid "CN South-Guangzhou" -msgstr "华南-广州" - -#: xpack/plugins/cloud/providers/huaweicloud.py:30 -msgid "CN Southwest-Guiyang1" -msgstr "西南-贵阳一" - -#: xpack/plugins/cloud/providers/huaweicloud.py:31 -msgid "EU-Paris" -msgstr "欧洲-巴黎" - -#: xpack/plugins/cloud/providers/huaweicloud.py:32 msgid "LA-Santiago" msgstr "拉美-圣地亚哥" -#: xpack/plugins/cloud/providers/qcloud.py:17 +#: xpack/plugins/cloud/providers/qcloud.py:14 msgid "Tencent Cloud" msgstr "腾讯云" +#: xpack/plugins/cloud/serializers.py:53 +msgid "History count" +msgstr "用户数量" + +#: xpack/plugins/cloud/serializers.py:54 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:15 +msgid "Instance count" +msgstr "实例个数" + +#: xpack/plugins/cloud/serializers.py:76 +#: xpack/plugins/gathered_user/serializers.py:20 +msgid "Periodic display" +msgstr "定时执行" + #: xpack/plugins/cloud/templates/cloud/account_detail.html:17 #: xpack/plugins/cloud/views.py:79 msgid "Account detail" @@ -6678,10 +6695,6 @@ msgstr "创建同步实例任务" msgid "Run count" msgstr "执行次数" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:15 -msgid "Instance count" -msgstr "实例个数" - #: xpack/plugins/cloud/utils.py:38 msgid "Account unavailable" msgstr "账户无效" @@ -6727,6 +6740,10 @@ msgstr "收集用户执行" msgid "Assets is empty, please change nodes" msgstr "资产为空,请更改节点" +#: xpack/plugins/gathered_user/serializers.py:21 +msgid "Executed times" +msgstr "执行次数" + #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:170 msgid "Asset user" msgstr "资产用户" @@ -6828,6 +6845,14 @@ msgstr "恢复默认失败!" msgid "It is already in the default setting state!" msgstr "当前已经是初始化状态!" +#: xpack/plugins/license/api.py:46 xpack/plugins/license/views.py:47 +msgid "License import successfully" +msgstr "许可证导入成功" + +#: xpack/plugins/license/api.py:47 xpack/plugins/license/views.py:49 +msgid "License is invalid" +msgstr "无效的许可证" + #: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:94 #: xpack/plugins/license/templates/license/license_detail.html:41 #: xpack/plugins/license/templates/license/license_detail.html:46 @@ -6906,73 +6931,47 @@ msgstr "技术咨询" msgid "Consult" msgstr "咨询" -#: xpack/plugins/license/views.py:47 -msgid "License import successfully" -msgstr "许可证导入成功" - -#: xpack/plugins/license/views.py:49 -msgid "License is invalid" -msgstr "无效的许可证" - #: xpack/plugins/orgs/forms.py:23 msgid "Select auditor" msgstr "选择审计员" #: xpack/plugins/orgs/forms.py:28 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:75 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:71 #: xpack/plugins/orgs/templates/orgs/org_list.html:13 msgid "Admin" msgstr "管理员" -#: xpack/plugins/orgs/meta.py:8 xpack/plugins/orgs/views.py:27 -#: xpack/plugins/orgs/views.py:44 xpack/plugins/orgs/views.py:62 -#: xpack/plugins/orgs/views.py:85 xpack/plugins/orgs/views.py:116 +#: xpack/plugins/orgs/meta.py:8 xpack/plugins/orgs/views.py:26 +#: xpack/plugins/orgs/views.py:43 xpack/plugins/orgs/views.py:61 +#: xpack/plugins/orgs/views.py:79 msgid "Organizations" msgstr "组织管理" #: xpack/plugins/orgs/templates/orgs/org_detail.html:17 -#: xpack/plugins/orgs/templates/orgs/org_users.html:13 -#: xpack/plugins/orgs/views.py:86 +#: xpack/plugins/orgs/views.py:80 msgid "Org detail" msgstr "组织详情" -#: xpack/plugins/orgs/templates/orgs/org_detail.html:20 -#: xpack/plugins/orgs/templates/orgs/org_users.html:16 -msgid "Org users" -msgstr "组织用户" - -#: xpack/plugins/orgs/templates/orgs/org_detail.html:83 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:79 msgid "Add admin" msgstr "添加管理员" -#: xpack/plugins/orgs/templates/orgs/org_detail.html:117 -msgid "Add auditor" -msgstr "添加审计员" - #: xpack/plugins/orgs/templates/orgs/org_list.html:5 msgid "Create organization " msgstr "创建组织" -#: xpack/plugins/orgs/templates/orgs/org_users.html:59 -msgid "Add user to organization" -msgstr "添加用户" - -#: xpack/plugins/orgs/views.py:28 +#: xpack/plugins/orgs/views.py:27 msgid "Org list" msgstr "组织列表" -#: xpack/plugins/orgs/views.py:45 +#: xpack/plugins/orgs/views.py:44 msgid "Create org" msgstr "创建组织" -#: xpack/plugins/orgs/views.py:63 +#: xpack/plugins/orgs/views.py:62 msgid "Update org" msgstr "更新组织" -#: xpack/plugins/orgs/views.py:117 -msgid "Org user list" -msgstr "组织用户列表" - #: xpack/plugins/vault/meta.py:11 xpack/plugins/vault/views.py:23 #: xpack/plugins/vault/views.py:38 msgid "Vault" @@ -6994,6 +6993,27 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "CN North-Beijing1" +#~ msgstr "华北-北京1" + +#~ msgid "CN Northeast-Dalian" +#~ msgstr "华北-大连" + +#~ msgid "EU-Paris" +#~ msgstr "欧洲-巴黎" + +#~ msgid "Org users" +#~ msgstr "组织用户" + +#~ msgid "Add auditor" +#~ msgstr "添加审计员" + +#~ msgid "Add user to organization" +#~ msgstr "添加用户" + +#~ msgid "Org user list" +#~ msgstr "组织用户列表" + #~ msgid "Total hosts" #~ msgstr "主机总数" @@ -7006,9 +7026,6 @@ msgstr "创建" #~ msgid "Region & Instance" #~ msgstr "地域 & 实例" -#~ msgid "Interval" -#~ msgstr "间隔" - #~ msgid "Crontab" #~ msgstr "Crontab" @@ -7024,9 +7041,6 @@ msgstr "创建" #~ msgid "History detail of" #~ msgstr "执行历史详情" -#~ msgid "History of " -#~ msgstr "执行历史" - #~ msgid "Assets count: {}" #~ msgstr "资产数量" @@ -7486,9 +7500,6 @@ msgstr "创建" #~ msgid "Update assets hardware info period" #~ msgstr "定期更新资产硬件信息" -#~ msgid "Date finished" -#~ msgstr "结束日期" - #~ msgid "User id" #~ msgstr "用户" diff --git a/apps/ops/migrations/0018_auto_20200509_1434.py b/apps/ops/migrations/0018_auto_20200509_1434.py new file mode 100644 index 000000000..5bbf87610 --- /dev/null +++ b/apps/ops/migrations/0018_auto_20200509_1434.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.10 on 2020-05-09 06:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0017_auto_20200306_1747'), + ] + + operations = [ + migrations.AlterField( + model_name='commandexecution', + name='date_created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='commandexecution', + name='date_finished', + field=models.DateTimeField(null=True, verbose_name='Date finished'), + ), + migrations.AlterField( + model_name='commandexecution', + name='date_start', + field=models.DateTimeField(null=True, verbose_name='Date start'), + ), + migrations.AlterField( + model_name='commandexecution', + name='is_finished', + field=models.BooleanField(default=False, verbose_name='Is finished'), + ), + ] diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index d3d397220..fd6f9cf27 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -110,7 +110,7 @@ class PeriodTaskSerializerMixin(serializers.Serializer): max_length=128, allow_blank=True, allow_null=True, required=False, label=_('Regularly perform') ) - interval = serializers.IntegerField(allow_null=True, required=False) + interval = serializers.IntegerField(allow_null=True, required=False, label=_('Interval')) INTERVAL_MAX = 65535 INTERVAL_MIN = 1 diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 662131bc5..fb6642f85 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -23,10 +23,10 @@ class CommandExecution(OrgModelMixin): command = models.TextField(verbose_name=_("Command")) _result = models.TextField(blank=True, null=True, verbose_name=_('Result')) user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True) - is_finished = models.BooleanField(default=False) - date_created = models.DateTimeField(auto_now_add=True) - date_start = models.DateTimeField(null=True) - date_finished = models.DateTimeField(null=True) + is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + date_start = models.DateTimeField(null=True, verbose_name=_('Date start')) + date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) def __str__(self): return self.command[:10] diff --git a/apps/orgs/api.py b/apps/orgs/api.py index 63eebb25d..466ac7254 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -10,7 +10,7 @@ from common.permissions import IsSuperUserOrAppUser from .models import Organization from .serializers import OrgSerializer, OrgReadSerializer, \ OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \ - OrgAllUserSerializer + OrgAllUserSerializer, OrgRetrieveSerializer from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission @@ -28,10 +28,11 @@ class OrgViewSet(BulkModelViewSet): org = None def get_serializer_class(self): - if self.action in ('list', 'retrieve'): - return OrgReadSerializer - else: - return super().get_serializer_class() + mapper = { + 'list': OrgReadSerializer, + 'retrieve': OrgRetrieveSerializer + } + return mapper.get(self.action, super().get_serializer_class()) def get_data_from_model(self, model): if model == User: diff --git a/apps/orgs/middleware.py b/apps/orgs/middleware.py index 3e491d3d2..efbee2dde 100644 --- a/apps/orgs/middleware.py +++ b/apps/orgs/middleware.py @@ -34,8 +34,7 @@ class OrgMiddleware: def __call__(self, request): self.set_permed_org_if_need(request) org = get_org_from_request(request) - if org is not None: - request.current_org = org - set_current_org(org) + request.current_org = org + set_current_org(org) response = self.get_response(request) return response diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 7695292c3..2ad34831c 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # from django.shortcuts import get_object_or_404 -from rest_framework.viewsets import ModelViewSet +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 @@ -44,6 +45,10 @@ class OrgModelViewSet(CommonApiMixin, OrgQuerySetMixin, ModelViewSet): pass +class OrgGenericViewSet(CommonApiMixin, OrgQuerySetMixin, GenericViewSet): + pass + + class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet): def allow_bulk_destroy(self, qs, filtered): qs_count = qs.count() @@ -76,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/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index a34f8d9b1..2b415e31b 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from common.validators import ProjectUniqueValidator -from common.mixins import BulkSerializerMixin +from common.mixins import BulkSerializerMixin, CommonSerializerMixin from ..utils import get_current_org_id_for_serializer @@ -16,7 +16,7 @@ __all__ = [ ] -class OrgResourceSerializerMixin(serializers.Serializer): +class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): """ 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id (同时为serializer.is_valid()对Model的unique_together校验做准备) diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 5ff3b5f4d..ca5bdddba 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -1,8 +1,7 @@ -import re from rest_framework.serializers import ModelSerializer from rest_framework import serializers -from users.models import User, UserGroup +from users.models import UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission from common.serializers import AdaptedBulkListSerializer @@ -92,3 +91,12 @@ class OrgAllUserSerializer(serializers.Serializer): @staticmethod def get_user_display(obj): return str(obj) + + +class OrgRetrieveSerializer(OrgReadSerializer): + admins = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + auditors = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + users = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + + class Meta(OrgReadSerializer.Meta): + pass diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index d5ea4ca30..6870942da 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -24,7 +24,7 @@ def get_org_from_request(request): oid = Organization.DEFAULT_ID elif oid.lower() == "root": oid = Organization.ROOT_ID - org = Organization.get_instance(oid) + org = Organization.get_instance(oid, True) return org 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/asset_permission.py b/apps/perms/api/asset_permission.py index ff477f5af..18061f236 100644 --- a/apps/perms/api/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -22,10 +22,7 @@ class AssetPermissionViewSet(OrgModelViewSet): 资产授权列表的增删改查api """ model = AssetPermission - serializer_classes = { - 'default': serializers.AssetPermissionCreateUpdateSerializer, - 'display': serializers.AssetPermissionListSerializer - } + serializer_class = serializers.AssetPermissionSerializer filter_fields = ['name'] permission_classes = (IsOrgAdmin,) 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 b7fa6de19..cb1998675 100644 --- a/apps/perms/api/remote_app_permission.py +++ b/apps/perms/api/remote_app_permission.py @@ -11,10 +11,8 @@ from ..serializers import ( RemoteAppPermissionSerializer, RemoteAppPermissionUpdateUserSerializer, RemoteAppPermissionUpdateRemoteAppSerializer, - RemoteAppPermissionListSerializer, ) - __all__ = [ 'RemoteAppPermissionViewSet', 'RemoteAppPermissionAddUserApi', 'RemoteAppPermissionAddRemoteAppApi', @@ -26,10 +24,7 @@ class RemoteAppPermissionViewSet(OrgModelViewSet): model = RemoteAppPermission filter_fields = ('name', ) search_fields = filter_fields - serializer_classes = { - 'default': RemoteAppPermissionSerializer, - 'display': RemoteAppPermissionListSerializer, - } + serializer_class = RemoteAppPermissionSerializer permission_classes = (IsOrgAdmin,) 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/api/user_database_app_permission.py b/apps/perms/api/user_database_app_permission.py index 3a973b8c1..19885d2ef 100644 --- a/apps/perms/api/user_database_app_permission.py +++ b/apps/perms/api/user_database_app_permission.py @@ -26,8 +26,8 @@ __all__ = [ class UserGrantedDatabaseAppsApi(generics.ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = DatabaseAppSerializer - filter_fields = ['id', 'name'] - search_fields = ['name'] + filter_fields = ['id', 'name', 'type', 'comment'] + search_fields = ['name', 'comment'] def get_object(self): user_id = self.kwargs.get('pk', '') diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index 6af3bb6ba..17900a6bc 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -5,7 +5,7 @@ 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, RetrieveAPIView + ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView ) from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin @@ -25,6 +25,7 @@ __all__ = [ 'UserGrantedAssetSystemUsersApi', 'ValidateUserAssetPermissionApi', 'GetUserAssetPermissionActionsApi', + 'UserAssetPermissionsCacheApi', ] @@ -117,3 +118,10 @@ class UserGrantedAssetSystemUsersApi(UserAssetPermissionMixin, ListAPIView): system_user.actions = actions return system_users + +class UserAssetPermissionsCacheApi(UserAssetPermissionMixin, DestroyAPIView): + permission_classes = (IsOrgAdmin,) + + def destroy(self, request, *args, **kwargs): + self.util.expire_user_tree_cache() + return Response(status=204) diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 4b0cc32a5..426f7d34d 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -2,6 +2,7 @@ # from common.utils import lazyproperty from common.tree import TreeNodeSerializer +from django.db.models import QuerySet from ..mixin import UserPermissionMixin from ...utils import AssetPermissionUtil, ParserNode from ...hands import Node, Asset @@ -32,7 +33,8 @@ class UserNodeTreeMixin: nodes_only_fields = ParserNode.nodes_only_fields def parse_nodes_to_queryset(self, nodes): - nodes = nodes.only(*self.nodes_only_fields) + if isinstance(nodes, QuerySet): + nodes = nodes.only(*self.nodes_only_fields) _queryset = [] for node in nodes: diff --git a/apps/perms/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py index 51a9217ce..8dca299a3 100644 --- a/apps/perms/api/user_remote_app_permission.py +++ b/apps/perms/api/user_remote_app_permission.py @@ -26,8 +26,8 @@ __all__ = [ class UserGrantedRemoteAppsApi(generics.ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = RemoteAppSerializer - filter_fields = ['name', 'id'] - search_fields = ['name'] + filter_fields = ['name', 'id', 'type', 'comment'] + search_fields = ['name', 'comment'] def get_object(self): user_id = self.kwargs.get('pk', '') diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 1d92b9852..8552edc74 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -3,9 +3,9 @@ import logging from functools import reduce 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 orgs.models import Organization from orgs.utils import get_current_org from assets.models import Asset, SystemUser, Node @@ -87,6 +87,18 @@ class AssetPermission(BasePermission): verbose_name = _("Asset permission") ordering = ('name',) + @lazyproperty + def assets_amount(self): + return self.assets.count() + + @lazyproperty + def nodes_amount(self): + return self.nodes.count() + + @lazyproperty + def system_users_amount(self): + return self.system_users.count() + @classmethod def get_queryset_with_prefetch(cls): return cls.objects.all().valid().prefetch_related( diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index da40ced9d..4ad52b2ce 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -8,7 +8,7 @@ from django.db.models import Q from django.utils import timezone from orgs.mixins.models import OrgModelMixin -from common.utils import date_expired_default +from common.utils import date_expired_default, lazyproperty from orgs.mixins.models import OrgManager @@ -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) @@ -87,3 +104,11 @@ class BasePermission(OrgModelMixin): Q(id__in=users_id) | Q(groups__id__in=groups_id) ).distinct() return users + + @lazyproperty + def users_amount(self): + return self.users.count() + + @lazyproperty + def user_groups_amount(self): + return self.user_groups.count() 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 57a806b80..40114875c 100644 --- a/apps/perms/models/remote_app_permission.py +++ b/apps/perms/models/remote_app_permission.py @@ -1,9 +1,9 @@ # coding: utf-8 # - from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.utils import lazyproperty from .base import BasePermission __all__ = [ @@ -22,3 +22,15 @@ 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() + + @lazyproperty + def system_users_amount(self): + return self.system_users.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/asset_permission.py b/apps/perms/serializers/asset_permission.py index 73612a7e6..a256a7a3c 100644 --- a/apps/perms/serializers/asset_permission.py +++ b/apps/perms/serializers/asset_permission.py @@ -3,12 +3,12 @@ from rest_framework import serializers -from common.fields import StringManyToManyField +from django.db.models import Count from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action __all__ = [ - 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', + 'AssetPermissionSerializer', 'ActionsField', ] @@ -34,27 +34,31 @@ class ActionsDisplayField(ActionsField): return [choices.get(i) for i in values] -class AssetPermissionCreateUpdateSerializer(BulkOrgResourceModelSerializer): +class AssetPermissionSerializer(BulkOrgResourceModelSerializer): actions = ActionsField(required=False, allow_null=True) + is_valid = serializers.BooleanField(read_only=True) + is_expired = serializers.BooleanField(read_only=True) class Meta: model = AssetPermission - exclude = ('created_by', 'date_created') - - -class AssetPermissionListSerializer(BulkOrgResourceModelSerializer): - users = StringManyToManyField(many=True, read_only=True) - user_groups = StringManyToManyField(many=True, read_only=True) - assets = StringManyToManyField(many=True, read_only=True) - nodes = StringManyToManyField(many=True, read_only=True) - system_users = StringManyToManyField(many=True, read_only=True) - actions = ActionsDisplayField() - is_valid = serializers.BooleanField() - is_expired = serializers.BooleanField() - - class Meta: - model = AssetPermission - fields = '__all__' - - + mini_fields = ['id', 'name'] + small_fields = mini_fields + [ + 'is_active', 'is_expired', 'is_valid', 'actions', 'created_by', 'date_created', + 'date_expired', 'date_start', 'comment' + ] + m2m_fields = [ + 'users', 'user_groups', 'assets', 'nodes', 'system_users', + 'users_amount', 'user_groups_amount', 'assets_amount', 'nodes_amount', 'system_users_amount', + ] + fields = small_fields + m2m_fields + read_only_fields = ['created_by', 'date_created'] + @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), + assets_amount=Count('assets', distinct=True), nodes_amount=Count('nodes', distinct=True), + system_users_amount=Count('system_users', distinct=True) + ) + return queryset 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..0442a6122 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 @@ -13,27 +13,41 @@ __all__ = [ ] -class DatabaseAppPermissionSerializer(BulkOrgResourceModelSerializer): +class AmountMixin: + @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 + + +class DatabaseAppPermissionSerializer(AmountMixin, BulkOrgResourceModelSerializer): class Meta: model = models.DatabaseAppPermission list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'users', 'user_groups', - 'database_apps', 'system_users', 'comment', 'is_active', - 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created' + 'id', 'name', 'users', 'user_groups', 'database_apps', 'system_users', + 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', + 'created_by', 'date_created', 'users_amount', 'user_groups_amount', + 'database_apps_amount', 'system_users_amount', + ] + read_only_fields = [ + 'created_by', 'date_created', 'users_amount', 'user_groups_amount', + 'database_apps_amount', 'system_users_amount', ] - read_only_fields = ['created_by', 'date_created'] -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() +class DatabaseAppPermissionListSerializer(AmountMixin, BulkOrgResourceModelSerializer): 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' + ] 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 41c5d7022..a0bd7c410 100644 --- a/apps/perms/serializers/remote_app_permission.py +++ b/apps/perms/serializers/remote_app_permission.py @@ -1,9 +1,8 @@ # coding: utf-8 # - from rest_framework import serializers +from django.db.models import Count -from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import RemoteAppPermission @@ -13,7 +12,6 @@ __all__ = [ 'RemoteAppPermissionSerializer', 'RemoteAppPermissionUpdateUserSerializer', 'RemoteAppPermissionUpdateRemoteAppSerializer', - 'RemoteAppPermissionListSerializer', ] @@ -21,25 +19,27 @@ class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = RemoteAppPermission list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'users', 'user_groups', 'remote_apps', 'system_users', + mini_fields = ['id', 'name'] + small_fields = mini_fields + [ 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created', + 'created_by', 'date_created' ] + m2m_fields = [ + 'users', 'user_groups', 'remote_apps', 'system_users', + 'users_amount', 'user_groups_amount', 'remote_apps_amount', + 'system_users_amount' + ] + fields = small_fields + m2m_fields read_only_fields = ['created_by', 'date_created'] - -class RemoteAppPermissionListSerializer(BulkOrgResourceModelSerializer): - users = StringManyToManyField(many=True, read_only=True) - user_groups = StringManyToManyField(many=True, read_only=True) - remote_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 = RemoteAppPermission - fields = '__all__' + @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), + remote_apps_amount=Count('remote_apps', distinct=True), system_users_amount=Count('system_users', distinct=True) + ) + return queryset class RemoteAppPermissionUpdateUserSerializer(serializers.ModelSerializer): 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/tests.py b/apps/perms/tests.py index 4fd76b0f2..344266b19 100644 --- a/apps/perms/tests.py +++ b/apps/perms/tests.py @@ -1,4 +1,3 @@ from django.test import TestCase from django.contrib.sessions.backends import file, db, cache -from django.contrib.auth.views import login \ No newline at end of file diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index c14db7c3e..ff8ac0c48 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -51,6 +51,11 @@ user_permission_urlpatterns = [ # Asset System users path('/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'), path('assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), + + # Expire user permission cache + path('/asset-permissions/cache/', api.UserAssetPermissionsCacheApi.as_view(), + name='user-asset-permission-cache'), + path('asset-permissions/cache/', api.UserAssetPermissionsCacheApi.as_view(), name='my-asset-permission-cache'), ] user_group_permission_urlpatterns = [ 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 - diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 80b1c19e5..1f3a3e94b 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -290,11 +290,12 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): def parse_user_tree_to_full_tree(self, user_tree): """ 经过前面两个动作,用户授权的节点已放到树上,但是树不是完整的, - 这里要讲树构造成一个完整的树 + 这里要将树构造成一个完整的树 """ # 开始修正user_tree,保证父节点都在树上 root_children = user_tree.children('') for child in root_children: + # print("child: {}".format(child.identifier)) if child.identifier.isdigit(): continue if child.identifier.startswith('-'): @@ -302,6 +303,7 @@ class AssetPermissionUtil(AssetPermissionUtilCacheMixin): ancestors = self.full_tree.ancestors( child.identifier, with_self=False, deep=True, ) + # print("Get ancestors: {}".format(len(ancestors))) if not ancestors: continue user_tree.safe_add_ancestors(child, ancestors) diff --git a/apps/settings/api.py b/apps/settings/api.py index 38167a7e3..fb3740c1f 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -2,28 +2,28 @@ # import json - +from collections.abc import Iterable from smtplib import SMTPSenderRefused from rest_framework import generics from rest_framework.views import Response, APIView from django.conf import settings from django.core.mail import send_mail, get_connection from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers from .utils import ( LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil, - LDAP_USE_CACHE_FLAGS, LDAPTestUtil, + LDAP_USE_CACHE_FLAGS, LDAPTestUtil, ObjectDict ) from .tasks import sync_ldap_user_task from common.permissions import IsOrgAdmin, IsSuperUser from common.utils import get_logger from .serializers import ( MailTestSerializer, LDAPTestConfigSerializer, LDAPUserSerializer, - PublicSettingSerializer, LDAPTestLoginSerializer, + PublicSettingSerializer, LDAPTestLoginSerializer, SettingsSerializer ) from users.models import User - logger = get_logger(__file__) @@ -59,7 +59,7 @@ class MailTestingAPI(APIView): use_tls=email_use_tls, use_ssl=email_use_ssl, ) send_mail( - subject, message, email_from, [email_recipient], + subject, message, email_from, [email_recipient], connection=connection ) except SMTPSenderRefused as e: @@ -72,13 +72,13 @@ class MailTestingAPI(APIView): continue else: break - return Response({"error": str(resp)}, status=401) + return Response({"error": str(resp)}, status=400) except Exception as e: print(e) - return Response({"error": str(e)}, status=401) + return Response({"error": str(e)}, status=400) return Response({"msg": self.success_message.format(email_recipient)}) else: - return Response({"error": str(serializer.errors)}, status=401) + return Response({"error": str(serializer.errors)}, status=400) class LDAPTestingConfigAPI(APIView): @@ -88,10 +88,10 @@ class LDAPTestingConfigAPI(APIView): def post(self, request): serializer = self.serializer_class(data=request.data) if not serializer.is_valid(): - return Response({"error": str(serializer.errors)}, status=401) + return Response({"error": str(serializer.errors)}, status=400) config = self.get_ldap_config(serializer) ok, msg = LDAPTestUtil(config).test_config() - status = 200 if ok else 401 + status = 200 if ok else 400 return Response(msg, status=status) @staticmethod @@ -124,11 +124,11 @@ class LDAPTestingLoginAPI(APIView): def post(self, request): serializer = self.serializer_class(data=request.data) if not serializer.is_valid(): - return Response({"error": str(serializer.errors)}, status=401) + return Response({"error": str(serializer.errors)}, status=400) username = serializer.validated_data['username'] password = serializer.validated_data['password'] ok, msg = LDAPTestUtil().test_login(username, password) - status = 200 if ok else 401 + status = 200 if ok else 400 return Response(msg, status=status) @@ -236,14 +236,14 @@ class LDAPUserImportAPI(APIView): try: users = self.get_ldap_users() except Exception as e: - return Response({'error': str(e)}, status=401) + return Response({'error': str(e)}, status=400) if users is None: - return Response({'msg': _('Get ldap users is None')}, status=401) + return Response({'msg': _('Get ldap users is None')}, status=400) errors = LDAPImportUtil().perform_import(users) if errors: - return Response({'errors': errors}, status=401) + return Response({'errors': errors}, status=400) count = users if users is None else len(users) return Response({'msg': _('Imported {} users successfully').format(count)}) @@ -270,8 +270,34 @@ class PublicSettingApi(generics.RetrieveAPIView): "data": { "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME, + "XPACK_ENABLED": settings.XPACK_ENABLED, + "XPACK_LICENSE_IS_VALID": settings.XPACK_LICENSE_IS_VALID } } return instance +class SettingsApi(generics.RetrieveUpdateAPIView): + permission_classes = (IsSuperUser,) + serializer_class = SettingsSerializer + + def get_object(self): + instance = {category: self._get_setting_fields_obj(list(category_serializer.get_fields())) + for category, category_serializer in self.serializer_class().get_fields().items() + if isinstance(category_serializer, serializers.Serializer)} + return ObjectDict(instance) + + def perform_update(self, serializer): + serializer.save() + + def _get_setting_fields_obj(self, category_fields): + if isinstance(category_fields, Iterable): + fields_data = {field_name: getattr(settings, field_name) + for field_name in category_fields} + return ObjectDict(fields_data) + + if isinstance(category_fields, str): + fields_data = {category_fields: getattr(settings, category_fields)} + return ObjectDict(fields_data) + + return ObjectDict() diff --git a/apps/settings/models.py b/apps/settings/models.py index 75b56bb54..30732a00c 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -10,7 +10,8 @@ from common.utils import signer class SettingQuerySet(models.QuerySet): def __getattr__(self, item): - instances = self.filter(name=item) + queryset = list(self) + instances = [i for i in queryset if i.name == item] if len(instances) == 1: return instances[0] else: diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py index 045763364..5868a76df 100644 --- a/apps/settings/serializers/__init__.py +++ b/apps/settings/serializers/__init__.py @@ -4,3 +4,4 @@ from .email import * from .ldap import * from .public import * +from .settings import * diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py new file mode 100644 index 000000000..c5baa67da --- /dev/null +++ b/apps/settings/serializers/settings.py @@ -0,0 +1,124 @@ +# coding: utf-8 + +from django.db import transaction +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from ..models import Setting + +__all__ = ['SettingsSerializer'] + + +class BasicSettingSerializer(serializers.Serializer): + SITE_URL = serializers.URLField(required=True) + USER_GUIDE_URL = serializers.URLField(required=False, allow_blank=True, ) + EMAIL_SUBJECT_PREFIX = serializers.CharField(max_length=1024, required=True) + + +class EmailSettingSerializer(serializers.Serializer): + encrypt_fields = ["EMAIL_HOST_PASSWORD", ] + + EMAIL_HOST = serializers.CharField(max_length=1024, required=True) + EMAIL_PORT = serializers.CharField(max_length=5, required=True) + EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True) + EMAIL_HOST_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False, ) + EMAIL_FROM = serializers.CharField(max_length=128, allow_blank=True, required=False) + EMAIL_RECIPIENT = serializers.CharField(max_length=128, allow_blank=True, required=False) + EMAIL_USE_SSL = serializers.BooleanField(required=False) + EMAIL_USE_TLS = serializers.BooleanField(required=False) + + +class EmailContentSettingSerializer(serializers.Serializer): + EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField(max_length=1024, allow_blank=True, required=False, ) + EMAIL_CUSTOM_USER_CREATED_HONORIFIC = serializers.CharField(max_length=1024, allow_blank=True, required=False, ) + EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField(max_length=4096, allow_blank=True, required=False) + EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField(max_length=512, allow_blank=True, required=False) + + +class LdapSettingSerializer(serializers.Serializer): + encrypt_fields = ["AUTH_LDAP_BIND_PASSWORD", ] + + AUTH_LDAP_SERVER_URI = serializers.CharField(required=True) + AUTH_LDAP_BIND_DN = serializers.CharField(required=False) + AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False) + AUTH_LDAP_SEARCH_OU = serializers.CharField(max_length=1024, allow_blank=True, required=False) + AUTH_LDAP_SEARCH_FILTER = serializers.CharField(max_length=1024, required=True) + AUTH_LDAP_USER_ATTR_MAP = serializers.DictField(required=True) + AUTH_LDAP = serializers.BooleanField(required=False) + + +class TerminalSettingSerializer(serializers.Serializer): + SORT_BY_CHOICES = ( + ('hostname', _('Hostname')), + ('ip', _('IP')) + ) + + PAGE_SIZE_CHOICES = ( + ('all', _('All')), + ('auto', _('Auto')), + (10, 10), + (15, 15), + (25, 25), + (50, 50), + ) + TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False) + TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False) + TERMINAL_HEARTBEAT_INTERVAL = serializers.IntegerField(min_value=5, max_value=99999, required=True) + TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False) + TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False) + TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(min_value=1, max_value=99999, required=True) + TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, required=False) + + +class SecuritySettingSerializer(serializers.Serializer): + SECURITY_MFA_AUTH = serializers.BooleanField(required=False) + SECURITY_COMMAND_EXECUTION = serializers.BooleanField(required=False) + SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(required=True) + SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(min_value=3, max_value=99999, required=True) + SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(min_value=5, max_value=99999, required=True) + SECURITY_MAX_IDLE_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=False) + SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=True) + SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(min_value=6, max_value=30, required=True) + SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(required=False) + SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False) + SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False) + SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False) + + +class SettingsSerializer(serializers.Serializer): + basic = BasicSettingSerializer(required=False) + email = EmailSettingSerializer(required=False) + email_content = EmailContentSettingSerializer(required=False) + ldap = LdapSettingSerializer(required=False) + terminal = TerminalSettingSerializer(required=False) + security = SecuritySettingSerializer(required=False) + + encrypt_fields = ["EMAIL_HOST_PASSWORD", "AUTH_LDAP_BIND_PASSWORD"] + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + for category, category_data in validated_data.items(): + if not category_data: + continue + self.update_validated_settings(category_data) + for field_name, field_value in category_data.items(): + setattr(getattr(instance, category), field_name, field_value) + + return instance + + def update_validated_settings(self, validated_data, category='default'): + if not validated_data: + return + with transaction.atomic(): + for field_name, field_value in validated_data.items(): + try: + setting = Setting.objects.get(name=field_name) + except Setting.DoesNotExist: + setting = Setting() + encrypted = True if field_name in self.encrypt_fields else False + setting.name = field_name + setting.category = category + setting.encrypted = encrypted + setting.cleaned_value = field_value + setting.save() diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 689e1ea82..0db9c7c54 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -14,5 +14,6 @@ urlpatterns = [ path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), + path('setting/', api.SettingsApi.as_view(), name='settings-setting'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'), ] diff --git a/apps/settings/utils/__init__.py b/apps/settings/utils/__init__.py index 87bc6198f..e17c4e43c 100644 --- a/apps/settings/utils/__init__.py +++ b/apps/settings/utils/__init__.py @@ -2,3 +2,4 @@ # from .ldap import * +from .common import * diff --git a/apps/settings/utils/common.py b/apps/settings/utils/common.py new file mode 100644 index 000000000..e64ceaf7f --- /dev/null +++ b/apps/settings/utils/common.py @@ -0,0 +1,18 @@ +# coding: utf-8 + + +class ObjectDict(dict): + def __getattr__(self, name): + if name in self: + return self[name] + else: + raise AttributeError("No such attribute: " + name) + + def __setattr__(self, name, value): + self[name] = value + + def __delattr__(self, name): + if name in self: + del self[name] + else: + raise AttributeError("No such attribute: " + name) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 9081d3a94..b930154ff 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -138,11 +138,11 @@ function setAjaxCSRFToken() { } function activeNav(prefix) { - var path = document.location.pathname; - if (prefix) { - path = path.replace(prefix, ''); - console.log(path); + if (!prefix) { + prefix = '/core' } + var path = document.location.pathname; + path = path.replace(prefix, ''); var urlArray = path.split("/"); var app = urlArray[1]; var resource = urlArray[2]; diff --git a/apps/templates/index.html b/apps/templates/index.html index 1942ad2cb..a216ddac8 100644 --- a/apps/templates/index.html +++ b/apps/templates/index.html @@ -58,11 +58,11 @@
- {% trans 'In the past week, a total of ' %}{% trans ' users have logged in ' %}{% trans ' times asset.' %} -
    + {% trans 'In the past week, a total of ' %}{% trans ' users have logged in ' %}{% trans ' times asset.' %} +
-
+

@@ -73,12 +73,12 @@

-
+
{% trans 'User' %}
-
+
{% trans 'Asset' %}
@@ -112,7 +112,7 @@

{% trans 'Top 10 assets in a week'%}

{% trans 'Login frequency and last login record.' %}
-
+

@@ -130,7 +130,7 @@
-
+
@@ -158,7 +158,7 @@

{% trans 'Top 10 users in a week' %}

{% trans 'User login frequency and last login record.' %}
-
+
@@ -178,7 +178,7 @@ function requireMonthMetricsECharts(data){ 'echarts/chart/line' ], function (ec) { - var monthMetricsECharts = ec.init(document.getElementById('month_metrics_echarts')); + var monthMetricsECharts = ec.init(document.getElementById('dates_metrics_echarts')); var option = { title : { text: "{% trans 'Monthly data overview' %}", @@ -204,7 +204,7 @@ function requireMonthMetricsECharts(data){ { type : 'category', boundaryGap : false, - data : data['month_metrics_date'], + data : data['dates_metrics_date'], } ], yAxis : [ @@ -218,21 +218,21 @@ function requireMonthMetricsECharts(data){ type:'line', smooth: true, itemStyle: {normal: {areaStyle: {type: 'default'}}}, - data: data['month_metrics_total_count_login'] + data: data['dates_metrics_total_count_login'] }, { name: "{% trans 'Active users' %}", type: 'line', smooth: true, itemStyle: {normal: {areaStyle: {type: 'default'}}}, - data: data['month_metrics_total_count_active_users'] + data: data['dates_metrics_total_count_active_users'] }, { name:"{% trans 'Active assets' %}", type:'line', smooth:true, itemStyle: {normal: {areaStyle: {type: 'default'}}}, - data: data['month_metrics_total_count_active_assets'] + data: data['dates_metrics_total_count_active_assets'] } ] }; @@ -249,7 +249,7 @@ function requireMonthTotalCountUsersPie(data){ 'echarts/chart/pie' ], function (ec) { - var monthTotalCountUsersPie = ec.init(document.getElementById('month_total_count_users_pie')); + var monthTotalCountUsersPie = ec.init(document.getElementById('dates_total_count_users_pie')); var option = { tooltip : { trigger: 'item', @@ -310,9 +310,9 @@ function requireMonthTotalCountUsersPie(data){ } }, data:[ - {value:data['month_total_count_active_users'], name:"{% trans 'Monthly active users' %}"}, - {value:data['month_total_count_disabled_users'], name:"{% trans 'Disable user' %}"}, - {value:data['month_total_count_inactive_users'], name:"{% trans 'Month not logged in user' %}"} + {value:data['dates_total_count_active_users'], name:"{% trans 'Monthly active users' %}"}, + {value:data['dates_total_count_disabled_users'], name:"{% trans 'Disable user' %}"}, + {value:data['dates_total_count_inactive_users'], name:"{% trans 'Month not logged in user' %}"} ] } ] @@ -329,7 +329,7 @@ function requireMonthTotalCountAssetsPie(data){ 'echarts/chart/pie' ], function (ec) { - var monthTotalCountAssetsPie = ec.init(document.getElementById('month_total_count_assets_pie')); + var monthTotalCountAssetsPie = ec.init(document.getElementById('dates_total_count_assets_pie')); var option = { tooltip : { trigger: 'item', @@ -389,9 +389,9 @@ function requireMonthTotalCountAssetsPie(data){ } }, data:[ - {value:data['month_total_count_active_assets'], name:"{% trans 'Month is logged into the host' %}"}, - {value:data['month_total_count_disabled_assets'], name:"{% trans 'Disable host' %}"}, - {value:data['month_total_count_inactive_assets'], name:"{% trans 'Month not logged on host' %}"} + {value:data['dates_total_count_active_assets'], name:"{% trans 'Month is logged into the host' %}"}, + {value:data['dates_total_count_disabled_assets'], name:"{% trans 'Disable host' %}"}, + {value:data['dates_total_count_inactive_assets'], name:"{% trans 'Month not logged on host' %}"} ] } ] @@ -431,14 +431,14 @@ function renderMonthMetricsECharts(){ var success = function (data) { requireMonthMetricsECharts(data) }; - renderRequestApi('month_metrics=1', success) + renderRequestApi('dates_metrics=1', success) } function renderMonthTotalCountUsersPie(){ var success = function (data) { requireMonthTotalCountUsersPie(data) }; - renderRequestApi('month_total_count_users=1', success) + renderRequestApi('dates_total_count_users=1', success) } @@ -446,15 +446,15 @@ function renderMonthTotalCountAssetsPie(){ var success = function (data) { requireMonthTotalCountAssetsPie(data) }; - renderRequestApi('month_total_count_assets=1', success) + renderRequestApi('dates_total_count_assets=1', success) } function renderWeekTotalCount(){ var success = function (data) { - $('#week_total_count_login_users').html(data['week_total_count_login_users']); - $('#week_total_count_login_times').html(data['week_total_count_login_times']) + $('#dates_total_count_login_users').html(data['dates_total_count_login_users']); + $('#dates_total_count_login_times').html(data['dates_total_count_login_times']) }; - renderRequestApi('week_total_count=1', success) + renderRequestApi('dates_total_count=1', success) } function renderWeekLoginTimesTop5Users(){ @@ -468,14 +468,14 @@ function renderWeekLoginTimesTop5Users(){ "{INDEX} {USER}" + ""; - $.each(data['week_login_times_top5_users'], function(index, value){ + $.each(data['dates_login_times_top5_users'], function(index, value){ html += html_cell.replace('{TOTAL}', value['total']) .replace('{USER}', value['user']) .replace('{INDEX}', index+1) }); - $('#week_login_times_top5_users').html(html) + $('#dates_login_times_top5_users').html(html) }; - renderRequestApi('week_login_times_top5_users=1', success) + renderRequestApi('dates_login_times_top5_users=1', success) } function renderWeekLoginTimesTop10Assets(){ @@ -497,7 +497,7 @@ function renderWeekLoginTimesTop10Assets(){ "" + ""; - var assets = data['week_login_times_top10_assets']; + var assets = data['dates_login_times_top10_assets']; if (assets.length !== 0){ $.each(assets, function(index, value){ html += html_cell @@ -509,9 +509,9 @@ function renderWeekLoginTimesTop10Assets(){ else{ html += "

{% trans '(No)' %}

" } - $('#week_login_times_top10_assets').html(html) + $('#dates_login_times_top10_assets').html(html) }; - renderRequestApi('week_login_times_top10_assets=1', success) + renderRequestApi('dates_login_times_top10_assets=1', success) } function renderWeekLoginTimesTop10Users(){ @@ -533,7 +533,7 @@ function renderWeekLoginTimesTop10Users(){ "" + ""; - var users = data['week_login_times_top10_users']; + var users = data['dates_login_times_top10_users']; if (users.length !== 0){ $.each(users, function(index, value){ html += html_cell.replaceAll('{USER}', value['user']) @@ -544,9 +544,9 @@ function renderWeekLoginTimesTop10Users(){ else{ html += "

{% trans '(No)' %}

" } - $('#week_login_times_top10_users').html(html) + $('#dates_login_times_top10_users').html(html) }; - renderRequestApi('week_login_times_top10_users=1', success) + renderRequestApi('dates_login_times_top10_users=1', success) } function renderWeekLoginRecordTop10Sessions(){ @@ -564,7 +564,7 @@ function renderWeekLoginRecordTop10Sessions(){ "" + ""; - var users = data['week_login_record_top10_sessions']; + var users = data['dates_login_record_top10_sessions']; if (users.length !== 0){ $.each(users, function(index, value){ console.log(value['is_finished']) @@ -579,10 +579,10 @@ function renderWeekLoginRecordTop10Sessions(){ else{ html += "

{% trans '(No)' %}

" } - $('#week_login_record_top10_sessions').html(html) + $('#dates_login_record_top10_sessions').html(html) }; - renderRequestApi('week_login_record_top10_sessions=1', success) + renderRequestApi('dates_login_record_top10_sessions=1', success) } function renderData(){ diff --git a/apps/users/api/group.py b/apps/users/api/group.py index 860ca36b4..f91b1d3bd 100644 --- a/apps/users/api/group.py +++ b/apps/users/api/group.py @@ -16,4 +16,3 @@ class UserGroupViewSet(OrgBulkModelViewSet): search_fields = filter_fields permission_classes = (IsOrgAdmin,) serializer_class = UserGroupSerializer - diff --git a/apps/users/api/profile.py b/apps/users/api/profile.py index 70e028a85..321ef54e1 100644 --- a/apps/users/api/profile.py +++ b/apps/users/api/profile.py @@ -14,6 +14,7 @@ from .mixins import UserQuerysetMixin __all__ = [ 'UserResetPasswordApi', 'UserResetPKApi', 'UserProfileApi', 'UserUpdatePKApi', + 'UserPasswordApi', 'UserPublicKeyApi' ] @@ -55,9 +56,9 @@ class UserUpdatePKApi(UserQuerysetMixin, generics.UpdateAPIView): user.save() -class UserProfileApi(generics.RetrieveAPIView): +class UserProfileApi(generics.RetrieveUpdateAPIView): permission_classes = (IsAuthenticated,) - serializer_class = serializers.UserSerializer + serializer_class = serializers.UserProfileSerializer def get_object(self): return self.request.user @@ -66,3 +67,24 @@ class UserProfileApi(generics.RetrieveAPIView): age = request.session.get_expiry_age() request.session.set_expiry(age) return super().retrieve(request, *args, **kwargs) + + +class UserPasswordApi(generics.RetrieveUpdateAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = serializers.UserUpdatePasswordSerializer + + def get_object(self): + return self.request.user + + +class UserPublicKeyApi(generics.RetrieveUpdateAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = serializers.UserUpdatePublicKeySerializer + + def get_object(self): + return self.request.user + + def perform_update(self, serializer): + user = self.get_object() + user.public_key = serializer.validated_data['public_key'] + user.save() diff --git a/apps/users/api/user.py b/apps/users/api/user.py index fac0fabca..8729f4dcc 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -27,12 +27,9 @@ __all__ = [ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): - filter_fields = ('username', 'email', 'name', 'id') + filter_fields = ('username', 'email', 'name', 'id', 'source') search_fields = filter_fields - serializer_classes = { - 'default': serializers.UserSerializer, - 'display': serializers.UserDisplaySerializer - } + serializer_class = serializers.UserSerializer permission_classes = (IsOrgAdmin, CanUpdateDeleteUser) def get_queryset(self): diff --git a/apps/users/migrations/0026_auto_20200508_2105.py b/apps/users/migrations/0026_auto_20200508_2105.py new file mode 100644 index 000000000..0ccd09e09 --- /dev/null +++ b/apps/users/migrations/0026_auto_20200508_2105.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-05-08 13:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0025_auto_20200206_1216'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS')], default='ldap', max_length=30, verbose_name='Source'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 26f231fbc..3e38cbb18 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -47,6 +47,10 @@ class AuthMixin: post_user_change_password.send(self.__class__, user=self) super().set_password(raw_password) + def set_public_key(self, public_key): + self.public_key = public_key + self.save() + def can_update_password(self): return self.is_local @@ -79,6 +83,14 @@ class AuthMixin: pass return PubKey() + def get_public_key_comment(self): + return self.public_key_obj.comment + + def get_public_key_hash_md5(self): + if not callable(self.public_key_obj.hash_md5): + return '' + return self.public_key_obj.hash_md5() + def reset_password(self, new_password): self.set_password(new_password) self.save() @@ -159,6 +171,16 @@ class RoleMixin: roles.append(str(_('User'))) return " | ".join(roles) + def current_org_roles(self): + roles = [] + if self.can_admin_current_org: + roles.append('Admin') + if self.can_audit_current_org: + roles.append('Auditor') + else: + roles.append('User') + return roles + @property def is_superuser(self): if self.role == 'Admin': @@ -481,7 +503,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): max_length=30, default='', blank=True, verbose_name=_('Created by') ) source = models.CharField( - max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES, + max_length=30, default=SOURCE_LDAP, choices=SOURCE_CHOICES, verbose_name=_('Source') ) date_password_last_updated = models.DateTimeField( diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py index 67b21668c..a6b9f67b9 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ - +from django.db.models import Prefetch from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer @@ -18,15 +18,18 @@ __all__ = [ class UserGroupSerializer(BulkOrgResourceModelSerializer): users = serializers.PrimaryKeyRelatedField( required=False, many=True, queryset=User.objects, label=_('User'), - write_only=True + # write_only=True, # group can return many to many on detail ) class Meta: model = UserGroup list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'users', 'users_amount', 'comment', - 'date_created', 'created_by', + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'comment', 'date_created', 'created_by' + ] + fields = fields_mini + fields_small + [ + 'users', 'users_amount', ] extra_kwargs = { 'created_by': {'label': _('Created by'), 'read_only': True} @@ -37,8 +40,9 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): self.set_fields_queryset() def set_fields_queryset(self): - users_field = self.fields['users'] - users_field.child_relation.queryset = utils.get_current_org_members() + users_field = self.fields.get('users') + if users_field: + users_field.child_relation.queryset = utils.get_current_org_members(exclude=('Auditor',)) def validate_users(self, users): for user in users: @@ -50,5 +54,7 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.annotate(users_amount=Count('users')) + queryset = queryset.prefetch_related( + Prefetch('users', queryset=User.objects.only('id')) + ).annotate(users_amount=Count('users')) return queryset diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 156c4a3da..437ec9302 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # +from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.utils import validate_ssh_public_key -from common.mixins import BulkSerializerMixin +from common.mixins import CommonBulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser from ..models import User @@ -13,7 +14,8 @@ from ..models import User __all__ = [ 'UserSerializer', 'UserPKUpdateSerializer', 'ChangeUserPasswordSerializer', 'ResetOTPSerializer', - 'UserProfileSerializer', 'UserDisplaySerializer', + 'UserProfileSerializer', 'UserOrgSerializer', + 'UserUpdatePasswordSerializer', 'UserUpdatePublicKeySerializer' ] @@ -22,20 +24,43 @@ class UserOrgSerializer(serializers.Serializer): name = serializers.CharField() -class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): - admin_or_audit_orgs = UserOrgSerializer(many=True, read_only=True) +class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): + EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') + CUSTOM_PASSWORD = _('Set password') + PASSWORD_STRATEGY_CHOICES = ( + (0, EMAIL_SET_PASSWORD), + (1, CUSTOM_PASSWORD) + ) + password_strategy = serializers.ChoiceField( + choices=PASSWORD_STRATEGY_CHOICES, required=False, initial=0, + label=_('Password strategy'), write_only=True + ) + mfa_level_display = serializers.ReadOnlyField(source='get_mfa_level_display') + login_blocked = serializers.SerializerMethodField() + can_update = serializers.SerializerMethodField() + can_delete = serializers.SerializerMethodField() + + key_prefix_block = "_LOGIN_BLOCK_{}" class Meta: model = User list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'username', 'password', 'email', 'public_key', - 'groups', 'role', 'wechat', 'phone', 'mfa_level', + # mini 是指能识别对象的最小单元 + fields_mini = ['id', 'name', 'username'] + # small 指的是 不需要计算的直接能从一张表中获取到的数据 + fields_small = fields_mini + [ + 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', + 'mfa_level_display', 'mfa_force_enabled', 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', - 'date_password_last_updated', 'date_expired', - 'avatar_url', 'admin_or_audit_orgs', + 'password_strategy', 'date_password_last_updated', 'date_expired', + 'avatar_url', 'source_display', 'date_joined', 'last_login' ] + fields = fields_small + [ + 'groups', 'role', 'groups_display', 'role_display', + 'can_update', 'can_delete', 'login_blocked', + ] + extra_kwargs = { 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, @@ -44,8 +69,25 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): 'is_expired': {'label': _('Is expired')}, 'avatar_url': {'label': _('Avatar url')}, 'created_by': {'read_only': True, 'allow_blank': True}, + 'can_update': {'read_only': True}, + 'can_delete': {'read_only': True}, + 'groups_display': {'label': _('Groups name')}, + 'source_display': {'label': _('Source name')}, + 'role_display': {'label': _('Role name')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_role_choices() + + def set_role_choices(self): + role = self.fields.get('role') + if not role: + return + choices = role._choices + choices.pop('App', None) + role._choices = choices + def validate_role(self, value): request = self.context.get('request') if not request.user.is_superuser and value != User.ROLE_USER: @@ -67,6 +109,9 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): return password def validate_groups(self, groups): + """ + 审计员不能加入到组中 + """ role = self.initial_data.get('role') if self.instance: role = role or self.instance.role @@ -92,19 +137,9 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): def validate(self, attrs): attrs = self.change_password_to_raw(attrs) attrs = self.clean_auth_fields(attrs) + attrs.pop('password_strategy', None) return attrs - -class UserDisplaySerializer(UserSerializer): - can_update = serializers.SerializerMethodField() - can_delete = serializers.SerializerMethodField() - - class Meta(UserSerializer.Meta): - fields = UserSerializer.Meta.fields + [ - 'groups_display', 'role_display', 'source_display', - 'can_update', 'can_delete', - ] - def get_can_update(self, obj): return CanUpdateDeleteUser.has_update_object_permission( self.context['request'], self.context['view'], obj @@ -115,16 +150,10 @@ class UserDisplaySerializer(UserSerializer): self.context['request'], self.context['view'], obj ) - def get_extra_kwargs(self): - kwargs = super().get_extra_kwargs() - kwargs.update({ - 'can_update': {'read_only': True}, - 'can_delete': {'read_only': True}, - 'groups_display': {'label': _('Groups name')}, - 'source_display': {'label': _('Source name')}, - 'role_display': {'label': _('Role name')}, - }) - return kwargs + def get_login_blocked(self, obj): + key_block = self.key_prefix_block.format(obj.username) + blocked = bool(cache.get(key_block)) + return blocked class UserPKUpdateSerializer(serializers.ModelSerializer): @@ -156,9 +185,109 @@ class ResetOTPSerializer(serializers.Serializer): pass -class UserProfileSerializer(serializers.ModelSerializer): +class UserRoleSerializer(serializers.Serializer): + name = serializers.CharField(max_length=24) + display = serializers.CharField(max_length=64) + + +class UserProfileSerializer(UserSerializer): + admin_or_audit_orgs = UserOrgSerializer(many=True, read_only=True) + current_org_roles = serializers.ListField(read_only=True) + public_key_comment = serializers.CharField( + source='get_public_key_comment', required=False, read_only=True, max_length=128 + ) + public_key_hash_md5 = serializers.CharField( + source='get_public_key_hash_md5', required=False, read_only=True, max_length=128 + ) + + class Meta(UserSerializer.Meta): + fields = UserSerializer.Meta.fields + [ + 'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles' + ] + extra_kwargs = dict(UserSerializer.Meta.extra_kwargs) + extra_kwargs.update({ + 'name': {'read_only': True, 'max_length': 128}, + 'username': {'read_only': True, 'max_length': 128}, + 'email': {'read_only': True}, + 'mfa_level': {'read_only': True}, + 'source': {'read_only': True}, + 'is_valid': {'read_only': True}, + 'is_active': {'read_only': True}, + 'groups': {'read_only': True}, + 'roles': {'read_only': True}, + 'password_strategy': {'read_only': True}, + 'date_expired': {'read_only': True}, + 'date_joined': {'read_only': True}, + 'last_login': {'read_only': True}, + 'role': {'read_only': True}, + }) + + if 'password' in fields: + fields.remove('password') + extra_kwargs.pop('password', None) + + if 'public_key' in fields: + fields.remove('public_key') + extra_kwargs.pop('public_key', None) + + +class UserUpdatePasswordSerializer(serializers.ModelSerializer): + old_password = serializers.CharField(required=True, max_length=128, write_only=True) + new_password = serializers.CharField(required=True, max_length=128, write_only=True) + new_password_again = serializers.CharField(required=True, max_length=128, write_only=True) + class Meta: model = User - fields = [ - 'id', 'username', 'name', 'role', 'email' - ] + fields = ['old_password', 'new_password', 'new_password_again'] + + def validate_old_password(self, value): + if not self.instance.check_password(value): + msg = 'The old password is incorrect' + raise serializers.ValidationError(msg) + return value + + @staticmethod + def validate_new_password(value): + from ..utils import check_password_rules + if not check_password_rules(value): + msg = _('Password does not match security rules') + raise serializers.ValidationError(msg) + return value + + def validate_new_password_again(self, value): + if value != self.initial_data.get('new_password', ''): + msg = 'The newly set password is inconsistent' + raise serializers.ValidationError(msg) + return value + + def update(self, instance, validated_data): + new_password = self.validated_data.get('new_password') + instance.reset_password(new_password) + return instance + + +class UserUpdatePublicKeySerializer(serializers.ModelSerializer): + public_key_comment = serializers.CharField( + source='get_public_key_comment', required=False, read_only=True, max_length=128 + ) + public_key_hash_md5 = serializers.CharField( + source='get_public_key_hash_md5', required=False, read_only=True, max_length=128 + ) + + class Meta: + model = User + fields = ['public_key_comment', 'public_key_hash_md5', 'public_key'] + extra_kwargs = { + 'public_key': {'required': True, 'write_only': True, 'max_length': 2048} + } + + @staticmethod + def validate_public_key(value): + if not validate_ssh_public_key(value): + raise serializers.ValidationError(_('Not a valid ssh public key')) + return value + + def update(self, instance, validated_data): + new_public_key = self.validated_data.get('public_key') + instance.set_public_key(new_public_key) + return instance diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index 63dd981be..28b30f73d 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -132,7 +132,7 @@ function initTable() { $(document).ready(function(){ usersTable = initTable(); - initCsvImportExport(usersTable, "{% trans 'User groups' %}") + initCsvImportExport(usersTable, "{% trans 'User' %}") }).on('click', '#btn_bulk_update', function(){ var action = $('#slct_bulk_update').val(); var id_list = usersTable.selected; diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index b3a75f9ba..ea1101eae 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -21,6 +21,8 @@ urlpatterns = [ path('connection-token/', auth_api.UserConnectionTokenApi.as_view(), name='connection-token'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'), + path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'), + path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'), path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'), path('users//otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'), path('users//password/', api.UserChangePasswordApi.as_view(), name='change-user-password'), diff --git a/requirements/requirements.txt b/requirements/requirements.txt index bd65081c3..741e66ed8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ amqp==2.1.4 -ansible==2.8.2 +ansible==2.8.8 asn1crypto==0.24.0 bcrypt==3.1.4 billiard==3.5.0.3 @@ -49,7 +49,7 @@ olefile==0.44 openapi-codec==1.3.2 paramiko==2.4.2 passlib==1.7.1 -Pillow==6.2.0 +Pillow==6.2.2 pyasn1==0.4.8 pycparser==2.19 pycrypto==2.6.1 @@ -89,7 +89,7 @@ flower==0.9.3 channels-redis==2.4.0 channels==2.3.0 daphne==2.3.0 -psutil==5.6.5 +psutil==5.6.6 django-cas-ng==4.0.1 python-cas==1.5.0 ipython