diff --git a/apps/applications/forms/remote_app.py b/apps/applications/forms/remote_app.py index cfa841f83..ba7661acd 100644 --- a/apps/applications/forms/remote_app.py +++ b/apps/applications/forms/remote_app.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins import OrgModelForm -from assets.models import SystemUser, Protocol +from assets.models import SystemUser from ..models import RemoteApp from .. import const @@ -88,9 +88,7 @@ class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm): # 过滤RDP资产和系统用户 super().__init__(*args, **kwargs) field_asset = self.fields['asset'] - field_asset.queryset = field_asset.queryset.filter( - protocols__name=Protocol.PROTOCOL_RDP - ) + field_asset.queryset = field_asset.queryset.has_protocol('rdp') field_system_user = self.fields['system_user'] field_system_user.queryset = field_system_user.queryset.filter( protocol=SystemUser.PROTOCOL_RDP diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index f8ed14cb9..2d22dd41b 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -20,6 +20,7 @@ from common.mixins import IDInCacheFilterMixin, ApiMessageMixin from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from orgs.mixins import OrgBulkModelViewSet from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from ..models import Asset, AdminUser, Node from .. import serializers @@ -36,7 +37,7 @@ __all__ = [ ] -class AssetViewSet(IDInCacheFilterMixin, LabelFilter, ApiMessageMixin, BulkModelViewSet): +class AssetViewSet(LabelFilter, OrgBulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ @@ -100,11 +101,6 @@ class AssetViewSet(IDInCacheFilterMixin, LabelFilter, ApiMessageMixin, BulkModel queryset = self.filter_admin_user_id(queryset) return queryset - def get_queryset(self): - queryset = super().get_queryset().distinct() - queryset = self.get_serializer_class().setup_eager_loading(queryset) - return queryset - class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView): """ diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index 2b2eb18a5..38d820349 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # -import time - from rest_framework.response import Response from rest_framework import viewsets, status, generics from rest_framework.pagination import LimitOffsetPagination @@ -10,7 +8,7 @@ from rest_framework import filters from rest_framework_bulk import BulkModelViewSet from django.shortcuts import get_object_or_404 -from common.permissions import IsOrgAdminOrAppUser +from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger from common.mixins import IDInCacheFilterMixin from ..backends import AssetUserManager @@ -57,7 +55,7 @@ class AssetUserSearchBackend(filters.BaseFilterBackend): class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): pagination_class = LimitOffsetPagination serializer_class = serializers.AssetUserSerializer - permission_classes = (IsOrgAdminOrAppUser, ) + permission_classes = [IsOrgAdminOrAppUser] http_method_names = ['get', 'post'] filter_fields = [ "id", "ip", "hostname", "username", "asset_id", "node_id", @@ -78,7 +76,7 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): system_user_id = self.request.GET.get("system_user_id") kwargs = {} - assets = [] + assets = None manager = AssetUserManager() if system_user_id: @@ -92,7 +90,7 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): manager.prefer('admin_user') if asset_id: - asset = get_object_or_none(Asset, pk=asset_id) + asset = get_object_or_404(Asset, id=asset_id) assets = [asset] elif node_id: node = get_object_or_404(Node, id=node_id) @@ -100,7 +98,7 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): if username: kwargs['username'] = username - if assets: + if assets is not None: kwargs['assets'] = assets queryset = manager.filter(**kwargs) @@ -110,23 +108,14 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): class AssetUserExportViewSet(AssetUserViewSet): serializer_class = serializers.AssetUserExportSerializer http_method_names = ['get'] - - def list(self, request, *args, **kwargs): - otp_last_verify = request.session.get("OTP_LAST_VERIFY_TIME") - if not otp_last_verify or time.time() - int(otp_last_verify) > 600: - return Response({"error": "Need MFA confirm mfa auth"}, status=403) - return super().list(request, *args, **kwargs) + permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] class AssetUserAuthInfoApi(generics.RetrieveAPIView): serializer_class = serializers.AssetUserAuthInfoSerializer - permission_classes = (IsOrgAdminOrAppUser,) + permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] def retrieve(self, request, *args, **kwargs): - otp_last_verify = request.session.get("OTP_LAST_VERIFY_TIME") - if not otp_last_verify or time.time() - int(otp_last_verify) > 600: - return Response({"error": "Need MFA confirm mfa auth"}, status=403) - instance = self.get_object() serializer = self.get_serializer(instance) status_code = status.HTTP_200_OK @@ -135,15 +124,14 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): return Response(serializer.data, status=status_code) def get_object(self): - username = self.request.GET.get('username') - asset_id = self.request.GET.get('asset_id') - prefer = self.request.GET.get("prefer") + query_params = self.request.query_params + username = query_params.get('username') + asset_id = query_params.get('asset_id') + prefer = query_params.get("prefer") asset = get_object_or_none(Asset, pk=asset_id) try: manger = AssetUserManager() - if prefer: - manger.prefer(prefer) - instance = manger.get(username, asset) + instance = manger.get(username, asset, prefer=prefer) except Exception as e: logger.error(e, exc_info=True) return None @@ -156,13 +144,15 @@ class AssetUserTestConnectiveApi(generics.RetrieveAPIView): Test asset users connective """ permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.TaskIDSerializer def get_asset_users(self): username = self.request.GET.get('username') asset_id = self.request.GET.get('asset_id') + prefer = self.request.GET.get("prefer") asset = get_object_or_none(Asset, pk=asset_id) manager = AssetUserManager() - asset_users = manager.filter(username=username, assets=[asset]) + asset_users = manager.filter(username=username, assets=[asset], prefer=prefer) return asset_users def retrieve(self, request, *args, **kwargs): diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index f772a2ace..51f9a8739 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -26,6 +26,7 @@ from ..hands import IsOrgAdmin from ..models import Node from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util from .. import serializers +from ..utils import NodeUtil logger = get_logger(__file__) @@ -79,12 +80,10 @@ class NodeListAsTreeApi(generics.ListAPIView): serializer_class = TreeNodeSerializer def get_queryset(self): - queryset = [node.as_tree_node() for node in Node.objects.all()] - return queryset - - def filter_queryset(self, queryset): - if self.request.query_params.get('refresh', '0') == '1': - queryset = self.refresh_nodes(queryset) + queryset = Node.objects.all() + util = NodeUtil() + nodes = util.get_nodes_by_queryset(queryset) + queryset = [node.as_tree_node() for node in nodes] return queryset @staticmethod @@ -113,16 +112,16 @@ class NodeChildrenAsTreeApi(generics.ListAPIView): is_root = False def get_queryset(self): + self.check_need_refresh_nodes() node_key = self.request.query_params.get('key') - if node_key: - self.node = Node.objects.get(key=node_key) - queryset = self.node.get_children(with_self=False) - else: - self.is_root = True - self.node = Node.root() - queryset = list(self.node.get_children(with_self=True)) - nodes_invalid = Node.objects.exclude(key__startswith=self.node.key) - queryset.extend(list(nodes_invalid)) + util = NodeUtil() + # 是否包含自己 + with_self = False + if not node_key: + node_key = Node.root().key + with_self = True + self.node = util.get_node_by_key(node_key) + queryset = self.node.get_children(with_self=with_self) queryset = [node.as_tree_node() for node in queryset] queryset = sorted(queryset) return queryset @@ -131,21 +130,20 @@ class NodeChildrenAsTreeApi(generics.ListAPIView): include_assets = self.request.query_params.get('assets', '0') == '1' if not include_assets: return queryset - assets = self.node.get_assets() + assets = self.node.get_assets().only( + "id", "hostname", "ip", 'platform', "os", "org_id", + ) for asset in assets: queryset.append(asset.as_tree_node(self.node)) return queryset def filter_queryset(self, queryset): queryset = self.filter_assets(queryset) - queryset = self.filter_refresh_nodes(queryset) return queryset - def filter_refresh_nodes(self, queryset): + def check_need_refresh_nodes(self): if self.request.query_params.get('refresh', '0') == '1': - Node.expire_nodes_assets_amount() - Node.expire_nodes_full_value() - return queryset + Node.refresh_nodes() class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index f6398e974..8baacd4f8 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -22,6 +22,7 @@ from rest_framework.pagination import LimitOffsetPagination from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.mixins import IDInCacheFilterMixin +from orgs.mixins import OrgBulkModelViewSet from ..models import SystemUser, Asset from .. import serializers from ..tasks import push_system_user_to_assets_manual, \ @@ -39,7 +40,7 @@ __all__ = [ ] -class SystemUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): +class SystemUserViewSet(OrgBulkModelViewSet): """ System user api set, for add,delete,update,list,retrieve resource """ diff --git a/apps/assets/backends/asset_user.py b/apps/assets/backends/asset_user.py index 62ab24b4d..774172e6c 100644 --- a/apps/assets/backends/asset_user.py +++ b/apps/assets/backends/asset_user.py @@ -14,6 +14,11 @@ class AssetUserBackend(BaseBackend): @classmethod def filter(cls, username=None, assets=None, **kwargs): queryset = cls.model.objects.all() + prefer_id = kwargs.get('prefer_id') + if prefer_id: + queryset = queryset.filter(id=prefer_id) + instances = cls.construct_authbook_objects(queryset, assets) + return instances if username: queryset = queryset.filter(username=username) if assets: diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py index d6f30f940..f45b0b3b8 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -7,11 +7,13 @@ from abc import abstractmethod class BaseBackend: @classmethod @abstractmethod - def filter(cls, username=None, assets=None, latest=True): + def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None): """ :param username: 用户名 :param assets: 对象 :param latest: 是否是最新记录 + :param prefer: 优先使用 + :param prefer_id: 使用id :return: 元素为的可迭代对象( or ) """ pass diff --git a/apps/assets/backends/db.py b/apps/assets/backends/db.py index f37569e51..40fa41444 100644 --- a/apps/assets/backends/db.py +++ b/apps/assets/backends/db.py @@ -7,7 +7,7 @@ from .base import BaseBackend class AuthBookBackend(BaseBackend): @classmethod - def filter(cls, username=None, assets=None, latest=True): + def filter(cls, username=None, assets=None, latest=True, **kwargs): queryset = AuthBook.objects.all() if username is not None: queryset = queryset.filter(username=username) diff --git a/apps/assets/backends/manager.py b/apps/assets/backends/manager.py index 180c2861c..6745ea44b 100644 --- a/apps/assets/backends/manager.py +++ b/apps/assets/backends/manager.py @@ -30,21 +30,22 @@ class AssetUserManager: ) _prefer = "system_user" - _using = None - def filter(self, username=None, assets=None, latest=True): - if self._using: - backend = dict(self.backends).get(self._using) - if not backend: - return self.none() - instances = backend.filter(username=username, assets=assets, latest=latest) - return AssetUserQuerySet(instances) + def filter(self, username=None, assets=None, latest=True, prefer=None, prefer_id=None): + if assets is not None and not assets: + return AssetUserQuerySet([]) + + if prefer: + self._prefer = prefer instances_map = {} instances = [] for name, backend in self.backends: + if name != "db" and self._prefer != name: + continue _instances = backend.filter( - username=username, assets=assets, latest=latest + username=username, assets=assets, latest=latest, + prefer=self._prefer, prefer_id=prefer_id, ) instances_map[name] = _instances @@ -61,12 +62,12 @@ class AssetUserManager: else: ordering.extend(["admin_user", "system_user"]) # 根据prefer决定优先使用系统用户或管理用户谁的 - ordering_instances = [instances_map.get(i) for i in ordering] + ordering_instances = [instances_map.get(i, []) for i in ordering] instances = self._merge_instances(*ordering_instances) return AssetUserQuerySet(instances) - def get(self, username, asset): - instances = self.filter(username, assets=[asset]) + def get(self, username, asset, **kwargs): + instances = self.filter(username, assets=[asset], **kwargs) if len(instances) == 1: return instances[0] elif len(instances) == 0: @@ -92,10 +93,6 @@ class AssetUserManager: self._prefer = s return self - def using(self, s): - self._using = s - return self - @staticmethod def none(): return AssetUserQuerySet() diff --git a/apps/assets/const.py b/apps/assets/const.py index eebb5ecca..e5f65fce0 100644 --- a/apps/assets/const.py +++ b/apps/assets/const.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # - +from django.utils.translation import ugettext_lazy as _ UPDATE_ASSETS_HARDWARE_TASKS = [ { @@ -11,7 +11,6 @@ UPDATE_ASSETS_HARDWARE_TASKS = [ } ] -ADMIN_USER_CONN_CACHE_KEY = "ADMIN_USER_CONN_{}" TEST_ADMIN_USER_CONN_TASKS = [ { "name": "ping", @@ -49,7 +48,6 @@ TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ } ] -ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}' TEST_ASSET_USER_CONN_TASKS = [ { "name": "ping", @@ -74,5 +72,10 @@ TASK_OPTIONS = { } CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}' - +CONN_UNREACHABLE, CONN_REACHABLE, CONN_UNKNOWN = range(0, 3) +CONNECTIVITY_CHOICES = ( + (CONN_UNREACHABLE, _("Unreachable")), + (CONN_REACHABLE, _('Reachable')), + (CONN_UNKNOWN, _("Unknown")), +) diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 624b6c818..5973f731b 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -6,32 +6,32 @@ from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins import OrgModelForm -from ..models import Asset, Protocol +from ..models import Asset, Node logger = get_logger(__file__) __all__ = [ - 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', - 'ProtocolForm' + 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', ] -class ProtocolForm(forms.ModelForm): - class Meta: - model = Protocol - fields = ['name', 'port'] - widgets = { - 'name': forms.Select(attrs={ - 'class': 'form-control protocol-name' - }), - 'port': forms.TextInput(attrs={ - 'class': 'form-control protocol-port' - }), - } +class ProtocolForm(forms.Form): + name = forms.ChoiceField( + choices=Asset.PROTOCOL_CHOICES, label=_("Name"), initial='ssh', + widget=forms.Select(attrs={'class': 'form-control protocol-name'}) + ) + port = forms.IntegerField( + max_value=65534, min_value=1, label=_("Port"), initial=22, + widget=forms.TextInput(attrs={'class': 'form-control protocol-port'}) + ) class AssetCreateForm(OrgModelForm): - PROTOCOL_CHOICES = Protocol.PROTOCOL_CHOICES + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.data: + nodes_field = self.fields['nodes'] + nodes_field._queryset = Node.get_queryset() class Meta: model = Asset diff --git a/apps/assets/forms/cmd_filter.py b/apps/assets/forms/cmd_filter.py index 6cb35e631..308a008d0 100644 --- a/apps/assets/forms/cmd_filter.py +++ b/apps/assets/forms/cmd_filter.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # from django import forms +from django.core.exceptions import ValidationError +import re from orgs.mixins import OrgModelForm from ..models import CommandFilter, CommandFilterRule @@ -15,6 +17,8 @@ class CommandFilterForm(OrgModelForm): class CommandFilterRuleForm(OrgModelForm): + invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]') + class Meta: model = CommandFilterRule fields = [ @@ -25,3 +29,11 @@ class CommandFilterRuleForm(OrgModelForm): 'placeholder': 'eg:\r\nreboot\r\nrm -rf' }), } + + def clean_content(self): + content = self.cleaned_data.get("content") + if self.invalid_pattern.search(content): + invalid_char = self.invalid_pattern.pattern.replace('\\', '') + msg = _("Content should not be contain: {}").format(invalid_char) + raise ValidationError(msg) + return content diff --git a/apps/assets/migrations/0002_auto_20180105_1807.py b/apps/assets/migrations/0002_auto_20180105_1807.py deleted file mode 100644 index bf1f022ac..000000000 --- a/apps/assets/migrations/0002_auto_20180105_1807.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-01-05 10:07 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='adminuser', - options={'ordering': ['name'], 'verbose_name': 'Admin user'}, - ), - migrations.AlterModelOptions( - name='asset', - options={'verbose_name': 'Asset'}, - ), - migrations.AlterModelOptions( - name='assetgroup', - options={'ordering': ['name'], 'verbose_name': 'Asset group'}, - ), - migrations.AlterModelOptions( - name='cluster', - options={'ordering': ['name'], 'verbose_name': 'Cluster'}, - ), - migrations.AlterModelOptions( - name='systemuser', - options={'ordering': ['name'], 'verbose_name': 'System user'}, - ), - ] diff --git a/apps/assets/migrations/0003_auto_20180109_2331.py b/apps/assets/migrations/0003_auto_20180109_2331.py deleted file mode 100644 index 254de6236..000000000 --- a/apps/assets/migrations/0003_auto_20180109_2331.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-01-09 15:31 -from __future__ import unicode_literals - -import assets.models.asset -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0002_auto_20180105_1807'), - ] - - operations = [ - migrations.AlterField( - model_name='asset', - name='cluster', - field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'), - ), - ] diff --git a/apps/assets/migrations/0004_auto_20180125_1218.py b/apps/assets/migrations/0004_auto_20180125_1218.py deleted file mode 100644 index 1886fa499..000000000 --- a/apps/assets/migrations/0004_auto_20180125_1218.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-01-25 04:18 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0003_auto_20180109_2331'), - ] - - operations = [ - migrations.AlterField( - model_name='assetgroup', - name='created_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), - ), - ] diff --git a/apps/assets/migrations/0005_auto_20180126_1637.py b/apps/assets/migrations/0005_auto_20180126_1637.py deleted file mode 100644 index 8db19e482..000000000 --- a/apps/assets/migrations/0005_auto_20180126_1637.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-01-26 08:37 -from __future__ import unicode_literals - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0004_auto_20180125_1218'), - ] - - operations = [ - migrations.CreateModel( - name='Label', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('value', models.CharField(max_length=128, verbose_name='Value')), - ('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ], - options={ - 'db_table': 'assets_label', - }, - ), - migrations.AlterUniqueTogether( - name='label', - unique_together=set([('name', 'value')]), - ), - migrations.AddField( - model_name='asset', - name='labels', - field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'), - ), - ] diff --git a/apps/assets/migrations/0006_auto_20180130_1502.py b/apps/assets/migrations/0006_auto_20180130_1502.py deleted file mode 100644 index b77470d27..000000000 --- a/apps/assets/migrations/0006_auto_20180130_1502.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-01-30 07:02 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0005_auto_20180126_1637'), - ] - - operations = [ - migrations.RemoveField( - model_name='asset', - name='cabinet_no', - ), - migrations.RemoveField( - model_name='asset', - name='cabinet_pos', - ), - migrations.RemoveField( - model_name='asset', - name='env', - ), - migrations.RemoveField( - model_name='asset', - name='remote_card_ip', - ), - migrations.RemoveField( - model_name='asset', - name='status', - ), - migrations.RemoveField( - model_name='asset', - name='type', - ), - ] diff --git a/apps/assets/migrations/0007_auto_20180225_1815.py b/apps/assets/migrations/0007_auto_20180225_1815.py deleted file mode 100644 index 009381bcb..000000000 --- a/apps/assets/migrations/0007_auto_20180225_1815.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-02-25 10:15 -from __future__ import unicode_literals - -import assets.models.asset -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0006_auto_20180130_1502'), - ] - - operations = [ - migrations.CreateModel( - name='Node', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('key', models.CharField(max_length=64, unique=True, verbose_name='Key')), - ('value', models.CharField(max_length=128, unique=True, verbose_name='Value')), - ('child_mark', models.IntegerField(default=0)), - ('date_create', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.RemoveField( - model_name='asset', - name='cluster', - ), - migrations.RemoveField( - model_name='asset', - name='groups', - ), - migrations.RemoveField( - model_name='systemuser', - name='cluster', - ), - migrations.AlterField( - model_name='asset', - name='admin_user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'), - ), - migrations.AlterField( - model_name='systemuser', - name='protocol', - field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'), - ), - migrations.AddField( - model_name='asset', - name='nodes', - field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), - ), - migrations.AddField( - model_name='systemuser', - name='nodes', - field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'), - ), - ] diff --git a/apps/assets/migrations/0008_auto_20180306_1804.py b/apps/assets/migrations/0008_auto_20180306_1804.py deleted file mode 100644 index 48d352619..000000000 --- a/apps/assets/migrations/0008_auto_20180306_1804.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-03-06 10:04 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0007_auto_20180225_1815'), - ] - - operations = [ - migrations.AlterField( - model_name='adminuser', - name='created_by', - field=models.CharField(max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='adminuser', - name='username', - field=models.CharField(max_length=128, verbose_name='Username'), - ), - migrations.AlterField( - model_name='asset', - name='platform', - field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'), - ), - migrations.AlterField( - model_name='systemuser', - name='created_by', - field=models.CharField(max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='systemuser', - name='username', - field=models.CharField(max_length=128, verbose_name='Username'), - ), - ] diff --git a/apps/assets/migrations/0009_auto_20180307_1212.py b/apps/assets/migrations/0009_auto_20180307_1212.py deleted file mode 100644 index 08d770642..000000000 --- a/apps/assets/migrations/0009_auto_20180307_1212.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-03-07 04:12 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0008_auto_20180306_1804'), - ] - - operations = [ - migrations.AlterField( - model_name='node', - name='value', - field=models.CharField(max_length=128, verbose_name='Value'), - ), - ] diff --git a/apps/assets/migrations/0010_auto_20180307_1749.py b/apps/assets/migrations/0010_auto_20180307_1749.py deleted file mode 100644 index 5e6be0943..000000000 --- a/apps/assets/migrations/0010_auto_20180307_1749.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-03-07 09:49 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0009_auto_20180307_1212'), - ] - - operations = [ - migrations.AlterField( - model_name='node', - name='value', - field=models.CharField(max_length=128, unique=True, verbose_name='Value'), - ), - ] diff --git a/apps/assets/migrations/0011_auto_20180326_0957.py b/apps/assets/migrations/0011_auto_20180326_0957.py deleted file mode 100644 index 07b9055dc..000000000 --- a/apps/assets/migrations/0011_auto_20180326_0957.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-03-26 01:57 -from __future__ import unicode_literals - -import assets.models.utils -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0010_auto_20180307_1749'), - ] - - operations = [ - migrations.CreateModel( - name='Domain', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ], - ), - migrations.CreateModel( - name='Gateway', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), - ('username', models.CharField(max_length=128, verbose_name='Username')), - ('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')), - ('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')), - ('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_updated', models.DateTimeField(auto_now=True)), - ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), - ('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')), - ('port', models.IntegerField(default=22, verbose_name='Port')), - ('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')), - ('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='asset', - name='domain', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'), - ), - ] diff --git a/apps/assets/migrations/0012_auto_20180404_1302.py b/apps/assets/migrations/0012_auto_20180404_1302.py deleted file mode 100644 index 0ccb63e27..000000000 --- a/apps/assets/migrations/0012_auto_20180404_1302.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-04 05:02 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0011_auto_20180326_0957'), - ] - - operations = [ - migrations.AlterField( - model_name='asset', - name='domain', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'), - ), - ] diff --git a/apps/assets/migrations/0013_auto_20180411_1135.py b/apps/assets/migrations/0013_auto_20180411_1135.py deleted file mode 100644 index baaf789bd..000000000 --- a/apps/assets/migrations/0013_auto_20180411_1135.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-11 03:35 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0012_auto_20180404_1302'), - ] - - operations = [ - migrations.AddField( - model_name='systemuser', - name='assets', - field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'), - ), - migrations.AlterField( - model_name='systemuser', - name='sudo', - field=models.TextField(default='/bin/whoami', verbose_name='Sudo'), - ), - ] diff --git a/apps/assets/migrations/0014_auto_20180427_1245.py b/apps/assets/migrations/0014_auto_20180427_1245.py deleted file mode 100644 index 735a50879..000000000 --- a/apps/assets/migrations/0014_auto_20180427_1245.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-27 04:45 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0013_auto_20180411_1135'), - ] - - operations = [ - migrations.AlterField( - model_name='adminuser', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='gateway', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='systemuser', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), - ), - ] diff --git a/apps/assets/migrations/0015_auto_20180510_1235.py b/apps/assets/migrations/0015_auto_20180510_1235.py deleted file mode 100644 index 81d12d2e4..000000000 --- a/apps/assets/migrations/0015_auto_20180510_1235.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-10 04:35 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0014_auto_20180427_1245'), - ] - - operations = [ - migrations.AlterField( - model_name='adminuser', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='gateway', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='systemuser', - name='username', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - ] diff --git a/apps/assets/migrations/0016_auto_20180511_1203.py b/apps/assets/migrations/0016_auto_20180511_1203.py deleted file mode 100644 index 32f79a3c6..000000000 --- a/apps/assets/migrations/0016_auto_20180511_1203.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-05-11 04:03 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0015_auto_20180510_1235'), - ] - - operations = [ - migrations.AlterField( - model_name='node', - name='value', - field=models.CharField(max_length=128, verbose_name='Value'), - ), - ] diff --git a/apps/assets/migrations/0017_auto_20180702_1415.py b/apps/assets/migrations/0017_auto_20180702_1415.py deleted file mode 100644 index 9950424a6..000000000 --- a/apps/assets/migrations/0017_auto_20180702_1415.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-07-02 06:15 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -def migrate_win_to_ssh_protocol(apps, schema_editor): - asset_model = apps.get_model("assets", "Asset") - db_alias = schema_editor.connection.alias - asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp') - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0016_auto_20180511_1203'), - ] - - operations = [ - migrations.AddField( - model_name='asset', - name='protocol', - field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'), - ), - migrations.AddField( - model_name='systemuser', - name='login_mode', - field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'), - ), - migrations.AlterField( - model_name='adminuser', - name='username', - field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='asset', - name='platform', - field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'), - ), - migrations.AlterField( - model_name='gateway', - name='username', - field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.AlterField( - model_name='systemuser', - name='protocol', - field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'), - ), - migrations.AlterField( - model_name='systemuser', - name='username', - field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), - ), - migrations.RunPython(migrate_win_to_ssh_protocol), - ] diff --git a/apps/assets/migrations/0018_auto_20180807_1116.py b/apps/assets/migrations/0018_auto_20180807_1116.py deleted file mode 100644 index c4e848b43..000000000 --- a/apps/assets/migrations/0018_auto_20180807_1116.py +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by Django 2.0.7 on 2018-08-07 03:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0017_auto_20180702_1415'), - ] - - operations = [ - migrations.AddField( - model_name='adminuser', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='asset', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='domain', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='gateway', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='label', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='node', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='systemuser', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AlterField( - model_name='adminuser', - name='name', - field=models.CharField(max_length=128, verbose_name='Name'), - ), - migrations.AlterField( - model_name='asset', - name='hostname', - field=models.CharField(max_length=128, verbose_name='Hostname'), - ), - migrations.AlterField( - model_name='gateway', - name='name', - field=models.CharField(max_length=128, verbose_name='Name'), - ), - migrations.AlterField( - model_name='systemuser', - name='name', - field=models.CharField(max_length=128, verbose_name='Name'), - ), - migrations.AlterUniqueTogether( - name='adminuser', - unique_together={('name', 'org_id')}, - ), - migrations.AlterUniqueTogether( - name='asset', - unique_together={('org_id', 'hostname')}, - ), - migrations.AlterUniqueTogether( - name='gateway', - unique_together={('name', 'org_id')}, - ), - migrations.AlterUniqueTogether( - name='systemuser', - unique_together={('name', 'org_id')}, - ), - ] diff --git a/apps/assets/migrations/0019_auto_20180816_1320.py b/apps/assets/migrations/0019_auto_20180816_1320.py deleted file mode 100644 index 0d468e511..000000000 --- a/apps/assets/migrations/0019_auto_20180816_1320.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.7 on 2018-08-16 05:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0018_auto_20180807_1116'), - ] - - operations = [ - migrations.AddField( - model_name='asset', - name='cpu_vcpus', - field=models.IntegerField(null=True, verbose_name='CPU vcpus'), - ), - migrations.AlterUniqueTogether( - name='label', - unique_together={('name', 'value', 'org_id')}, - ), - ] diff --git a/apps/assets/migrations/0029_auto_20190522_1114.py b/apps/assets/migrations/0029_auto_20190522_1114.py index e1d20bb5f..46836c846 100644 --- a/apps/assets/migrations/0029_auto_20190522_1114.py +++ b/apps/assets/migrations/0029_auto_20190522_1114.py @@ -3,14 +3,6 @@ from django.db import migrations -def migrate_assets_protocol(apps, schema_editor): - asset_model = apps.get_model("assets", "Asset") - db_alias = schema_editor.connection.alias - assets = asset_model.objects.using(db_alias).all() - for asset in assets: - asset.protocols.create(name=asset.protocol, port=asset.port) - - class Migration(migrations.Migration): dependencies = [ @@ -18,5 +10,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(migrate_assets_protocol), ] diff --git a/apps/assets/migrations/0032_auto_20190624_2108.py b/apps/assets/migrations/0032_auto_20190624_2108.py new file mode 100644 index 000000000..441f13cdb --- /dev/null +++ b/apps/assets/migrations/0032_auto_20190624_2108.py @@ -0,0 +1,75 @@ +# Generated by Django 2.1.7 on 2019-06-24 13:08 + +import assets.models.utils +import common.fields.model +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0031_auto_20190621_1332'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='_password', + field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), + ), + migrations.AlterField( + model_name='adminuser', + name='_private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='adminuser', + name='_public_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), + ), + migrations.AlterField( + model_name='authbook', + name='_password', + field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), + ), + migrations.AlterField( + model_name='authbook', + name='_private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='authbook', + name='_public_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), + ), + migrations.AlterField( + model_name='gateway', + name='_password', + field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), + ), + migrations.AlterField( + model_name='gateway', + name='_private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='gateway', + name='_public_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), + ), + migrations.AlterField( + model_name='systemuser', + name='_password', + field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), + ), + migrations.AlterField( + model_name='systemuser', + name='_private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), + ), + migrations.AlterField( + model_name='systemuser', + name='_public_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), + ), + ] diff --git a/apps/assets/migrations/0033_auto_20190624_2108.py b/apps/assets/migrations/0033_auto_20190624_2108.py new file mode 100644 index 000000000..dc89bc125 --- /dev/null +++ b/apps/assets/migrations/0033_auto_20190624_2108.py @@ -0,0 +1,73 @@ +# Generated by Django 2.1.7 on 2019-06-24 13:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0032_auto_20190624_2108'), + ] + + operations = [ + migrations.RenameField( + model_name='adminuser', + old_name='_private_key', + new_name='private_key', + ), + migrations.RenameField( + model_name='adminuser', + old_name='_public_key', + new_name='public_key', + ), + migrations.RenameField( + model_name='authbook', + old_name='_private_key', + new_name='private_key', + ), + migrations.RenameField( + model_name='authbook', + old_name='_public_key', + new_name='public_key', + ), + migrations.RenameField( + model_name='gateway', + old_name='_private_key', + new_name='private_key', + ), + migrations.RenameField( + model_name='gateway', + old_name='_public_key', + new_name='public_key', + ), + migrations.RenameField( + model_name='systemuser', + old_name='_private_key', + new_name='private_key', + ), + migrations.RenameField( + model_name='systemuser', + old_name='_public_key', + new_name='public_key', + ), + migrations.RenameField( + model_name='adminuser', + old_name='_password', + new_name='password', + ), + migrations.RenameField( + model_name='authbook', + old_name='_password', + new_name='password', + ), + migrations.RenameField( + model_name='gateway', + old_name='_password', + new_name='password', + ), + migrations.RenameField( + model_name='systemuser', + old_name='_password', + new_name='password', + ), + ] diff --git a/apps/assets/migrations/0034_auto_20190705_1348.py b/apps/assets/migrations/0034_auto_20190705_1348.py new file mode 100644 index 000000000..161ce2b30 --- /dev/null +++ b/apps/assets/migrations/0034_auto_20190705_1348.py @@ -0,0 +1,39 @@ +# Generated by Django 2.1.7 on 2019-07-05 05:48 + +from django.db import migrations +from django.db.models import F +from django.db.models import CharField, Value as V +from django.db.models.functions import Concat + + +def migrate_assets_protocol(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + db_alias = schema_editor.connection.alias + assets = asset_model.objects.using(db_alias).all().annotate( + protocols_new=Concat( + 'protocol', V('/'), 'port', + output_field=CharField(), + ), + ) + assets.update(protocols=F('protocols_new')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0033_auto_20190624_2108'), + ] + + operations = [ + migrations.RemoveField( + model_name='asset', + name='protocols', + ), + migrations.AddField( + model_name='asset', + name='protocols', + field=CharField(blank=True, default='ssh/22', max_length=128, verbose_name='Protocols'), + ), + migrations.RunPython(migrate_assets_protocol), + migrations.DeleteModel(name='Protocol'), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index b87a18796..4b97a5929 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -1,10 +1,11 @@ -from .user import * +from .asset import * from .label import Label +from .user import * from .cluster import * from .group import * from .domain import * from .node import * -from .asset import * from .cmd_filter import * +from .authbook import * from .utils import * from .authbook import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index cb094dfeb..a37aa16b1 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -6,18 +6,16 @@ import uuid import logging import random from functools import reduce -from collections import defaultdict +from collections import OrderedDict from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from django.core.cache import cache -from django.core.validators import MinValueValidator, MaxValueValidator from .user import AdminUser, SystemUser +from .utils import Connectivity from orgs.mixins import OrgModelMixin, OrgManager -__all__ = ['Asset', 'Protocol'] +__all__ = ['Asset', 'ProtocolsMixin'] logger = logging.getLogger(__name__) @@ -47,14 +45,12 @@ class AssetQuerySet(models.QuerySet): def valid(self): return self.active() - -class AssetManager(OrgManager): - def get_queryset(self): - queryset = super().get_queryset().prefetch_related("nodes", "protocols") - return queryset + def has_protocol(self, name): + return self.filter(protocols__contains=name) -class Protocol(models.Model): +class ProtocolsMixin: + protocols = '' PROTOCOL_SSH = 'ssh' PROTOCOL_RDP = 'rdp' PROTOCOL_TELNET = 'telnet' @@ -65,19 +61,42 @@ class Protocol(models.Model): (PROTOCOL_TELNET, 'telnet (beta)'), (PROTOCOL_VNC, 'vnc'), ) - PORT_VALIDATORS = [MaxValueValidator(65535), MinValueValidator(1)] - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, - default=PROTOCOL_SSH, verbose_name=_("Name")) - port = models.IntegerField(default=22, verbose_name=_("Port"), - validators=PORT_VALIDATORS) + @property + def protocols_as_list(self): + if not self.protocols: + return [] + return self.protocols.split(' ') - def __str__(self): - return "{}/{}".format(self.name, self.port) + @property + def protocols_as_dict(self): + d = OrderedDict() + protocols = self.protocols_as_list + for i in protocols: + if '/' not in i: + continue + name, port = i.split('/')[:2] + if not all([name, port]): + continue + d[name] = int(port) + return d + + @property + def protocols_as_json(self): + return [ + {"name": name, "port": port} + for name, port in self.protocols_as_dict.items() + ] + + def has_protocol(self, name): + return name in self.protocols_as_dict + + @property + def ssh_port(self): + return self.protocols_as_dict.get("ssh", 22) -class Asset(OrgModelMixin): +class Asset(ProtocolsMixin, OrgModelMixin): # Important PLATFORM_CHOICES = ( ('Linux', 'Linux'), @@ -92,12 +111,12 @@ class Asset(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) - protocol = models.CharField(max_length=128, default=Protocol.PROTOCOL_SSH, - choices=Protocol.PROTOCOL_CHOICES, + protocol = models.CharField(max_length=128, default=ProtocolsMixin.PROTOCOL_SSH, + choices=ProtocolsMixin.PROTOCOL_CHOICES, verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) - protocols = models.ManyToManyField('Protocol', verbose_name=_("Protocol")) + protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) @@ -133,14 +152,8 @@ class Asset(OrgModelMixin): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) - objects = AssetManager.from_queryset(AssetQuerySet)() - CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}' - UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) - CONNECTIVITY_CHOICES = ( - (UNREACHABLE, _("Unreachable")), - (REACHABLE, _('Reachable')), - (UNKNOWN, _("Unknown")), - ) + objects = OrgManager.from_queryset(AssetQuerySet)() + _connectivity = None def __str__(self): return '{0.hostname}({0.ip})'.format(self) @@ -150,41 +163,9 @@ class Asset(OrgModelMixin): warning = '' if not self.is_active: warning += ' inactive' - else: - return True, '' - return False, warning - - @property - def protocols_name(self): - names = [] - for protocol in self.protocols.all(): - names.append(protocol.name) - return names - - def has_protocol(self, name): - return name in self.protocols_name - - def get_protocol_by_name(self, name): - for i in self.protocols.all(): - if i.name.lower() == name.lower(): - return i - return None - - @property - def protocol_ssh(self): - return self.get_protocol_by_name("ssh") - - @property - def protocol_rdp(self): - return self.get_protocol_by_name("rdp") - - @property - def ssh_port(self): - if self.protocol_ssh: - port = self.protocol_ssh.port - else: - port = 22 - return port + if warning: + return False, warning + return True, warning def is_windows(self): if self.platform in ("Windows", "Windows2016"): @@ -215,20 +196,6 @@ class Asset(OrgModelMixin): nodes = list(reduce(lambda x, y: set(x) | set(y), nodes)) return nodes - @classmethod - def get_queryset_by_fullname_list(cls, fullname_list): - org_fullname_map = defaultdict(list) - for fullname in fullname_list: - hostname, org = cls.split_fullname(fullname) - org_fullname_map[org].append(hostname) - filter_arg = Q() - for org, hosts in org_fullname_map.items(): - if org.is_real(): - filter_arg |= Q(hostname__in=hosts, org_id=org.id) - else: - filter_arg |= Q(Q(org_id__isnull=True) | Q(org_id=''), hostname__in=hosts) - return Asset.objects.filter(filter_arg) - @property def cpu_info(self): info = "" @@ -250,15 +217,18 @@ class Asset(OrgModelMixin): @property def connectivity(self): + if self._connectivity: + return self._connectivity if not self.admin_user: - return self.UNKNOWN - return self.admin_user.get_connectivity_of(self) + return Connectivity.unknown() + connectivity = self.admin_user.get_asset_connectivity(self) + return connectivity @connectivity.setter def connectivity(self, value): if not self.admin_user: return - self.admin_user.set_connectivity_of(self, value) + self.admin_user.set_asset_connectivity(self, value) def get_auth_info(self): if not self.admin_user: @@ -303,10 +273,7 @@ class Asset(OrgModelMixin): 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, - 'protocols': [ - {"name": p.name, "port": p.port} - for p in self.protocols.all() - ], + 'protocols': self.protocols_as_list, 'platform': self.platform, } } @@ -321,20 +288,25 @@ class Asset(OrgModelMixin): @classmethod def generate_fake(cls, count=100): from random import seed, choice - import forgery_py from django.db import IntegrityError from .node import Node + from orgs.utils import get_current_org + from orgs.models import Organization + org = get_current_org() + if not org or not org.is_real(): + Organization.default().change_to() + nodes = list(Node.objects.all()) seed() for i in range(count): ip = [str(i) for i in random.sample(range(255), 4)] asset = cls(ip='.'.join(ip), - hostname=forgery_py.internet.user_name(True), + hostname='.'.join(ip), admin_user=choice(AdminUser.objects.all()), created_by='Fake') try: asset.save() - asset.protocols.create(name="ssh", port=22) + asset.protocols = 'ssh/22' if nodes and len(nodes) > 3: _nodes = random.sample(nodes, 3) else: diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index e61c3423d..85cb22606 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -3,12 +3,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.core.cache import cache from orgs.mixins import OrgManager - from .base import AssetUser -from ..const import ASSET_USER_CONN_CACHE_KEY __all__ = ['AuthBook'] @@ -32,6 +29,7 @@ class AuthBook(AssetUser): backend = "db" # 用于system user和admin_user的动态设置 _connectivity = None + CONN_CACHE_KEY = "ASSET_USER_CONN_{}" class Meta: verbose_name = _('AuthBook') @@ -65,20 +63,15 @@ class AuthBook(AssetUser): self._set_version() self._set_latest() - @property - def _conn_cache_key(self): - return ASSET_USER_CONN_CACHE_KEY.format(self.id) + def get_related_assets(self): + return [self.asset] + + def generate_id_with_asset(self, asset): + return self.id @property def connectivity(self): - if self._connectivity: - return self._connectivity - value = cache.get(self._conn_cache_key, self.UNKNOWN) - return value - - @connectivity.setter - def connectivity(self, value): - cache.set(self._conn_cache_key, value, 3600) + return self.get_asset_connectivity(self.asset) @property def keyword(self): diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index d58b97a18..bd85d144e 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -5,8 +5,8 @@ import uuid from hashlib import md5 import sshpubkeys -from django.db import models from django.core.cache import cache +from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings @@ -14,8 +14,9 @@ from common.utils import ( get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger ) from common.validators import alphanumeric +from common import fields from orgs.mixins import OrgModelMixin -from .utils import private_key_validator +from .utils import private_key_validator, Connectivity signer = get_signer() @@ -26,50 +27,25 @@ class AssetUser(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric]) - _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) - _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) + password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) + private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) + public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) - UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) - CONNECTIVITY_CHOICES = ( - (UNREACHABLE, _("Unreachable")), - (REACHABLE, _('Reachable')), - (UNKNOWN, _("Unknown")), - ) - CONNECTIVITY_CACHE_KEY = "CONNECTIVITY_{}" + CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY" + CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT" + ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" + ASSET_USER_CACHE_TIME = 3600 * 24 + _prefer = "system_user" - @property - def password(self): - if self._password: - return signer.unsign(self._password) - else: - return None - - @password.setter - def password(self, password_raw): - # raise AttributeError("Using set_auth do that") - self._password = signer.sign(password_raw) - - @property - def private_key(self): - if self._private_key: - return signer.unsign(self._private_key) - - @private_key.setter - def private_key(self, private_key_raw): - # raise AttributeError("Using set_auth do that") - self._private_key = signer.sign(private_key_raw) - @property def private_key_obj(self): - if self._private_key: - key_str = signer.unsign(self._private_key) - return ssh_key_string_to_obj(key_str, password=self.password) + if self.private_key: + return ssh_key_string_to_obj(self.private_key, password=self.password) else: return None @@ -79,27 +55,13 @@ class AssetUser(OrgModelMixin): return None project_dir = settings.PROJECT_DIR tmp_dir = os.path.join(project_dir, 'tmp') - key_str = signer.unsign(self._private_key) - key_name = '.' + md5(key_str.encode('utf-8')).hexdigest() + key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() key_path = os.path.join(tmp_dir, key_name) if not os.path.exists(key_path): self.private_key_obj.write_private_key_file(key_path) os.chmod(key_path, 0o400) return key_path - @property - def public_key(self): - key = signer.unsign(self._public_key) - if key: - return key - else: - return None - - @public_key.setter - def public_key(self, public_key_raw): - # raise AttributeError("Using set_auth do that") - self._public_key = signer.sign(public_key_raw) - @property def public_key_obj(self): if self.public_key: @@ -109,47 +71,119 @@ class AssetUser(OrgModelMixin): pass return None + @property + def part_id(self): + i = '-'.join(str(self.id).split('-')[:3]) + return i + + def get_related_assets(self): + assets = self.assets.all() + return assets + def set_auth(self, password=None, private_key=None, public_key=None): update_fields = [] if password: - self._password = signer.sign(password) - update_fields.append('_password') + self.password = password + update_fields.append('password') if private_key: - self._private_key = signer.sign(private_key) - update_fields.append('_private_key') + self.private_key = private_key + update_fields.append('private_key') if public_key: - self._public_key = signer.sign(public_key) - update_fields.append('_public_key') + self.public_key = public_key + update_fields.append('public_key') if update_fields: self.save(update_fields=update_fields) - def get_auth(self, asset=None): - pass + def set_connectivity(self, summary): + unreachable = summary.get('dark', {}).keys() + reachable = summary.get('contacted', {}).keys() - def get_connectivity_of(self, asset): - i = self.generate_id_with_asset(asset) - key = self.CONNECTIVITY_CACHE_KEY.format(i) - return cache.get(key) + assets = self.get_related_assets() + if not isinstance(assets, list): + assets = assets.only('id', 'hostname', 'admin_user__id') + for asset in assets: + if asset.hostname in unreachable: + self.set_asset_connectivity(asset, Connectivity.unreachable()) + elif asset.hostname in reachable: + self.set_asset_connectivity(asset, Connectivity.reachable()) + else: + self.set_asset_connectivity(asset, Connectivity.unknown()) + cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id) + cache.delete(cache_key) - def set_connectivity_of(self, asset, c): - i = self.generate_id_with_asset(asset) - key = self.CONNECTIVITY_CACHE_KEY.format(i) - cache.set(key, c, 3600) + @property + def connectivity(self): + assets = self.get_related_assets() + if not isinstance(assets, list): + assets = assets.only('id', 'hostname', 'admin_user__id') + data = { + 'unreachable': [], + 'reachable': [], + 'unknown': [], + } + for asset in assets: + connectivity = self.get_asset_connectivity(asset) + if connectivity.is_reachable(): + data["reachable"].append(asset.hostname) + elif connectivity.is_unreachable(): + data["unreachable"].append(asset.hostname) + else: + data["unknown"].append(asset.hostname) + return data - def load_specific_asset_auth(self, asset): + @property + def connectivity_amount(self): + cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id) + amount = cache.get(cache_key) + if not amount: + amount = {k: len(v) for k, v in self.connectivity.items()} + cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME) + return amount + + @property + def assets_amount(self): + cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) + cached = cache.get(cache_key) + if not cached: + cached = self.get_related_assets().count() + cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME) + return cached + + def expire_assets_amount(self): + cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) + cache.delete(cache_key) + + def get_asset_connectivity(self, asset): + key = self.get_asset_connectivity_key(asset) + return Connectivity.get(key) + + def get_asset_connectivity_key(self, asset): + return self.CONNECTIVITY_ASSET_CACHE_KEY.format(self.username, asset.id) + + def set_asset_connectivity(self, asset, c): + key = self.get_asset_connectivity_key(asset) + Connectivity.set(key, c) + # 当为某个系统用户或管理用户设置的的时候,失效掉他们的连接数量 + amount_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, '*') + cache.delete_pattern(amount_key) + + def get_asset_user(self, asset): from ..backends import AssetUserManager try: manager = AssetUserManager().prefer(self._prefer) - other = manager.get(username=self.username, asset=asset) + other = manager.get(username=self.username, asset=asset, prefer_id=self.id) + return other except Exception as e: logger.error(e, exc_info=True) - else: - self._merge_auth(other) + return None + + def load_specific_asset_auth(self, asset): + instance = self.get_asset_user(asset) + if instance: + self._merge_auth(instance) def _merge_auth(self, other): - if not other: - return if other.password: self.password = other.password if other.public_key: @@ -158,9 +192,9 @@ class AssetUser(OrgModelMixin): self.private_key = other.private_key def clear_auth(self): - self._password = '' - self._private_key = '' - self._public_key = '' + self.password = '' + self.private_key = '' + self.public_key = '' self.save() def auto_gen_auth(self): @@ -168,9 +202,10 @@ class AssetUser(OrgModelMixin): private_key, public_key = ssh_key_gen( username=self.username ) - self.set_auth(password=password, - private_key=private_key, - public_key=public_key) + self.set_auth( + password=password, private_key=private_key, + public_key=public_key + ) def auto_gen_auth_password(self): password = str(uuid.uuid4()) @@ -187,20 +222,20 @@ class AssetUser(OrgModelMixin): } def generate_id_with_asset(self, asset): - id_ = '{}_{}'.format(asset.id, self.id) - id_ = uuid.UUID(md5(id_.encode()).hexdigest()) - return id_ + user_id = [self.part_id] + asset_id = str(asset.id).split('-')[3:] + ids = user_id + asset_id + return '-'.join(ids) def construct_to_authbook(self, asset): from . import AuthBook fields = [ 'name', 'username', 'comment', 'org_id', - '_password', '_private_key', '_public_key', + 'password', 'private_key', 'public_key', 'date_created', 'date_updated', 'created_by' ] - id_ = self.generate_id_with_asset(asset) - obj = AuthBook(id=id_, asset=asset, version=0, is_latest=True) - obj._connectivity = self.get_connectivity_of(asset) + i = self.generate_id_with_asset(asset) + obj = AuthBook(id=i, asset=asset, version=0, is_latest=True) for field in fields: value = getattr(self, field) setattr(obj, field, value) @@ -208,3 +243,4 @@ class AssetUser(OrgModelMixin): class Meta: abstract = True + diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 7f7487f06..aab5ebaf4 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # import uuid +import re from django.db import models, transaction from django.db.models import Q @@ -8,61 +9,195 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.core.cache import cache -from orgs.mixins import OrgModelMixin +from orgs.mixins import OrgModelMixin, OrgManager from orgs.utils import set_current_org, get_current_org from orgs.models import Organization __all__ = ['Node'] -class Node(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' - value = models.CharField(max_length=128, verbose_name=_("Value")) - child_mark = models.IntegerField(default=0) - date_create = models.DateTimeField(auto_now_add=True) +class NodeQuerySet(models.QuerySet): + def delete(self): + raise PermissionError("Bulk delete node deny") + +class FamilyMixin: + _parents = None + _children = None + _all_children = None is_node = True - _assets_amount = None - _full_value_cache_key = '_NODE_VALUE_{}' - _assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}' - - class Meta: - verbose_name = _("Node") - ordering = ['key'] - - def __str__(self): - return self.full_value - - def __eq__(self, other): - if not other: - return False - return self.id == other.id - - def __gt__(self, other): - if self.is_root() and not other.is_root(): - return True - elif not self.is_root() and other.is_root(): - return False - self_key = [int(k) for k in self.key.split(':')] - other_key = [int(k) for k in other.key.split(':')] - self_parent_key = self_key[:-1] - other_parent_key = other_key[:-1] - - if self_parent_key == other_parent_key: - return self.name > other.name - if len(self_parent_key) < len(other_parent_key): - return True - elif len(self_parent_key) > len(other_parent_key): - return False - return self_key > other_key - - def __lt__(self, other): - return not self.__gt__(other) @property - def name(self): - return self.value + def children(self): + if self._children: + return self._children + pattern = r'^{0}:[0-9]+$'.format(self.key) + return Node.objects.filter(key__regex=pattern) + + @children.setter + def children(self, value): + self._children = value + + @property + def all_children(self): + if self._all_children: + return self._all_children + pattern = r'^{0}:'.format(self.key) + return Node.objects.filter( + key__regex=pattern + ) + + def get_children(self, with_self=False): + children = list(self.children) + if with_self: + children.append(self) + return children + + def get_all_children(self, with_self=False): + children = self.all_children + if with_self: + children = list(children) + children.append(self) + return children + + @property + def parents(self): + if self._parents: + return self._parents + ancestor_keys = self.get_ancestor_keys() + ancestor = Node.objects.filter( + key__in=ancestor_keys + ).order_by('key') + return ancestor + + @parents.setter + def parents(self, value): + self._parents = value + + def get_ancestor(self, with_self=False): + parents = self.parents + if with_self: + parents = list(parents) + parents.append(self) + return parents + + @property + def parent(self): + if self._parents: + return self._parents[0] + if self.is_root(): + return self + try: + parent = Node.objects.get(key=self.parent_key) + return parent + except Node.DoesNotExist: + return Node.root() + + @parent.setter + def parent(self, parent): + if not self.is_node: + self.key = parent.key + ':fake' + return + children = self.get_all_children() + old_key = self.key + with transaction.atomic(): + self.key = parent.get_next_child_key() + for child in children: + child.key = child.key.replace(old_key, self.key, 1) + child.save() + self.save() + + def get_sibling(self, with_self=False): + key = ':'.join(self.key.split(':')[:-1]) + pattern = r'^{}:[0-9]+$'.format(key) + sibling = Node.objects.filter( + key__regex=pattern.format(self.key) + ) + if not with_self: + sibling = sibling.exclude(key=self.key) + return sibling + + def get_family(self): + ancestor = self.get_ancestor() + children = self.get_all_children() + return [*tuple(ancestor), self, *tuple(children)] + + def get_ancestor_keys(self, with_self=False): + parent_keys = [] + key_list = self.key.split(":") + if not with_self: + key_list.pop() + for i in range(len(key_list)): + parent_keys.append(":".join(key_list)) + key_list.pop() + return parent_keys + + def is_children(self, other): + pattern = re.compile(r'^{0}:[0-9]+$'.format(self.key)) + return pattern.match(other.key) + + def is_parent(self, other): + pattern = re.compile(r'^{0}:[0-9]+$'.format(other.key)) + return pattern.match(self.key) + + @property + def parent_key(self): + parent_key = ":".join(self.key.split(":")[:-1]) + return parent_key + + @property + def parents_keys(self, with_self=False): + keys = [] + key_list = self.key.split(":") + if not with_self: + key_list.pop() + for i in range(len(key_list)): + keys.append(':'.join(key_list)) + key_list.pop() + return keys + + +class FullValueMixin: + _full_value_cache_key = '_NODE_VALUE_{}' + _full_value = '' + key = '' + + @property + def full_value(self): + if self._full_value: + return self._full_value + key = self._full_value_cache_key.format(self.key) + cached = cache.get(key) + if cached: + return cached + if self.is_root(): + return self.value + parent_full_value = self.parent.full_value + value = parent_full_value + ' / ' + self.value + self.full_value = value + return value + + @full_value.setter + def full_value(self, value): + self._full_value = value + key = self._full_value_cache_key.format(self.key) + cache.set(key, value, 3600*24) + + def expire_full_value(self): + key = self._full_value_cache_key.format(self.key) + cache.delete_pattern(key+'*') + + @classmethod + def expire_nodes_full_value(cls, nodes=None): + key = cls._full_value_cache_key.format('*') + cache.delete_pattern(key+'*') + + +class AssetsAmountMixin: + _assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}' + _assets_amount = None + key = '' + cache_time = 3600 * 24 * 7 @property def assets_amount(self): @@ -77,53 +212,82 @@ class Node(OrgModelMixin): if cached is not None: return cached assets_amount = self.get_all_assets().count() - cache.set(cache_key, assets_amount, 3600) + self.assets_amount = assets_amount return assets_amount @assets_amount.setter def assets_amount(self, value): self._assets_amount = value + cache_key = self._assets_amount_cache_key.format(self.key) + cache.set(cache_key, value, self.cache_time) def expire_assets_amount(self): ancestor_keys = self.get_ancestor_keys(with_self=True) - cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys] + cache_keys = [self._assets_amount_cache_key.format(k) for k in + ancestor_keys] cache.delete_many(cache_keys) @classmethod def expire_nodes_assets_amount(cls, nodes=None): - if nodes: - for node in nodes: - node.expire_assets_amount() - return key = cls._assets_amount_cache_key.format('*') cache.delete_pattern(key) - @property - def full_value(self): - key = self._full_value_cache_key.format(self.key) - cached = cache.get(key) - if cached: - return cached - if self.is_root(): - return self.value - parent_full_value = self.parent.full_value - value = parent_full_value + ' / ' + self.value - key = self._full_value_cache_key.format(self.key) - cache.set(key, value, 3600) - return value - - def expire_full_value(self): - key = self._full_value_cache_key.format(self.key) - cache.delete_pattern(key+'*') - @classmethod - def expire_nodes_full_value(cls, nodes=None): - if nodes: - for node in nodes: - node.expire_full_value() - return - key = cls._full_value_cache_key.format('*') - cache.delete_pattern(key+'*') + def refresh_nodes(cls): + from ..utils import NodeUtil + util = NodeUtil(with_assets_amount=True) + util.set_assets_amount() + util.set_full_value() + + +class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' + value = models.CharField(max_length=128, verbose_name=_("Value")) + child_mark = models.IntegerField(default=0) + date_create = models.DateTimeField(auto_now_add=True) + + objects = OrgManager.from_queryset(NodeQuerySet)() + is_node = True + _parents = None + + class Meta: + verbose_name = _("Node") + ordering = ['key'] + + def __str__(self): + return self.full_value + + def __eq__(self, other): + if not other: + return False + return self.id == other.id + + def __gt__(self, other): + # if self.is_root() and not other.is_root(): + # return False + # elif not self.is_root() and other.is_root(): + # return True + self_key = [int(k) for k in self.key.split(':')] + other_key = [int(k) for k in other.key.split(':')] + self_parent_key = self_key[:-1] + other_parent_key = other_key[:-1] + + if self_parent_key and other_parent_key and \ + self_parent_key == other_parent_key: + return self.value > other.value + # if len(self_parent_key) < len(other_parent_key): + # return True + # elif len(self_parent_key) > len(other_parent_key): + # return False + return self_key > other_key + + def __lt__(self, other): + return not self.__gt__(other) + + @property + def name(self): + return self.value @property def level(self): @@ -152,33 +316,6 @@ class Node(OrgModelMixin): child = self.__class__.objects.create(id=_id, key=child_key, value=value) return child - def get_children(self, with_self=False): - pattern = r'^{0}$|^{0}:[0-9]+$' if with_self else r'^{0}:[0-9]+$' - return self.__class__.objects.filter( - key__regex=pattern.format(self.key) - ) - - def get_all_children(self, with_self=False): - pattern = r'^{0}$|^{0}:' if with_self else r'^{0}:' - return self.__class__.objects.filter( - key__regex=pattern.format(self.key) - ) - - def get_sibling(self, with_self=False): - key = ':'.join(self.key.split(':')[:-1]) - pattern = r'^{}:[0-9]+$'.format(key) - sibling = self.__class__.objects.filter( - key__regex=pattern.format(self.key) - ) - if not with_self: - sibling = sibling.exclude(key=self.key) - return sibling - - def get_family(self): - ancestor = self.get_ancestor() - children = self.get_all_children() - return [*tuple(ancestor), self, *tuple(children)] - def get_assets(self): from .asset import Asset if self.is_default_node(): @@ -206,7 +343,7 @@ class Node(OrgModelMixin): return self.get_all_assets().valid() def is_default_node(self): - return self.is_root() and self.key == '0' + return self.is_root() and self.key == '1' def is_root(self): if self.key.isdigit(): @@ -214,52 +351,6 @@ class Node(OrgModelMixin): else: return False - @property - def parent_key(self): - parent_key = ":".join(self.key.split(":")[:-1]) - return parent_key - - @property - def parent(self): - if self.is_root(): - return self - try: - parent = self.__class__.objects.get(key=self.parent_key) - return parent - except Node.DoesNotExist: - return self.__class__.root() - - @parent.setter - def parent(self, parent): - if not self.is_node: - self.key = parent.key + ':fake' - return - children = self.get_all_children() - old_key = self.key - with transaction.atomic(): - self.key = parent.get_next_child_key() - for child in children: - child.key = child.key.replace(old_key, self.key, 1) - child.save() - self.save() - - def get_ancestor_keys(self, with_self=False): - parent_keys = [] - key_list = self.key.split(":") - if not with_self: - key_list.pop() - for i in range(len(key_list)): - parent_keys.append(":".join(key_list)) - key_list.pop() - return parent_keys - - def get_ancestor(self, with_self=False): - ancestor_keys = self.get_ancestor_keys(with_self=with_self) - ancestor = self.__class__.objects.filter( - key__in=ancestor_keys - ).order_by('key') - return ancestor - @classmethod def create_root_node(cls): # 如果使用current_org 在set_current_org时会死循环 @@ -292,9 +383,7 @@ class Node(OrgModelMixin): def as_tree_node(self): from common.tree import TreeNode - from ..serializers import NodeSerializer name = '{} ({})'.format(self.value, self.assets_amount) - node_serializer = NodeSerializer(instance=self) data = { 'id': self.key, 'name': name, @@ -303,16 +392,37 @@ class Node(OrgModelMixin): 'isParent': True, 'open': self.is_root(), 'meta': { - 'node': node_serializer.data, + 'node': { + "id": self.id, + "name": self.name, + "value": self.value, + "key": self.key, + "assets_amount": self.assets_amount, + }, 'type': 'node' } } tree_node = TreeNode(**data) return tree_node + def delete(self, using=None, keep_parents=False): + if self.children or self.get_assets(): + return + return super().delete(using=using, keep_parents=keep_parents) + + @classmethod + def get_queryset(cls): + from ..utils import NodeUtil + util = NodeUtil() + return sorted(util.nodes) + @classmethod def generate_fake(cls, count=100): import random + org = get_current_org() + if not org or not org.is_real(): + Organization.default().change_to() + for i in range(count): node = random.choice(cls.objects.all()) node.create_child('Node {}'.format(i)) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 2cded41a1..32f569b80 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -4,13 +4,11 @@ import logging -from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import get_signer -from ..const import SYSTEM_USER_CONN_CACHE_KEY from .base import AssetUser @@ -31,7 +29,7 @@ class AdminUser(AssetUser): become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) become_user = models.CharField(default='root', max_length=64) _become_pass = models.CharField(default='', max_length=128) - CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}' + CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}' _prefer = "admin_user" def __str__(self): @@ -61,31 +59,6 @@ class AdminUser(AssetUser): info = None return info - def get_related_assets(self): - assets = self.assets.all() - return assets - - @property - def assets_amount(self): - return self.get_related_assets().count() - - @property - def connectivity(self): - from .asset import Asset - assets = self.get_related_assets().values_list('id', 'hostname', flat=True) - data = { - 'unreachable': [], - 'reachable': [], - } - for asset_id, hostname in assets: - key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id)) - value = cache.get(key, Asset.UNKNOWN) - if value == Asset.REACHABLE: - data['reachable'].append(hostname) - elif value == Asset.UNREACHABLE: - data['unreachable'].append(hostname) - return data - class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] @@ -141,9 +114,6 @@ class SystemUser(AssetUser): login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) - SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}" - CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}' - def __str__(self): return '{0.name}({0.username})'.format(self) @@ -157,49 +127,6 @@ class SystemUser(AssetUser): 'auto_push': self.auto_push, } - def get_related_assets(self): - assets = set(self.assets.all()) - return assets - - @property - def connectivity(self): - cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id)) - value = cache.get(cache_key, None) - if not value or 'unreachable' not in value: - return {'unreachable': [], 'reachable': []} - else: - return value - - @connectivity.setter - def connectivity(self, value): - data = self.connectivity - unreachable = data['unreachable'] - reachable = data['reachable'] - assets = {asset.hostname: asset for asset in self.assets.all()} - - for host in value.get('dark', {}).keys(): - if host not in unreachable: - unreachable.append(host) - if host in reachable: - reachable.remove(host) - self.set_connectivity_of(assets.get(host), self.UNREACHABLE) - for host in value.get('contacted'): - if host not in reachable: - reachable.append(host) - if host in unreachable: - unreachable.remove(host) - self.set_connectivity_of(assets.get(host), self.REACHABLE) - cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id)) - cache.set(cache_key, data, 3600) - - @property - def assets_unreachable(self): - return self.connectivity.get('unreachable') - - @property - def assets_reachable(self): - return self.connectivity.get('reachable') - @property def login_mode_display(self): return self.get_login_mode_display() @@ -210,12 +137,6 @@ class SystemUser(AssetUser): else: return False - def set_cache(self): - cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600) - - def expire_cache(self): - cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id)) - @property def cmd_filter_rules(self): from .cmd_filter import CommandFilterRule @@ -233,18 +154,6 @@ class SystemUser(AssetUser): return False, matched_cmd return True, None - @classmethod - def get_system_user_by_id_or_cached(cls, sid): - cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid)) - if cached: - return cached - try: - system_user = cls.objects.get(id=sid) - system_user.set_cache() - return system_user - except cls.DoesNotExist: - return None - class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 11d347685..d010f95f2 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -2,11 +2,17 @@ # -*- coding: utf-8 -*- # +from django.utils import timezone +from django.core.cache import cache from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + from common.utils import validate_ssh_private_key -__all__ = ['init_model', 'generate_fake'] +__all__ = [ + 'init_model', 'generate_fake', 'private_key_validator', 'Connectivity', +] def init_model(): @@ -31,5 +37,72 @@ def private_key_validator(value): ) -if __name__ == '__main__': - pass +class Connectivity: + UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) + CONNECTIVITY_CHOICES = ( + (UNREACHABLE, _("Unreachable")), + (REACHABLE, _('Reachable')), + (UNKNOWN, _("Unknown")), + ) + + status = UNKNOWN + datetime = timezone.now() + + def __init__(self, status, datetime): + self.status = status + self.datetime = datetime + + def display(self): + return dict(self.__class__.CONNECTIVITY_CHOICES).get(self.status) + + def is_reachable(self): + return self.status == self.REACHABLE + + def is_unreachable(self): + return self.status == self.UNREACHABLE + + def is_unknown(self): + return self.status == self.UNKNOWN + + @classmethod + def unreachable(cls): + return cls(cls.UNREACHABLE, timezone.now()) + + @classmethod + def reachable(cls): + return cls(cls.REACHABLE, timezone.now()) + + @classmethod + def unknown(cls): + return cls(cls.UNKNOWN, timezone.now()) + + @classmethod + def set(cls, key, value, ttl=0): + cache.set(key, value, ttl) + + @classmethod + def get(cls, key): + value = cache.get(key, cls.unknown()) + if not isinstance(value, cls): + value = cls.unknown() + return value + + @classmethod + def set_unreachable(cls, key, ttl=0): + cls.set(key, cls.unreachable(), ttl) + + @classmethod + def set_reachable(cls, key, ttl=0): + cls.set(key, cls.reachable(), ttl) + + def __eq__(self, other): + return self.status == other.status + + def __gt__(self, other): + return self.status > other.status + + def __lt__(self, other): + return not self.__gt__(other) + + def __str__(self): + return self.display() diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 765559e70..dbbb16406 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -1,13 +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.serializers import AdaptedBulkListSerializer from ..models import Node, AdminUser -from ..const import ADMIN_USER_CONN_CACHE_KEY from orgs.mixins import BulkOrgResourceModelSerializer from .base import AuthSerializer @@ -17,54 +15,27 @@ class AdminUserSerializer(BulkOrgResourceModelSerializer): """ 管理用户 """ - password = serializers.CharField( - required=False, write_only=True, label=_('Password') - ) - unreachable_amount = serializers.SerializerMethodField(label=_('Unreachable')) - assets_amount = serializers.SerializerMethodField(label=_('Asset')) - reachable_amount = serializers.SerializerMethodField(label=_('Reachable')) class Meta: list_serializer_class = AdaptedBulkListSerializer model = AdminUser fields = [ - 'id', 'name', 'username', 'assets_amount', - 'reachable_amount', 'unreachable_amount', 'password', 'comment', - 'date_created', 'date_updated', 'become', 'become_method', - 'become_user', 'created_by', + 'id', 'name', 'username', 'password', 'private_key', 'public_key', + 'comment', 'connectivity_amount', 'assets_amount', + 'date_created', 'date_updated', 'created_by', ] extra_kwargs = { - 'date_created': {'label': _('Date created')}, - 'date_updated': {'label': _('Date updated')}, - 'become': {'read_only': True}, 'become_method': {'read_only': True}, - 'become_user': {'read_only': True}, 'created_by': {'read_only': True} + 'password': {"write_only": True}, + 'private_key': {"write_only": True}, + 'public_key': {"write_only": True}, + 'date_created': {'read_only': True}, + 'date_updated': {'read_only': True}, + 'created_by': {'read_only': True}, + 'assets_amount': {'label': _('Asset')}, + 'connectivity_amount': {'label': _('Connectivity')}, } - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - return [f for f in fields if not f.startswith('_')] - - @staticmethod - def get_unreachable_amount(obj): - data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) - if data: - return len(data.get('dark')) - else: - return 0 - - @staticmethod - def get_reachable_amount(obj): - data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) - if data: - return len(data.get('contacted')) - else: - return 0 - - @staticmethod - def get_assets_amount(obj): - return obj.assets_amount - class AdminUserAuthSerializer(AuthSerializer): diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 77715b351..5fdb3a362 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,48 +1,67 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from rest_framework.validators import ValidationError - +from django.db.models import Prefetch from django.utils.translation import ugettext_lazy as _ from orgs.mixins import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer -from ..models import Asset, Protocol -from .system_user import AssetSystemUserSerializer +from ..models import Asset, Node, Label +from .base import ConnectivitySerializer __all__ = [ - 'AssetSerializer', 'AssetGrantedSerializer', 'AssetSimpleSerializer', - 'ProtocolSerializer', + 'AssetSerializer', 'AssetSimpleSerializer', + 'ProtocolsField', ] -class ProtocolSerializer(serializers.ModelSerializer): - class Meta: - model = Protocol - fields = ["name", "port"] +class ProtocolField(serializers.RegexField): + protocols = '|'.join(dict(Asset.PROTOCOL_CHOICES).keys()) + default_error_messages = { + 'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535')) + } + regex = r'^(%s)/(\d{1,5})$' % protocols + + def __init__(self, *args, **kwargs): + super().__init__(self.regex, **kwargs) -class ProtocolsRelatedField(serializers.RelatedField): +def validate_duplicate_protocols(values): + errors = [] + names = [] + + for value in values: + if not value or '/' not in value: + continue + name = value.split('/')[0] + if name in names: + errors.append(_("Protocol duplicate: {}").format(name)) + names.append(name) + errors.append('') + if any(errors): + raise serializers.ValidationError(errors) + + +class ProtocolsField(serializers.ListField): + default_validators = [validate_duplicate_protocols] + + def __init__(self, *args, **kwargs): + kwargs['child'] = ProtocolField() + kwargs['allow_null'] = True + kwargs['allow_empty'] = True + kwargs['min_length'] = 1 + kwargs['max_length'] = 4 + super().__init__(*args, **kwargs) + def to_representation(self, value): - return str(value) - - def to_internal_value(self, data): - if isinstance(data, dict): - return data - if '/' not in data: - raise ValidationError("protocol not contain /: {}".format(data)) - v = data.split("/") - if len(v) != 2: - raise ValidationError("protocol format should be name/port: {}".format(data)) - name, port = v - cleaned_data = {"name": name, "port": port} - return cleaned_data + if not value: + return [] + return value.split(' ') class AssetSerializer(BulkOrgResourceModelSerializer): - protocols = ProtocolsRelatedField( - many=True, queryset=Protocol.objects.all(), label=_("Protocols") - ) + protocols = ProtocolsField(label=_('Protocols'), required=False) + connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) """ 资产的数据结构 @@ -57,7 +76,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): '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', 'connectivity' + 'hardware_info', 'connectivity', ] read_only_fields = ( 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', @@ -69,121 +88,41 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'protocol': {'write_only': True}, 'port': {'write_only': True}, 'hardware_info': {'label': _('Hardware info')}, - 'connectivity': {'label': _('Connectivity')}, 'org_name': {'label': _('Org name')} } @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('labels', 'nodes')\ - .select_related('admin_user') + 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') return queryset - @staticmethod - def validate_protocols(attr): - protocols_serializer = ProtocolSerializer(data=attr, many=True) - protocols_serializer.is_valid(raise_exception=True) - protocols_name = [i.get("name", "ssh") for i in attr] - errors = [{} for i in protocols_name] - for i, name in enumerate(protocols_name): - if name in protocols_name[:i]: - errors[i] = {"name": _("Protocol duplicate: {}").format(name)} - if any(errors): - raise ValidationError(errors) - return attr - - def create(self, validated_data): + def compatible_with_old_protocol(self, validated_data): protocols_data = validated_data.pop("protocols", []) # 兼容老的api - protocol = validated_data.get("protocol") + name = validated_data.get("protocol") port = validated_data.get("port") - if not protocols_data and protocol and port: - protocols_data = [{"name": protocol, "port": port}] + if not protocols_data and name and port: + protocols_data.insert(0, '/'.join([name, str(port)])) + elif not name and not port and protocols_data: + protocol = protocols_data[0].split('/') + validated_data["protocol"] = protocol[0] + validated_data["port"] = int(protocol[1]) + if validated_data: + validated_data["protocols"] = ' '.join(protocols_data) - if not protocol and not port and protocols_data: - validated_data["protocol"] = protocols_data[0]["name"] - validated_data["port"] = protocols_data[0]["port"] - - protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) - protocols_serializer.is_valid(raise_exception=True) - protocols = protocols_serializer.save() + def create(self, validated_data): + self.compatible_with_old_protocol(validated_data) instance = super().create(validated_data) - instance.protocols.set(protocols) return instance def update(self, instance, validated_data): - protocols_data = validated_data.pop("protocols", []) - - # 兼容老的api - protocol = validated_data.get("protocol") - port = validated_data.get("port") - if not protocols_data and protocol and port: - protocols_data = [{"name": protocol, "port": port}] - - if not protocol and not port and protocols_data: - validated_data["protocol"] = protocols_data[0]["name"] - validated_data["port"] = protocols_data[0]["port"] - protocols = None - if protocols_data: - protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) - protocols_serializer.is_valid(raise_exception=True) - protocols = protocols_serializer.save() - - instance = super().update(instance, validated_data) - if protocols: - instance.protocols.all().delete() - instance.protocols.set(protocols) - return instance - - -# class AssetAsNodeSerializer(serializers.ModelSerializer): -# protocols = ProtocolSerializer(many=True) -# -# class Meta: -# model = Asset -# fields = ['id', 'hostname', 'ip', 'platform', 'protocols'] - - -class AssetGrantedSerializer(serializers.ModelSerializer): - """ - 被授权资产的数据结构 - """ - protocols = ProtocolsRelatedField( - many=True, queryset=Protocol.objects.all(), label=_("Protocols") - ) - system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) - system_users_join = serializers.SerializerMethodField() - # nodes = NodeTMPSerializer(many=True, read_only=True) - - class Meta: - model = Asset - fields = ( - "id", "hostname", "ip", "protocol", "port", "protocols", - "system_users_granted", "is_active", "system_users_join", "os", - 'domain', "platform", "comment", "org_id", "org_name", - ) - - @staticmethod - def get_system_users_join(obj): - system_users = [s.username for s in obj.system_users_granted] - return ', '.join(system_users) - - -# class MyAssetGrantedSerializer(AssetGrantedSerializer): -# """ -# 普通用户获取授权的资产定义的数据结构 -# """ -# protocols = ProtocolSerializer(many=True) -# -# class Meta: -# model = Asset -# fields = ( -# "id", "hostname", "system_users_granted", -# "is_active", "system_users_join", "org_name", -# "os", "platform", "comment", "org_id", "protocols" -# ) + self.compatible_with_old_protocol(validated_data) + return super().update(instance, validated_data) class AssetSimpleSerializer(serializers.ModelSerializer): diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index bb5f83142..a111f1993 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -4,11 +4,12 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from ..models import AuthBook, Asset -from ..backends import AssetUserManager from common.utils import validate_ssh_private_key from common.serializers import AdaptedBulkListSerializer from orgs.mixins import BulkOrgResourceModelSerializer +from ..models import AuthBook, Asset +from ..backends import AssetUserManager +from .base import ConnectivitySerializer __all__ = [ @@ -26,20 +27,8 @@ class BasicAssetSerializer(serializers.ModelSerializer): class AssetUserSerializer(BulkOrgResourceModelSerializer): hostname = serializers.CharField(read_only=True, label=_("Hostname")) ip = serializers.CharField(read_only=True, label=_("IP")) - connectivity = serializers.CharField(read_only=True, label=_("Connectivity")) + connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) - password = serializers.CharField( - max_length=256, allow_blank=True, allow_null=True, write_only=True, - required=False, label=_('Password') - ) - public_key = serializers.CharField( - max_length=4096, allow_blank=True, allow_null=True, write_only=True, - required=False, label=_('Public key') - ) - private_key = serializers.CharField( - max_length=4096, allow_blank=True, allow_null=True, write_only=True, - required=False, label=_('Private key') - ) backend = serializers.CharField(read_only=True, label=_("Backend")) class Meta: @@ -56,6 +45,9 @@ class AssetUserSerializer(BulkOrgResourceModelSerializer): ] extra_kwargs = { 'username': {'required': True}, + 'password': {'write_only': True}, + 'private_key': {'write_only': True}, + 'public_key': {'write_only': True}, } def validate_private_key(self, key): @@ -66,17 +58,9 @@ class AssetUserSerializer(BulkOrgResourceModelSerializer): return key def create(self, validated_data): - kwargs = { - 'name': validated_data.get('username'), - 'username': validated_data.get('username'), - 'asset': validated_data.get('asset'), - 'comment': validated_data.get('comment', ''), - 'org_id': validated_data.get('org_id', ''), - 'password': validated_data.get('password'), - 'public_key': validated_data.get('public_key'), - 'private_key': validated_data.get('private_key') - } - instance = AssetUserManager.create(**kwargs) + if not validated_data.get("name") and validated_data.get("username"): + validated_data["name"] = validated_data["username"] + instance = AssetUserManager.create(**validated_data) return instance diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index bb34813b6..3d0fcd58b 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -24,3 +24,8 @@ class AuthSerializer(serializers.ModelSerializer): self.instance.set_auth(password=password, private_key=private_key, public_key=public_key) return self.instance + + +class ConnectivitySerializer(serializers.Serializer): + status = serializers.IntegerField() + datetime = serializers.DateTimeField() \ No newline at end of file diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index b523a1779..e5ab62037 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import re from rest_framework import serializers from common.fields import ChoiceDisplayField @@ -20,8 +21,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): serializer_choice_field = ChoiceDisplayField + invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]') class Meta: model = CommandFilterRule fields = '__all__' list_serializer_class = AdaptedBulkListSerializer + + def validate_content(self, content): + if self.invalid_pattern.search(content): + invalid_char = self.invalid_pattern.pattern.replace('\\', '') + msg = _("Content should not be contain: {}").format(invalid_char) + raise serializers.ValidationError(msg) + return content diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index 61df12f64..b33f22116 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from rest_framework import serializers +from django.utils.translation import ugettext as _ from orgs.mixins import BulkOrgResourceModelSerializer from ..models import Asset, Node @@ -25,11 +26,11 @@ class NodeSerializer(BulkOrgResourceModelSerializer): def validate_value(self, data): instance = self.instance if self.instance else Node.root() - children = instance.parent.get_children().exclude(key=instance.key) - values = [child.value for child in children] - if data in values: + children = instance.parent.get_children() + children_values = [node.value for node in children if node != instance] + if data in children_values: raise serializers.ValidationError( - 'The same level node name cannot be the same' + _('The same level node name cannot be the same') ) return data diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index c4cb5be05..116dcdd36 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -12,53 +12,31 @@ class SystemUserSerializer(BulkOrgResourceModelSerializer): """ 系统用户 """ - password = serializers.CharField( - required=False, write_only=True, label=_('Password') - ) - unreachable_amount = serializers.SerializerMethodField( - label=_('Unreachable') - ) - unreachable_assets = serializers.SerializerMethodField( - label=_('Unreachable assets') - ) - reachable_assets = serializers.SerializerMethodField( - label=_('Reachable assets') - ) - reachable_amount = serializers.SerializerMethodField(label=_('Reachable')) - assets_amount = serializers.SerializerMethodField(label=_('Asset')) class Meta: model = SystemUser list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'username', 'login_mode', 'login_mode_display', - 'login_mode_display', 'priority', 'protocol', 'auto_push', - 'password', 'assets_amount', 'reachable_amount', 'reachable_assets', - 'unreachable_amount', 'unreachable_assets', 'cmd_filters', 'sudo', - 'shell', 'comment', 'nodes', 'assets' + 'id', 'name', 'username', 'password', 'public_key', 'private_key', + 'login_mode', 'login_mode_display', 'priority', 'protocol', + 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'nodes', + 'assets_amount', 'connectivity_amount' ] extra_kwargs = { + 'password': {"write_only": True}, + 'public_key': {"write_only": True}, + 'private_key': {"write_only": True}, + 'assets_amount': {'label': _('Asset')}, + 'connectivity_amount': {'label': _('Connectivity')}, 'login_mode_display': {'label': _('Login mode display')}, 'created_by': {'read_only': True}, } - @staticmethod - def get_unreachable_assets(obj): - return obj.assets_unreachable - - @staticmethod - def get_reachable_assets(obj): - return obj.assets_reachable - - def get_unreachable_amount(self, obj): - return len(self.get_unreachable_assets(obj)) - - def get_reachable_amount(self, obj): - return len(self.get_reachable_assets(obj)) - - @staticmethod - def get_assets_amount(obj): - return len(obj.get_related_assets()) + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('cmd_filters', 'nodes') + return queryset class SystemUserAuthSerializer(AuthSerializer): @@ -74,23 +52,6 @@ class SystemUserAuthSerializer(AuthSerializer): ] -class AssetSystemUserSerializer(serializers.ModelSerializer): - """ - 查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少 - """ - actions = serializers.SerializerMethodField() - - class Meta: - model = SystemUser - fields = ( - 'id', 'name', 'username', 'priority', - 'protocol', 'comment', 'login_mode', 'actions', - ) - - @staticmethod - def get_actions(obj): - return [action.name for action in obj.actions] - class SystemUserSimpleSerializer(serializers.ModelSerializer): """ diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index a145437d0..59d01c98a 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -27,11 +27,6 @@ def test_asset_conn_on_created(asset): test_asset_connectivity_util.delay([asset]) -def set_asset_root_node(asset): - logger.debug("Set asset default node: {}".format(Node.root())) - asset.nodes.add(Node.root()) - - @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") @on_transaction_commit def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 149e9fa2c..dbd6808f1 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -15,7 +15,8 @@ from ops.celery.decorator import ( register_as_period_task, after_app_shutdown_clean_periodic ) -from .models import SystemUser, AdminUser, Asset +from .models import SystemUser, AdminUser +from .models.utils import Connectivity from . import const @@ -207,8 +208,7 @@ def test_asset_connectivity_util(assets, task_name=None): pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by, ) - result = task.run() - summary = result[1] + raw, summary = task.run() success = summary.get('success', False) contacted = summary.get('contacted', {}) dark = summary.get('dark', {}) @@ -218,13 +218,12 @@ def test_asset_connectivity_util(assets, task_name=None): results_summary['dark'].update(dark) for asset in assets: - if asset.hostname in results_summary.get('dark', {}): - asset.connectivity = asset.UNREACHABLE - elif asset.hostname in results_summary.get('contacted', []): - asset.connectivity = asset.REACHABLE + if asset.hostname in results_summary.get('dark', {}).keys(): + asset.connectivity = Connectivity.unreachable() + elif asset.hostname in results_summary.get('contacted', {}).keys(): + asset.connectivity = Connectivity.reachable() else: - asset.connectivity = asset.UNKNOWN - + asset.connectivity = Connectivity.unknown() return results_summary @@ -286,10 +285,6 @@ def test_admin_user_connectivity_manual(admin_user): ## System user connective ## -@shared_task -def set_system_user_connectivity_info(system_user, summary): - system_user.connectivity = summary - @shared_task def test_system_user_connectivity_util(system_user, assets, task_name): @@ -336,8 +331,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name): pattern='all', options=const.TASK_OPTIONS, run_as=system_user.username, created_by=system_user.org_id, ) - result = task.run() - summary = result[1] + raw, summary = task.run() success = summary.get('success', False) contacted = summary.get('contacted', {}) dark = summary.get('dark', {}) @@ -346,7 +340,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name): results_summary['contacted'].update(contacted) results_summary['dark'].update(dark) - set_system_user_connectivity_info(system_user, results_summary) + system_user.set_connectivity(results_summary) return results_summary @@ -567,23 +561,12 @@ def get_test_asset_user_connectivity_tasks(asset): return tasks -@shared_task -def set_asset_user_connectivity_info(asset_user, result): - summary = result[1] - if summary.get('contacted'): - connectivity = 1 - elif summary.get("dark"): - connectivity = 0 - else: - connectivity = 3 - asset_user.connectivity = connectivity - - @shared_task def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): """ :param asset_user: 对象 :param task_name: + :param run_as_admin: :return: """ from ops.utils import update_or_create_ansible_task @@ -593,6 +576,7 @@ def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False) tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) if not tasks: + logger.debug("No tasks ") return args = (task_name,) @@ -606,8 +590,8 @@ def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False) else: kwargs["run_as"] = asset_user.username task, created = update_or_create_ansible_task(*args, **kwargs) - result = task.run() - set_asset_user_connectivity_info(asset_user, result) + raw, summary = task.run() + asset_user.set_connectivity(summary) @shared_task diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index 4abfa0587..beafa9ae1 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -67,6 +67,7 @@ function initTable2() { columns: [ {data: "id"}, {data: "hostname" }, {data: "ip" } ], + lengthMenu: [[10, 25, 50], [10, 25, 50]], pageLength: 10 }; asset_table2 = jumpserver.initServerSideDataTable(options); diff --git a/apps/assets/templates/assets/_asset_user_list.html b/apps/assets/templates/assets/_asset_user_list.html index ccf8c8ef1..381aec13d 100644 --- a/apps/assets/templates/assets/_asset_user_list.html +++ b/apps/assets/templates/assets/_asset_user_list.html @@ -32,7 +32,9 @@ var assetUserListUrl = "{% url "api-assets:asset-user-list" %}"; var assetUserTable; var needPush = false; var prefer = null; -var lastMFATime = "{{ request.session.OTP_LAST_VERIFY_TIME }}"; +var lastMFATime = "{{ request.session.MFA_VERIFY_TIME }}"; +var testDatetime = "{% trans 'Test datetime: ' %}"; +var mfaVerifyTTL = "{{ SECURITY_MFA_VERIFY_TTL }}"; function initAssetUserTable() { var options = { @@ -41,19 +43,25 @@ function initAssetUserTable() { columnDefs: [ { targets: 5, createdCell: function (td, cellData) { - if (cellData == 1) { - $(td).html('') - } else if (cellData == 0) { - $(td).html('') + var innerHtml = ""; + if (cellData.status == 1) { + innerHtml = '' + } else if (cellData.status == 0) { + innerHtml = '' } else { - $(td).html('') + innerHtml = '' } + var date = new Date(cellData.datetime); + var dateManual = date.toLocaleString(); + var dataContent = testDatetime + dateManual; + innerHtml = "" + innerHtml + ""; + $(td).html(innerHtml); } }, { targets: 6, createdCell: function (td, cellData) { - var date = new Date(cellData); - $(td).html(date.toLocaleString()); + var data = formatDateAsCN(cellData); + $(td).html(data); }, }, { @@ -84,8 +92,8 @@ function initAssetUserTable() { ajax_url: assetUserListUrl, columns: [ {data: "id"}, {data: "hostname"}, {data: "ip"}, - {data: "username", orderable: false}, {data: "version", orderable: false}, - {data: "connectivity", orderable: false}, + {data: "username"}, {data: "version", orderable: false}, + {data: "connectivity"}, {data: "date_created", orderable: false}, {data: "asset", orderable: false} ], @@ -102,7 +110,7 @@ $(document).ready(function(){ authUsername = $(this).data('user'); var now = new Date(); var nowTime = now.getTime() / 1000; - if (nowTime - lastMFATime > 60*10 ) { + if ( !lastMFATime || nowTime - lastMFATime > mfaVerifyTTL ) { mfaFor = "viewAuth"; $("#mfa_auth_confirm").modal("show"); } else { diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html new file mode 100644 index 000000000..3f6e51596 --- /dev/null +++ b/apps/assets/templates/assets/_node_tree.html @@ -0,0 +1,358 @@ +{% load static %} +{% load i18n %} + +{# #} + + + + +
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index e52185af2..f34495dad 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -46,7 +46,7 @@ -
+
{% trans 'Quick update' %} @@ -81,21 +81,6 @@ $(document).ready(function () { prefer = "admin_user"; initAssetUserTable(); }) -.on('click', '.btn-test-asset', function () { - var asset_id = $(this).data('uid'); - var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id); - var success = function (data) { - var task_id = data.task; - var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); - window.open(url, '', 'width=800,height=600,left=400,top=400') - }; - APIUpdateAttr({ - url: the_url, - method: 'GET', - success: success, - flash_message: false - }); -}) .on('click', '.btn-test-connective', function () { var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var success = function (data) { @@ -110,17 +95,5 @@ $(document).ready(function () { flash_message: false }); }) -.on('click', '.btn-update-asset-user-auth', function() { - asset_id = $(this).data('aid'); - hostname = $(this).data('hostname'); - username = '{{ admin_user.username }}'; - $("#asset_user_auth_update_modal").modal(); -}) -.on("click", ".btn-view-auth", function (evt) { - asset_id = $(this).data("aid") ; - host = $(this).data("hostname"); - username = "{{ admin_user.username }}"; - $("#asset_user_auth_view").modal(); -}) {% endblock %} diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index 4f4356539..0a17eccaa 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -11,27 +11,27 @@ {% endblock %} {% block table_search %} + +
{% endblock %} {% block table_container %} @@ -75,27 +75,29 @@ function initTable() { }}, {targets: 4, createdCell: function (td, cellData) { var innerHtml = ""; - if (cellData !== 0) { - innerHtml = "" + cellData + ""; + var data = cellData.reachable; + if (data !== 0) { + innerHtml = "" + data + ""; } else { - innerHtml = "" + cellData + ""; + innerHtml = "" + data + ""; } - $(td).html('' + innerHtml + ''); + $(td).html(innerHtml) }}, {targets: 5, createdCell: function (td, cellData) { + var data = cellData.unreachable; var innerHtml = ""; - if (cellData !== 0) { - innerHtml = "" + cellData + ""; + if (data !== 0) { + innerHtml = "" + data + ""; } else { - innerHtml = "" + cellData + ""; + innerHtml = "" + data + ""; } - $(td).html('' + innerHtml + ''); + $(td).html('' + innerHtml + ''); }}, {targets: 6, createdCell: function (td, cellData, rowData) { var val = 0; var innerHtml = ""; var total = rowData.assets_amount; - var reachable = rowData.reachable_amount; + var reachable = cellData.reachable; if (total !== 0) { val = reachable/total * 100; } @@ -114,15 +116,18 @@ function initTable() { $(td).html(update_btn + del_btn) }}], ajax_url: '{% url "api-assets:admin-user-list" %}', - columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" }, - {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}] + columns: [ + {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" }, + {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"}, + {data: "comment"}, {data: "id"} + ] }; admin_user_table = jumpserver.initServerSideDataTable(options); return admin_user_table } $(document).ready(function(){ - initTable() + initTable(); }) .on('click', '.btn_admin_user_delete', function () { diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 2dcb569ac..ad635ca5b 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -70,11 +70,7 @@ {% trans 'Protocol' %} - - {% for protocol in asset.protocols.all %} - {{ protocol }} - {% endfor %} - + {{ asset.protocols }} {% trans 'Admin user' %}: diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index dc2bb7ebb..3b9df3373 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -49,15 +49,7 @@
-
-
-
-
-
-
-
-
-
+ {% include 'assets/_node_tree.html' %}
@@ -132,26 +124,7 @@
- +{% include 'assets/_node_tree.html' %} {% include 'assets/_asset_update_modal.html' %} {% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_list_modal.html' %} @@ -159,10 +132,11 @@ {% block custom_foot_js %} diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 0744b793b..680bfa421 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -15,27 +15,27 @@ {% block table_search %} + +
{% endblock %} {% block table_container %} @@ -46,7 +46,7 @@ - + {% trans 'Name' %} {% trans 'Username' %} @@ -80,28 +80,30 @@ function initTable() { }}, {targets: 6, createdCell: function (td, cellData) { var innerHtml = ""; - if (cellData !== 0) { - innerHtml = "" + cellData + ""; + var data = cellData.reachable; + if (data !== 0) { + innerHtml = "" + data + ""; } else { - innerHtml = "" + cellData + ""; + innerHtml = "" + data + ""; } - $(td).html('' + innerHtml + ''); + $(td).html(innerHtml) }}, {targets: 7, createdCell: function (td, cellData) { + var data = cellData.unreachable; var innerHtml = ""; - if (cellData !== 0) { - innerHtml = "" + cellData + ""; + if (data !== 0) { + innerHtml = "" + data + ""; } else { - innerHtml = "" + cellData + ""; + innerHtml = "" + data + ""; } - $(td).html('' + innerHtml + ''); + $(td).html('' + innerHtml + ''); }}, {targets: 8, createdCell: function (td, cellData, rowData) { var val = 0; var innerHtml = ""; var total = rowData.assets_amount; - var reachable = rowData.reachable_amount; - if (total !== 0) { + var reachable = cellData.reachable; + if (total && total !== 0) { val = reachable/total * 100; } @@ -112,20 +114,20 @@ function initTable() { innerHtml = "" + num.toFixed(1) + "% "; } $(td).html('' + innerHtml + ''); - }}, {targets: 10, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); $(td).html(update_btn + del_btn) - }}], + }}, + ], ajax_url: '{% url "api-assets:system-user-list" %}', columns: [ {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" }, - {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" } + {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "comment" }, {data: "id" } ], op_html: $('#actions').html() - }; + }; system_user_table = jumpserver.initServerSideDataTable(options); return system_user_table } diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index f9f67b849..71483a4df 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -9,8 +9,6 @@ urlpatterns = [ path('', views.AssetListView.as_view(), name='asset-index'), path('asset/', views.AssetListView.as_view(), name='asset-list'), path('asset/create/', views.AssetCreateView.as_view(), name='asset-create'), - path('asset/export/', views.AssetExportView.as_view(), name='asset-export'), - path('asset/import/', views.BulkImportAssetView.as_view(), name='asset-import'), path('asset//', views.AssetDetailView.as_view(), name='asset-detail'), path('asset//update/', views.AssetUpdateView.as_view(), name='asset-update'), path('asset//delete/', views.AssetDeleteView.as_view(), name='asset-delete'), diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 707d67a50..b64fea35e 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,23 +1,14 @@ # ~*~ coding: utf-8 ~*~ # -import os -import paramiko -from paramiko.ssh_exception import SSHException +import time +from django.db.models import Prefetch -from common.utils import get_object_or_none -from .models import Asset, SystemUser, Label +from common.utils import get_object_or_none, get_logger +from common.struct import Stack +from .models import SystemUser, Label, Node, Asset -def get_assets_by_id_list(id_list): - return Asset.objects.filter(id__in=id_list).filter(is_active=True) - - -def get_system_users_by_id_list(id_list): - return SystemUser.objects.filter(id__in=id_list) - - -def get_assets_by_fullname_list(hostname_list): - return Asset.get_queryset_by_fullname_list(hostname_list) +logger = get_logger(__file__) def get_system_user_by_name(name): @@ -49,3 +40,166 @@ class LabelFilter: for kwargs in conditions: queryset = queryset.filter(**kwargs) return queryset + + +class NodeUtil: + def __init__(self, with_assets_amount=False, debug=False): + self.stack = Stack() + self._nodes = {} + self.with_assets_amount = with_assets_amount + self._debug = debug + self.init() + + @staticmethod + def sorted_by(node): + return [int(i) for i in node.key.split(':')] + + def get_queryset(self): + all_nodes = Node.objects.all() + if self.with_assets_amount: + all_nodes = all_nodes.prefetch_related( + Prefetch('assets', queryset=Asset.objects.all().only('id')) + ) + all_nodes = list(all_nodes) + for node in all_nodes: + node._assets = set(node.assets.all()) + return all_nodes + + def get_all_nodes(self): + all_nodes = sorted(self.get_queryset(), key=self.sorted_by) + + guarder = Node(key='', value='Guarder') + guarder._assets = [] + all_nodes.append(guarder) + return all_nodes + + def push_to_stack(self, node): + # 入栈之前检查 + # 如果栈是空的,证明是一颗树的根部 + if self.stack.is_empty(): + node._full_value = node.value + node._parents = [] + else: + # 如果不是根节点, + # 该节点的祖先应该是父节点的祖先加上父节点 + # 该节点的名字是父节点的名字+自己的名字 + node._parents = [self.stack.top] + self.stack.top._parents + node._full_value = ' / '.join( + [self.stack.top._full_value, node.value] + ) + node._children = [] + node._all_children = [] + self.debug("入栈: {}".format(node.key)) + self.stack.push(node) + + # 出栈 + def pop_from_stack(self): + _node = self.stack.pop() + self.debug("出栈: {} 栈顶: {}".format(_node.key, self.stack.top.key if self.stack.top else None)) + self._nodes[_node.key] = _node + if not self.stack.top: + return + if self.with_assets_amount: + self.stack.top._assets.update(_node._assets) + _node._assets_amount = len(_node._assets) + delattr(_node, '_assets') + self.stack.top._children.append(_node) + self.stack.top._all_children.extend([_node] + _node._children) + + def init(self): + all_nodes = self.get_all_nodes() + for node in all_nodes: + self.debug("准备: {} 栈顶: {}".format(node.key, self.stack.top.key if self.stack.top else None)) + # 入栈之前检查,该节点是不是栈顶节点的子节点 + # 如果不是,则栈顶出栈 + while self.stack.top and not self.stack.top.is_children(node): + self.pop_from_stack() + self.push_to_stack(node) + # 出栈最后一个 + self.debug("剩余: {}".format(', '.join([n.key for n in self.stack]))) + + def get_nodes_by_queryset(self, queryset): + nodes = [] + for n in queryset: + node = self.get_node_by_key(n.key) + if not node: + continue + nodes.append(node) + return nodes + + def get_node_by_key(self, key): + return self._nodes.get(key) + + def debug(self, msg): + self._debug and logger.debug(msg) + + def set_assets_amount(self): + for node in self._nodes.values(): + node.assets_amount = node._assets_amount + + def set_full_value(self): + for node in self._nodes.values(): + node.full_value = node._full_value + + @property + def nodes(self): + return list(self._nodes.values()) + + # 使用给定节点生成一颗树 + # 找到他们的祖先节点 + # 可选找到他们的子孙节点 + def get_family(self, nodes, with_children=False): + tree_nodes = set() + for n in nodes: + node = self.get_node_by_key(n.key) + if not node: + continue + tree_nodes.update(node._parents) + tree_nodes.add(node) + if with_children: + tree_nodes.update(node._children) + return list(tree_nodes) + + def get_nodes_parents(self, nodes, with_self=True): + parents = set() + for n in nodes: + node = self.get_node_by_key(n.key) + parents.update(set(node._parents)) + if with_self: + parents.add(node) + return parents + + +def test_node_tree(): + tree = NodeUtil() + for node in tree._nodes.values(): + print("Check {}".format(node.key)) + children_wanted = node.get_all_children().count() + children = len(node._children) + if children != children_wanted: + print("{} children not equal: {} != {}".format(node.key, children, children_wanted)) + + assets_amount_wanted = node.get_all_assets().count() + if node._assets_amount != assets_amount_wanted: + print("{} assets amount not equal: {} != {}".format( + node.key, node._assets_amount, assets_amount_wanted) + ) + + full_value_wanted = node.full_value + if node._full_value != full_value_wanted: + print("{} full value not equal: {} != {}".format( + node.key, node._full_value, full_value_wanted) + ) + + parents_wanted = node.get_ancestor().count() + parents = len(node._parents) + if parents != parents_wanted: + print("{} parents count not equal: {} != {}".format( + node.key, parents, parents_wanted) + ) + + + + + + diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index 089efe352..455ca4a9d 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -81,7 +81,7 @@ class AdminUserDetailView(PermissionsMixin, DetailView): context = { 'app': _('Assets'), 'action': _('Admin user detail'), - 'nodes': Node.objects.all() + 'nodes': Node.get_queryset(), } kwargs.update(context) return super().get_context_data(**kwargs) @@ -106,8 +106,6 @@ class AdminUserAssetsView(PermissionsMixin, SingleObjectMixin, ListView): context = { 'app': _('Assets'), 'action': _('Admin user detail'), - "total_amount": len(self.queryset), - 'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False]) } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index aef420171..ce701b129 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -1,43 +1,31 @@ # coding:utf-8 from __future__ import absolute_import, unicode_literals -import csv -import json -import uuid -import codecs -import chardet -from io import StringIO - -from django.db import transaction from django.contrib import messages from django.utils.translation import ugettext_lazy as _ -from django.views.generic import TemplateView, ListView, View -from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView +from django.views.generic import TemplateView, ListView +from django.views.generic.edit import FormMixin +from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.urls import reverse_lazy from django.views.generic.detail import DetailView -from django.http import HttpResponse, JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator from django.core.cache import cache -from django.utils import timezone from django.shortcuts import redirect from django.contrib.messages.views import SuccessMessageMixin from django.forms.formsets import formset_factory -from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser from common.const import ( create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID ) from .. import forms -from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain +from ..models import Asset, SystemUser, Label, Node __all__ = [ 'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView', 'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView', - 'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView', + 'AssetDeleteView', ] logger = get_logger(__file__) @@ -87,7 +75,7 @@ class UserAssetListView(PermissionsMixin, TemplateView): return super().get_context_data(**kwargs) -class AssetCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): +class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): model = Asset form_class = forms.AssetCreateForm template_name = 'assets/asset_create.html' @@ -112,16 +100,6 @@ class AssetCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): formset = ProtocolFormset() return formset - def form_valid(self, form): - formset = self.get_protocol_formset() - valid = formset.is_valid() - if not valid: - return self.form_invalid(form) - protocols = formset.save() - instance = super().form_valid(form) - instance.protocols.set(protocols) - return instance - def get_context_data(self, **kwargs): formset = self.get_protocol_formset() context = { @@ -132,8 +110,32 @@ class AssetCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - return create_success_msg % ({"name": cleaned_data["hostname"]}) + +class AssetUpdateView(PermissionsMixin, UpdateView): + model = Asset + form_class = forms.AssetUpdateForm + template_name = 'assets/asset_update.html' + success_url = reverse_lazy('assets:asset-list') + permission_classes = [IsOrgAdmin] + + def get_protocol_formset(self): + ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) + if self.request.method == "POST": + formset = ProtocolFormset(self.request.POST) + else: + initial_data = self.object.protocols_as_json + formset = ProtocolFormset(initial=initial_data) + return formset + + def get_context_data(self, **kwargs): + formset = self.get_protocol_formset() + context = { + 'app': _('Assets'), + 'action': _('Update asset'), + 'formset': formset, + } + kwargs.update(context) + return super().get_context_data(**kwargs) class AssetBulkUpdateView(PermissionsMixin, ListView): @@ -177,36 +179,6 @@ class AssetBulkUpdateView(PermissionsMixin, ListView): return super().get_context_data(**kwargs) -class AssetUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): - model = Asset - form_class = forms.AssetUpdateForm - template_name = 'assets/asset_update.html' - success_url = reverse_lazy('assets:asset-list') - permission_classes = [IsOrgAdmin] - - def get_protocol_formset(self): - ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) - if self.request.method == "POST": - formset = ProtocolFormset(self.request.POST) - else: - initial_data = [{"name": p.name, "port": p.port} for p in self.object.protocols.all()] - formset = ProtocolFormset(initial=initial_data) - return formset - - def get_context_data(self, **kwargs): - formset = self.get_protocol_formset() - context = { - 'app': _('Assets'), - 'action': _('Update asset'), - 'formset': formset, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def get_success_message(self, cleaned_data): - return update_success_msg % ({"name": cleaned_data["hostname"]}) - - class AssetDeleteView(PermissionsMixin, DeleteView): model = Asset template_name = 'delete_confirm.html' @@ -220,6 +192,11 @@ class AssetDetailView(PermissionsMixin, DetailView): template_name = 'assets/asset_detail.html' permission_classes = [IsValidUser] + def get_queryset(self): + return super().get_queryset().prefetch_related( + "nodes", "labels", + ).select_related('admin_user', 'domain') + def get_context_data(self, **kwargs): nodes_remain = Node.objects.exclude(assets=self.object) context = { @@ -229,150 +206,3 @@ class AssetDetailView(PermissionsMixin, DetailView): } kwargs.update(context) return super().get_context_data(**kwargs) - - -@method_decorator(csrf_exempt, name='dispatch') -class AssetExportView(PermissionsMixin, View): - permission_classes = [IsValidUser] - - def get(self, request): - spm = request.GET.get('spm', '') - assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else [] - assets_id = cache.get(spm, assets_id_default) - fields = [ - field for field in Asset._meta.fields - if field.name not in [ - 'date_created', 'org_id' - ] - ] - filename = 'assets-{}.csv'.format( - timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S') - ) - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % filename - response.write(codecs.BOM_UTF8) - assets = Asset.objects.filter(id__in=assets_id) - writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL) - - header = [field.verbose_name for field in fields] - writer.writerow(header) - - for asset in assets: - data = [getattr(asset, field.name) for field in fields] - writer.writerow(data) - return response - - def post(self, request, *args, **kwargs): - try: - assets_id = json.loads(request.body).get('assets_id', []) - node_id = json.loads(request.body).get('node_id', None) - except ValueError: - return HttpResponse('Json object not valid', status=400) - - if not assets_id: - node = get_object_or_none(Node, id=node_id) if node_id else Node.root() - assets = node.get_all_assets() - for asset in assets: - assets_id.append(asset.id) - - spm = uuid.uuid4().hex - cache.set(spm, assets_id, 300) - url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm - return JsonResponse({'redirect': url}) - - -class BulkImportAssetView(PermissionsMixin, JSONResponseMixin, FormView): - form_class = forms.FileForm - permission_classes = [IsOrgAdmin] - - def form_valid(self, form): - node_id = self.request.GET.get("node_id") - node = get_object_or_none(Node, id=node_id) if node_id else Node.root() - f = form.cleaned_data['file'] - det_result = chardet.detect(f.read()) - f.seek(0) # reset file seek index - - file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode()) - csv_file = StringIO(file_data) - reader = csv.reader(csv_file) - csv_data = [row for row in reader] - fields = [ - field for field in Asset._meta.fields - if field.name not in [ - 'date_created' - ] - ] - header_ = csv_data[0] - mapping_reverse = {field.verbose_name: field.name for field in fields} - attr = [mapping_reverse.get(n, None) for n in header_] - if None in attr: - data = {'valid': False, - 'msg': 'Must be same format as ' - 'template or export file'} - return self.render_json_response(data) - - created, updated, failed = [], [], [] - assets = [] - for row in csv_data[1:]: - if set(row) == {''}: - continue - - asset_dict_raw = dict(zip(attr, row)) - asset_dict = dict() - for k, v in asset_dict_raw.items(): - v = v.strip() - if k == 'is_active': - v = False if v in ['False', 0, 'false'] else True - elif k == 'admin_user': - v = get_object_or_none(AdminUser, name=v) - elif k in ['port', 'cpu_count', 'cpu_cores']: - try: - v = int(v) - except ValueError: - v = '' - elif k == 'domain': - v = get_object_or_none(Domain, name=v) - elif k == 'platform': - v = v.lower().capitalize() - if v != '': - asset_dict[k] = v - - asset = None - asset_id = asset_dict.pop('id', None) - if asset_id: - asset = get_object_or_none(Asset, id=asset_id) - if not asset: - try: - if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))): - raise Exception(_('already exists')) - with transaction.atomic(): - asset = Asset.objects.create(**asset_dict) - if node: - asset.nodes.set([node]) - created.append(asset_dict['hostname']) - assets.append(asset) - except Exception as e: - failed.append('%s: %s' % (asset_dict['hostname'], str(e))) - else: - for k, v in asset_dict.items(): - if v != '': - setattr(asset, k, v) - try: - asset.save() - updated.append(asset_dict['hostname']) - except Exception as e: - failed.append('%s: %s' % (asset_dict['hostname'], str(e))) - - data = { - 'created': created, - 'created_info': 'Created {}'.format(len(created)), - 'updated': updated, - 'updated_info': 'Updated {}'.format(len(updated)), - 'failed': failed, - 'failed_info': 'Failed {}'.format(len(failed)), - 'valid': True, - 'msg': 'Created: {}. Updated: {}, Error: {}'.format( - len(created), len(updated), len(failed)) - } - return self.render_json_response(data) - diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index 3e400cd29..b47c9eb7d 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -74,10 +74,11 @@ class SystemUserDetailView(PermissionsMixin, DetailView): permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): + cmd_filters_remain = CommandFilter.objects.exclude(system_users=self.object) context = { 'app': _('Assets'), 'action': _('System user detail'), - 'cmd_filters_remain': CommandFilter.objects.exclude(system_users=self.object) + 'cmd_filters_remain': cmd_filters_remain, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -92,12 +93,15 @@ class SystemUserDeleteView(PermissionsMixin, DeleteView): class SystemUserAssetView(PermissionsMixin, DetailView): model = SystemUser - template_name = 'assets/system_user_asset.html' + template_name = 'assets/system_user_assets.html' context_object_name = 'system_user' permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): - nodes_remain = sorted(Node.objects.exclude(systemuser=self.object), reverse=True) + from ..utils import NodeUtil + nodes_remain = Node.objects.exclude(systemuser=self.object) + util = NodeUtil() + nodes_remain = util.get_nodes_by_queryset(nodes_remain) context = { 'app': _('assets'), 'action': _('System user asset'), diff --git a/apps/authentication/api/auth.py b/apps/authentication/api/auth.py index 42c196c51..042150517 100644 --- a/apps/authentication/api/auth.py +++ b/apps/authentication/api/auth.py @@ -194,7 +194,7 @@ class UserOtpVerifyApi(CreateAPIView): code = serializer.validated_data["code"] if request.user.check_otp(code): - request.session["OTP_LAST_VERIFY_TIME"] = int(time.time()) + request.session["MFA_VERIFY_TIME"] = int(time.time()) return Response({"ok": "1"}) else: return Response({"error": "Code not valid"}, status=400) diff --git a/apps/authentication/backends/openid/__init__.py b/apps/authentication/backends/openid/__init__.py index 2deaf3cae..9ed3bea78 100644 --- a/apps/authentication/backends/openid/__init__.py +++ b/apps/authentication/backends/openid/__init__.py @@ -4,3 +4,4 @@ from .backends import * from .middleware import * from .utils import * +from .decorator import * diff --git a/apps/authentication/backends/openid/backends.py b/apps/authentication/backends/openid/backends.py index b8e4ae609..f6285d1ed 100644 --- a/apps/authentication/backends/openid/backends.py +++ b/apps/authentication/backends/openid/backends.py @@ -20,7 +20,6 @@ __all__ = [ class BaseOpenIDAuthorizationBackend(object): - @staticmethod def user_can_authenticate(user): """ @@ -40,25 +39,20 @@ class BaseOpenIDAuthorizationBackend(object): class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): - def authenticate(self, request, **kwargs): logger.info('Authentication OpenID code backend') - code = kwargs.get('code') redirect_uri = kwargs.get('redirect_uri') - if not code or not redirect_uri: logger.info('Authenticate failed: No code or No redirect uri') return None - try: oidt_profile = client.update_or_create_from_code( - code=code, redirect_uri=redirect_uri + code=code, redirect_uri=redirect_uri ) - except Exception as e: logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - + return None else: # Check openid user single logout or not with access_token request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token @@ -68,25 +62,19 @@ class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend): class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend): - def authenticate(self, request, username=None, password=None, **kwargs): logger.info('Authentication OpenID password backend') - - if not settings.AUTH_OPENID: - logger.info('Authenticate failed: AUTH_OPENID is False') - return None - elif not username: + if not username: logger.info('Authenticate failed: Not username') return None - try: oidt_profile = client.update_or_create_from_password( username=username, password=password ) - except Exception as e: + logger.error(e, exc_info=True) logger.info('Authenticate failed: get oidt_profile: {}'.format(e)) - + return None else: user = oidt_profile.user logger.info('Authenticate success: user -> {}'.format(user)) diff --git a/apps/authentication/backends/openid/decorator.py b/apps/authentication/backends/openid/decorator.py new file mode 100644 index 000000000..7286b7a2f --- /dev/null +++ b/apps/authentication/backends/openid/decorator.py @@ -0,0 +1,57 @@ +# coding: utf-8 +# + +import warnings +import contextlib + +import requests +from urllib3.exceptions import InsecureRequestWarning +from django.conf import settings + +__all__ = [ + 'ssl_verification', +] + +old_merge_environment_settings = requests.Session.merge_environment_settings + + +@contextlib.contextmanager +def no_ssl_verification(): + """ + https://stackoverflow.com/questions/15445981/ + how-do-i-disable-the-security-certificate-check-in-python-requests + """ + opened_adapters = set() + + def merge_environment_settings(self, url, proxies, stream, verify, cert): + # Verification happens only once per connection so we need to close + # all the opened adapters once we're done. Otherwise, the effects of + # verify=False persist beyond the end of this context manager. + opened_adapters.add(self.get_adapter(url)) + _settings = old_merge_environment_settings( + self, url, proxies, stream, verify, cert + ) + _settings['verify'] = False + return _settings + + requests.Session.merge_environment_settings = merge_environment_settings + try: + with warnings.catch_warnings(): + warnings.simplefilter('ignore', InsecureRequestWarning) + yield + finally: + requests.Session.merge_environment_settings = old_merge_environment_settings + for adapter in opened_adapters: + try: + adapter.close() + except: + pass + + +def ssl_verification(func): + def wrapper(*args, **kwargs): + if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION: + return func(*args, **kwargs) + with no_ssl_verification(): + return func(*args, **kwargs) + return wrapper diff --git a/apps/authentication/backends/openid/middleware.py b/apps/authentication/backends/openid/middleware.py index 43c7dbd22..bacb4858c 100644 --- a/apps/authentication/backends/openid/middleware.py +++ b/apps/authentication/backends/openid/middleware.py @@ -19,24 +19,23 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin): """ Check openid user single logout (with access_token) """ - def process_request(self, request): # Don't need openid auth if AUTH_OPENID is False if not settings.AUTH_OPENID: return + # Don't need openid auth if no shared session enabled + if not settings.AUTH_OPENID_SHARE_SESSION: + return # Don't need check single logout if user not authenticated if not request.user.is_authenticated: return elif not request.session[BACKEND_SESSION_KEY].endswith( BACKEND_OPENID_AUTH_CODE): return - # Check openid user single logout or not with access_token - client = new_client() try: - client.openid_connect_client.userinfo( - token=request.session.get(OIDT_ACCESS_TOKEN) - ) + client = new_client() + client.get_userinfo(token=request.session.get(OIDT_ACCESS_TOKEN)) except Exception as e: logout(request) logger.error(e) diff --git a/apps/authentication/backends/openid/models.py b/apps/authentication/backends/openid/models.py index fd75ed870..b99ba402a 100644 --- a/apps/authentication/backends/openid/models.py +++ b/apps/authentication/backends/openid/models.py @@ -7,12 +7,24 @@ from keycloak.realm import KeycloakRealm from keycloak.keycloak_openid import KeycloakOpenID from .signals import post_create_openid_user +from .decorator import ssl_verification OIDT_ACCESS_TOKEN = 'oidt_access_token' -class OpenIDTokenProfile(object): +class Nonce(object): + """ + The openid-login is stored in cache as a temporary object, recording the + user's redirect_uri and next_pat + """ + def __init__(self, redirect_uri, next_path): + import uuid + self.state = uuid.uuid4() + self.redirect_uri = redirect_uri + self.next_path = next_path + +class OpenIDTokenProfile(object): def __init__(self, user, access_token, refresh_token): """ :param user: User object @@ -28,80 +40,109 @@ class OpenIDTokenProfile(object): class Client(object): - def __init__(self, server_url, realm_name, client_id, client_secret): self.server_url = server_url self.realm_name = realm_name self.client_id = client_id self.client_secret = client_secret - self.realm = self.new_realm() - self.openid_client = self.new_openid_client() - self.openid_connect_client = self.new_openid_connect_client() + self._openid_client = None + self._realm = None + self._openid_connect_client = None - def new_realm(self): - return KeycloakRealm( - server_url=self.server_url, - realm_name=self.realm_name, - headers={} - ) + @property + def realm(self): + if self._realm is None: + self._realm = KeycloakRealm( + server_url=self.server_url, + realm_name=self.realm_name, + headers={} + ) + return self._realm - def new_openid_connect_client(self): + @property + def openid_connect_client(self): """ :rtype: keycloak.openid_connect.KeycloakOpenidConnect """ - openid_connect = self.realm.open_id_connect( - client_id=self.client_id, - client_secret=self.client_secret - ) - return openid_connect + if self._openid_connect_client is None: + self._openid_connect_client = self.realm.open_id_connect( + client_id=self.client_id, + client_secret=self.client_secret + ) + return self._openid_connect_client - def new_openid_client(self): + @property + def openid_client(self): """ :rtype: keycloak.keycloak_openid.KeycloakOpenID """ + if self._openid_client is None: + self._openid_client = KeycloakOpenID( + server_url='%sauth/' % self.server_url, + realm_name=self.realm_name, + client_id=self.client_id, + client_secret_key=self.client_secret, + ) + return self._openid_client - return KeycloakOpenID( - server_url='%sauth/' % self.server_url, - realm_name=self.realm_name, - client_id=self.client_id, - client_secret_key=self.client_secret, + @ssl_verification + def get_url(self, name): + return self.openid_connect_client.get_url(name=name) + + def get_url_end_session_endpoint(self): + return self.get_url(name='end_session_endpoint') + + @ssl_verification + def get_authorization_url(self, redirect_uri, scope, state): + url = self.openid_connect_client.authorization_url( + redirect_uri=redirect_uri, scope=scope, state=state ) + return url - def update_or_create_from_password(self, username, password): - """ - Update or create an user based on an authentication username and password. + @ssl_verification + def get_userinfo(self, token): + user_info = self.openid_connect_client.userinfo(token=token) + return user_info - :param str username: authentication username - :param str password: authentication password - :return: OpenIDTokenProfile - """ + @ssl_verification + def authorization_code(self, code, redirect_uri): + token_response = self.openid_connect_client.authorization_code( + code=code, redirect_uri=redirect_uri + ) + return token_response + + @ssl_verification + def authorization_password(self, username, password): token_response = self.openid_client.token( username=username, password=password ) - - return self._update_or_create(token_response=token_response) + return token_response def update_or_create_from_code(self, code, redirect_uri): """ Update or create an user based on an authentication code. Response as specified in: - https://tools.ietf.org/html/rfc6749#section-4.1.4 - :param str code: authentication code :param str redirect_uri: :rtype: OpenIDTokenProfile """ + token_response = self.authorization_code(code, redirect_uri) + return self._update_or_create(token_response=token_response) - token_response = self.openid_connect_client.authorization_code( - code=code, redirect_uri=redirect_uri) - + def update_or_create_from_password(self, username, password): + """ + Update or create an user based on an authentication username and password. + :param str username: authentication username + :param str password: authentication password + :return: OpenIDTokenProfile + """ + token_response = self.authorization_password(username, password) return self._update_or_create(token_response=token_response) def _update_or_create(self, token_response): """ Update or create an user based on a token response. - `token_response` contains the items returned by the OpenIDConnect Token API end-point: - id_token @@ -109,14 +150,10 @@ class Client(object): - expires_in - refresh_token - refresh_expires_in - :param dict token_response: :rtype: OpenIDTokenProfile """ - - userinfo = self.openid_connect_client.userinfo( - token=token_response['access_token']) - + userinfo = self.get_userinfo(token=token_response['access_token']) with transaction.atomic(): user, _ = get_user_model().objects.update_or_create( username=userinfo.get('preferred_username', ''), @@ -126,13 +163,11 @@ class Client(object): 'last_name': userinfo.get('family_name', '') } ) - oidt_profile = OpenIDTokenProfile( user=user, access_token=token_response['access_token'], refresh_token=token_response['refresh_token'], ) - if user: post_create_openid_user.send(sender=user.__class__, user=user) @@ -140,17 +175,3 @@ class Client(object): def __str__(self): return self.client_id - - -class Nonce(object): - """ - The openid-login is stored in cache as a temporary object, recording the - user's redirect_uri and next_pat - """ - - def __init__(self, redirect_uri, next_path): - import uuid - self.state = uuid.uuid4() - self.redirect_uri = redirect_uri - self.next_path = next_path - diff --git a/apps/authentication/backends/openid/views.py b/apps/authentication/backends/openid/views.py index 45b5bfe23..5e51e7a38 100644 --- a/apps/authentication/backends/openid/views.py +++ b/apps/authentication/backends/openid/views.py @@ -24,7 +24,6 @@ __all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] class OpenIDLoginView(RedirectView): - def get_redirect_url(self, *args, **kwargs): redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL) nonce = Nonce( @@ -32,42 +31,36 @@ class OpenIDLoginView(RedirectView): next_path=self.request.GET.get('next') ) cache.set(str(nonce.state), nonce, 24*3600) + self.request.session['openid_state'] = str(nonce.state) - authorization_url = client.openid_connect_client.\ - authorization_url( - redirect_uri=nonce.redirect_uri, scope='code', - state=str(nonce.state) - ) + authorization_url = client.get_authorization_url( + redirect_uri=nonce.redirect_uri, + scope='code', + state=str(nonce.state) + ) return authorization_url class OpenIDLoginCompleteView(RedirectView): - def get(self, request, *args, **kwargs): if 'error' in request.GET: return HttpResponseServerError(self.request.GET['error']) - if 'code' not in self.request.GET and 'state' not in self.request.GET: - return HttpResponseBadRequest() - + return HttpResponseBadRequest(content='Code or State is empty') if self.request.GET['state'] != self.request.session['openid_state']: - return HttpResponseBadRequest() - + return HttpResponseBadRequest(content='State invalid') nonce = cache.get(self.request.GET['state']) - if not nonce: - return HttpResponseBadRequest() + return HttpResponseBadRequest(content='State failure') user = authenticate( request=self.request, code=self.request.GET['code'], redirect_uri=nonce.redirect_uri ) - cache.delete(str(nonce.state)) - if not user: - return HttpResponseBadRequest() + return HttpResponseBadRequest(content='Authenticate user failed') login(self.request, user) post_openid_login_success.send( diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index a0732894f..e401b7dc1 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -18,19 +18,17 @@ from .signals import post_auth_success, post_auth_failed def on_user_logged_out(sender, request, user, **kwargs): if not settings.AUTH_OPENID: return - + if not settings.AUTH_OPENID_SHARE_SESSION: + return query = QueryDict('', mutable=True) query.update({ 'redirect_uri': settings.BASE_SITE_URL }) - client = new_client() openid_logout_url = "%s?%s" % ( - client.openid_connect_client.get_url( - name='end_session_endpoint'), + client.get_url_end_session_endpoint(), query.urlencode() ) - request.COOKIES['next'] = openid_logout_url diff --git a/apps/common/apps.py b/apps/common/apps.py index ea797805d..9d4d80677 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -6,12 +6,16 @@ from django.dispatch import receiver from django.db.backends.signals import connection_created -@receiver(connection_created, dispatch_uid="my_unique_identifier") +@receiver(connection_created) def on_db_connection_ready(sender, **kwargs): from .signals import django_ready if 'migrate' not in sys.argv: django_ready.send(CommonConfig) + connection_created.disconnect(on_db_connection_ready) class CommonConfig(AppConfig): name = 'common' + + def ready(self): + from . import signals_handlers diff --git a/apps/common/fields/model.py b/apps/common/fields/model.py index c2bb1e0be..e1bd4e1e7 100644 --- a/apps/common/fields/model.py +++ b/apps/common/fields/model.py @@ -124,10 +124,27 @@ class EncryptTextField(EncryptMixin, models.TextField): class EncryptCharField(EncryptMixin, models.CharField): + @staticmethod + def change_max_length(kwargs): + kwargs.setdefault('max_length', 1024) + max_length = kwargs.get('max_length') + if max_length < 129: + max_length = 128 + max_length = max_length * 2 + kwargs['max_length'] = max_length + def __init__(self, *args, **kwargs): - kwargs['max_length'] = 2048 + self.change_max_length(kwargs) super().__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + max_length = kwargs.pop('max_length') + if max_length > 255: + max_length = max_length // 2 + kwargs['max_length'] = max_length + return name, path, args, kwargs + class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField): pass diff --git a/apps/common/filters.py b/apps/common/filters.py new file mode 100644 index 000000000..701f9c730 --- /dev/null +++ b/apps/common/filters.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import filters +from rest_framework.fields import DateTimeField +from rest_framework.serializers import ValidationError +import logging + +__all__ = ["DatetimeRangeFilter"] + + +class DatetimeRangeFilter(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + if not hasattr(view, 'date_range_filter_fields'): + return queryset + try: + fields = dict(view.date_range_filter_fields) + except ValueError: + msg = "View {} datetime_filter_fields set is error".format(view.name) + logging.error(msg) + return queryset + kwargs = {} + for attr, date_range_keyword in fields.items(): + if len(date_range_keyword) != 2: + continue + for i, v in enumerate(date_range_keyword): + value = request.query_params.get(v) + if not value: + continue + try: + field = DateTimeField() + value = field.to_internal_value(value) + if i == 0: + lookup = "__gte" + else: + lookup = "__lte" + kwargs[attr+lookup] = value + except ValidationError as e: + print(e) + continue + if kwargs: + queryset = queryset.filter(**kwargs) + return queryset diff --git a/apps/common/local.py b/apps/common/local.py new file mode 100644 index 000000000..37a2ccb0b --- /dev/null +++ b/apps/common/local.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +from werkzeug.local import Local + +thread_local = Local() + + +def _find(attr): + return getattr(thread_local, attr, None) diff --git a/apps/common/mixins.py b/apps/common/mixins.py deleted file mode 100644 index b46673cd6..000000000 --- a/apps/common/mixins.py +++ /dev/null @@ -1,234 +0,0 @@ -# coding: utf-8 - -from django.db import models -from django.http import JsonResponse -from django.utils import timezone -from django.core.cache import cache -from django.utils.translation import ugettext_lazy as _ -from django.contrib import messages -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 - -from .const import KEY_CACHE_RESOURCES_ID - - -class NoDeleteQuerySet(models.query.QuerySet): - - def delete(self): - return self.update(is_discard=True, discard_time=timezone.now()) - - -class NoDeleteManager(models.Manager): - - def get_all(self): - return NoDeleteQuerySet(self.model, using=self._db) - - def get_queryset(self): - return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=False) - - def get_deleted(self): - return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=True) - - -class NoDeleteModelMixin(models.Model): - is_discard = models.BooleanField(verbose_name=_("is discard"), default=False) - discard_time = models.DateTimeField(verbose_name=_("discard time"), null=True, blank=True) - - objects = NoDeleteManager() - - class Meta: - abstract = True - - def delete(self): - self.is_discard = True - self.discard_time = timezone.now() - return self.save() - - -class JSONResponseMixin(object): - """JSON mixin""" - @staticmethod - def render_json_response(context): - return JsonResponse(context) - - -class IDInFilterMixin(object): - def filter_queryset(self, queryset): - queryset = super(IDInFilterMixin, self).filter_queryset(queryset) - id_list = self.request.query_params.get('id__in') - if id_list: - import json - try: - ids = json.loads(id_list) - except Exception as e: - return queryset - if isinstance(ids, list): - queryset = queryset.filter(id__in=ids) - return queryset - - -class IDInCacheFilterMixin(object): - - def filter_queryset(self, queryset): - queryset = super(IDInCacheFilterMixin, self).filter_queryset(queryset) - spm = self.request.query_params.get('spm') - cache_key = KEY_CACHE_RESOURCES_ID.format(spm) - resources_id = cache.get(cache_key) - if resources_id and isinstance(resources_id, list): - queryset = queryset.filter(id__in=resources_id) - return queryset - - -class IDExportFilterMixin(object): - def filter_queryset(self, queryset): - # 下载导入模版 - if self.request.query_params.get('template') == 'import': - return [] - else: - return super(IDExportFilterMixin, self).filter_queryset(queryset) - - -class BulkSerializerMixin(object): - """ - Become rest_framework_bulk not support uuid as a primary key - so rewrite it. https://github.com/miki725/django-rest-framework-bulk/issues/66 - """ - def to_internal_value(self, data): - from rest_framework_bulk import BulkListSerializer - ret = super(BulkSerializerMixin, self).to_internal_value(data) - - id_attr = getattr(self.Meta, 'update_lookup_field', 'id') - if self.context.get('view'): - request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '') - # add update_lookup_field field back to validated data - # since super by default strips out read-only fields - # hence id will no longer be present in validated_data - if all((isinstance(self.root, BulkListSerializer), - id_attr, - request_method in ('PUT', 'PATCH'))): - id_field = self.fields[id_attr] - if data.get("id"): - id_value = id_field.to_internal_value(data.get("id")) - else: - id_value = id_field.to_internal_value(data.get("pk")) - ret[id_attr] = id_value - - return ret - - -class BulkListSerializerMixin(object): - """ - Become rest_framework_bulk doing bulk update raise Exception: - 'QuerySet' object has no attribute 'pk' when doing bulk update - so rewrite it . - https://github.com/miki725/django-rest-framework-bulk/issues/68 - """ - - def to_internal_value(self, data): - """ - List of dicts of native values <- List of dicts of primitive datatypes. - """ - if html.is_html_input(data): - data = html.parse_html_list(data) - - if not isinstance(data, list): - message = self.error_messages['not_a_list'].format( - input_type=type(data).__name__ - ) - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='not_a_list') - - if not self.allow_empty and len(data) == 0: - if self.parent and self.partial: - raise SkipField() - - message = self.error_messages['empty'] - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='empty') - - ret = [] - errors = [] - - for item in data: - try: - # prepare child serializer to only handle one instance - if 'id' in item.keys(): - self.child.instance = self.instance.get(id=item['id']) if self.instance else None - if 'pk' in item.keys(): - self.child.instance = self.instance.get(id=item['pk']) if self.instance else None - - self.child.initial_data = item - # raw - validated = self.child.run_validation(item) - except ValidationError as exc: - errors.append(exc.detail) - else: - ret.append(validated) - errors.append({}) - - if any(errors): - raise ValidationError(errors) - - return ret - - -class DatetimeSearchMixin: - date_format = '%Y-%m-%d' - date_from = date_to = None - - def get_date_range(self): - date_from_s = self.request.GET.get('date_from') - date_to_s = self.request.GET.get('date_to') - - if date_from_s: - date_from = timezone.datetime.strptime(date_from_s, self.date_format) - tz = timezone.get_current_timezone() - self.date_from = tz.localize(date_from) - else: - self.date_from = timezone.now() - timezone.timedelta(7) - - if date_to_s: - date_to = timezone.datetime.strptime( - date_to_s + ' 23:59:59', self.date_format + ' %H:%M:%S' - ) - self.date_to = date_to.replace( - tzinfo=timezone.get_current_timezone() - ) - else: - self.date_to = timezone.now() - - def get(self, request, *args, **kwargs): - self.get_date_range() - return super().get(request, *args, **kwargs) - - -class ApiMessageMixin: - success_message = _("%(name)s was %(action)s successfully") - _action_map = {"create": _("create"), "update": _("update")} - - def get_success_message(self, cleaned_data): - if not isinstance(cleaned_data, dict): - return '' - data = {k: v for k, v in cleaned_data.items()} - action = getattr(self, "action", "create") - data["action"] = self._action_map.get(action) - try: - message = self.success_message % data - except: - message = '' - return message - - def dispatch(self, request, *args, **kwargs): - resp = super().dispatch(request, *args, **kwargs) - if request.method.lower() in ("get", "delete", "patch"): - return resp - if resp.status_code >= 400: - return resp - message = self.get_success_message(resp.data) - if message: - messages.success(request, message) - return resp diff --git a/apps/common/mixins/__init__.py b/apps/common/mixins/__init__.py new file mode 100644 index 000000000..4249b3d64 --- /dev/null +++ b/apps/common/mixins/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# +from .models import * +from .serializers import * +from .api import * +from .views import * diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py new file mode 100644 index 000000000..ae92e3e97 --- /dev/null +++ b/apps/common/mixins/api.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +from django.http import JsonResponse +from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ +from django.contrib import messages + +from ..const import KEY_CACHE_RESOURCES_ID + +__all__ = [ + "JSONResponseMixin", "IDInCacheFilterMixin", "IDExportFilterMixin", + "IDInFilterMixin", "ApiMessageMixin" +] + + +class JSONResponseMixin(object): + """JSON mixin""" + @staticmethod + def render_json_response(context): + return JsonResponse(context) + + +class IDInFilterMixin(object): + def filter_queryset(self, queryset): + queryset = super(IDInFilterMixin, self).filter_queryset(queryset) + id_list = self.request.query_params.get('id__in') + if id_list: + import json + try: + ids = json.loads(id_list) + except Exception as e: + return queryset + if isinstance(ids, list): + queryset = queryset.filter(id__in=ids) + return queryset + + +class IDInCacheFilterMixin(object): + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + spm = self.request.query_params.get('spm') + if not spm: + return queryset + cache_key = KEY_CACHE_RESOURCES_ID.format(spm) + resources_id = cache.get(cache_key) + if resources_id and isinstance(resources_id, list): + queryset = queryset.filter(id__in=resources_id) + return queryset + + +class IDExportFilterMixin(object): + def filter_queryset(self, queryset): + # 下载导入模版 + if self.request.query_params.get('template') == 'import': + return [] + else: + return super(IDExportFilterMixin, self).filter_queryset(queryset) + + +class ApiMessageMixin: + success_message = _("%(name)s was %(action)s successfully") + _action_map = {"create": _("create"), "update": _("update")} + + def get_success_message(self, cleaned_data): + if not isinstance(cleaned_data, dict): + return '' + data = {k: v for k, v in cleaned_data.items()} + action = getattr(self, "action", "create") + data["action"] = self._action_map.get(action) + try: + message = self.success_message % data + except: + message = '' + return message + + def dispatch(self, request, *args, **kwargs): + resp = super().dispatch(request, *args, **kwargs) + if request.method.lower() in ("get", "delete", "patch"): + return resp + if resp.status_code >= 400: + return resp + message = self.get_success_message(resp.data) + if message: + messages.success(request, message) + return resp diff --git a/apps/common/mixins/models.py b/apps/common/mixins/models.py new file mode 100644 index 000000000..d4af23896 --- /dev/null +++ b/apps/common/mixins/models.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# + +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + + +__all__ = ["NoDeleteManager", "NoDeleteModelMixin", "NoDeleteQuerySet"] + + +class NoDeleteQuerySet(models.query.QuerySet): + + def delete(self): + return self.update(is_discard=True, discard_time=timezone.now()) + + +class NoDeleteManager(models.Manager): + + def get_all(self): + return NoDeleteQuerySet(self.model, using=self._db) + + def get_queryset(self): + return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=False) + + def get_deleted(self): + return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=True) + + +class NoDeleteModelMixin(models.Model): + is_discard = models.BooleanField(verbose_name=_("is discard"), default=False) + discard_time = models.DateTimeField(verbose_name=_("discard time"), null=True, blank=True) + + objects = NoDeleteManager() + + class Meta: + abstract = True + + def delete(self): + self.is_discard = True + self.discard_time = timezone.now() + return self.save() diff --git a/apps/common/mixins/serializers.py b/apps/common/mixins/serializers.py new file mode 100644 index 000000000..5f8668b93 --- /dev/null +++ b/apps/common/mixins/serializers.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# + +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 + +__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin'] + + +class BulkSerializerMixin(object): + """ + Become rest_framework_bulk not support uuid as a primary key + so rewrite it. https://github.com/miki725/django-rest-framework-bulk/issues/66 + """ + def to_internal_value(self, data): + from rest_framework_bulk import BulkListSerializer + ret = super(BulkSerializerMixin, self).to_internal_value(data) + + id_attr = getattr(self.Meta, 'update_lookup_field', 'id') + if self.context.get('view'): + request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '') + # add update_lookup_field field back to validated data + # since super by default strips out read-only fields + # hence id will no longer be present in validated_data + if all((isinstance(self.root, BulkListSerializer), + id_attr, + request_method in ('PUT', 'PATCH'))): + id_field = self.fields.get("id") or self.fields.get('pk') + if data.get("id"): + id_value = id_field.to_internal_value(data.get("id")) + else: + id_value = id_field.to_internal_value(data.get("pk")) + ret[id_attr] = id_value + return ret + + +class BulkListSerializerMixin(object): + """ + Become rest_framework_bulk doing bulk update raise Exception: + 'QuerySet' object has no attribute 'pk' when doing bulk update + so rewrite it . + https://github.com/miki725/django-rest-framework-bulk/issues/68 + """ + + def to_internal_value(self, data): + """ + List of dicts of native values <- List of dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_list(data) + + if not isinstance(data, list): + message = self.error_messages['not_a_list'].format( + input_type=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='not_a_list') + + if not self.allow_empty and len(data) == 0: + if self.parent and self.partial: + raise SkipField() + + message = self.error_messages['empty'] + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='empty') + + ret = [] + errors = [] + + for item in data: + try: + # prepare child serializer to only handle one instance + if 'id' in item.keys(): + self.child.instance = self.instance.get(id=item['id']) if self.instance else None + if 'pk' in item.keys(): + self.child.instance = self.instance.get(id=item['pk']) if self.instance else None + + self.child.initial_data = item + # raw + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py new file mode 100644 index 000000000..a00535b5b --- /dev/null +++ b/apps/common/mixins/views.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# coding: utf-8 + +from django.utils import timezone + + +__all__ = ["DatetimeSearchMixin"] + + +class DatetimeSearchMixin: + date_format = '%Y-%m-%d' + date_from = date_to = None + + def get_date_range(self): + date_from_s = self.request.GET.get('date_from') + date_to_s = self.request.GET.get('date_to') + + if date_from_s: + date_from = timezone.datetime.strptime(date_from_s, self.date_format) + tz = timezone.get_current_timezone() + self.date_from = tz.localize(date_from) + else: + self.date_from = timezone.now() - timezone.timedelta(7) + + if date_to_s: + date_to = timezone.datetime.strptime( + date_to_s + ' 23:59:59', self.date_format + ' %H:%M:%S' + ) + self.date_to = date_to.replace( + tzinfo=timezone.get_current_timezone() + ) + else: + self.date_to = timezone.now() + + def get(self, request, *args, **kwargs): + self.get_date_range() + return super().get(request, *args, **kwargs) + + diff --git a/apps/common/permissions.py b/apps/common/permissions.py index ec004df0b..edb5ee4d0 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -126,9 +126,33 @@ class WithBootstrapToken(permissions.BasePermission): class PermissionsMixin(UserPassesTestMixin): permission_classes = [] + def get_permissions(self): + return self.permission_classes + def test_func(self): - permission_classes = self.permission_classes + permission_classes = self.get_permissions() for permission_class in permission_classes: if not permission_class().has_permission(self.request, self): return False return True + + +class NeedMFAVerify(permissions.BasePermission): + def has_permission(self, request, view): + mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0) + if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL: + return True + return False + + +class CanUpdateSuperUser(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in ['GET', 'OPTIONS']: + return True + if str(request.user.id) == str(obj.id): + return False + if request.user.is_superuser: + return True + if hasattr(obj, 'is_superuser') and obj.is_superuser: + return False + return True diff --git a/apps/common/renders/csv.py b/apps/common/renders/csv.py index f917edd4c..9bd60cfbc 100644 --- a/apps/common/renders/csv.py +++ b/apps/common/renders/csv.py @@ -57,10 +57,15 @@ class JMSCSVRender(BaseRenderer): request = renderer_context['request'] template = request.query_params.get('template', 'export') view = renderer_context['view'] - data = json.loads(json.dumps(data, cls=encoders.JSONEncoder)) + + if isinstance(data, dict) and data.get("count"): + data = data["results"] + if template == 'import': data = [data[0]] if data else data + data = json.loads(json.dumps(data, cls=encoders.JSONEncoder)) + try: serializer = view.get_serializer() self.set_response_disposition(serializer, renderer_context) diff --git a/apps/common/signals_handlers.py b/apps/common/signals_handlers.py new file mode 100644 index 000000000..8a8463006 --- /dev/null +++ b/apps/common/signals_handlers.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# +import re +from collections import defaultdict +from django.conf import settings +from django.dispatch import receiver +from django.core.signals import request_finished +from django.db import connection + + +from common.utils import get_logger +from .local import thread_local + +pattern = re.compile(r'FROM `(\w+)`') +# logger = logging.getLogger('jmsdb') +logger = get_logger(__name__) + + +class Counter: + def __init__(self): + self.counter = 0 + self.time = 0 + + def __gt__(self, other): + return self.counter > other.counter + + def __lt__(self, other): + return self.counter < other.counter + + def __eq__(self, other): + return self.counter == other.counter + + +def on_request_finished_logging_db_query(sender, **kwargs): + queries = connection.queries + counters = defaultdict(Counter) + for query in queries: + if not query['sql'].startswith('SELECT'): + continue + tables = pattern.findall(query['sql']) + table_name = ''.join(tables) + time = query['time'] + counters[table_name].counter += 1 + counters[table_name].time += float(time) + counters['total'].counter += 1 + counters['total'].time += float(time) + + counters = sorted(counters.items(), key=lambda x: x[1]) + for name, counter in counters: + logger.debug("Query {:3} times using {:.2f}s {}".format( + counter.counter, counter.time, name) + ) + + +@receiver(request_finished) +def on_request_finished_release_local(sender, **kwargs): + thread_local.__release_local__() + + +if settings.DEBUG: + request_finished.connect(on_request_finished_logging_db_query) + + + + + + diff --git a/apps/common/struct.py b/apps/common/struct.py new file mode 100644 index 000000000..88bace4a7 --- /dev/null +++ b/apps/common/struct.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# + + +class Stack(list): + def is_empty(self): + return len(self) == 0 + + @property + def top(self): + if self.is_empty(): + return None + return self[-1] + + @property + def bottom(self): + if self.is_empty(): + return None + return self[0] + + def size(self): + return len(self) + + def push(self, item): + self.append(item) diff --git a/apps/common/templatetags/common_tags.py b/apps/common/templatetags/common_tags.py index 790759f0a..ede2ab9e0 100644 --- a/apps/common/templatetags/common_tags.py +++ b/apps/common/templatetags/common_tags.py @@ -112,7 +112,6 @@ def to_dict(data): @register.filter def sort(data): - print(data) return sorted(data) diff --git a/apps/common/tests.py b/apps/common/tests.py index 7ce503c2d..a9edb8f69 100644 --- a/apps/common/tests.py +++ b/apps/common/tests.py @@ -1,3 +1,16 @@ from django.test import TestCase # Create your tests here. + +from .utils import random_string, get_signer + + +def test_signer_len(): + signer = get_signer() + results = {} + for i in range(1, 4096): + s = random_string(i) + encs = signer.sign(s) + results[i] = (len(encs)/len(s)) + results = sorted(results.items(), key=lambda x: x[1], reverse=True) + print(results) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 79146c039..fdd4827d3 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -130,16 +130,13 @@ def get_short_uuid_str(): def is_uuid(seq): - if isinstance(seq, str): - if UUID_PATTERN.match(seq): - return True - else: - return False - else: - for s in seq: - if not is_uuid(s): - return False + if isinstance(seq, uuid.UUID): return True + elif isinstance(seq, str) and UUID_PATTERN.match(seq): + return True + elif isinstance(seq, (list, tuple)): + all([is_uuid(x) for x in seq]) + return False def get_request_ip(request): @@ -176,120 +173,9 @@ def with_cache(func): return wrapper -class LocalProxy(object): - - """ - Copy from werkzeug.local.LocalProxy - """ - __slots__ = ('__local', '__dict__', '__name__', '__wrapped__') - - def __init__(self, local, name=None): - object.__setattr__(self, '_LocalProxy__local', local) - object.__setattr__(self, '__name__', name) - if callable(local) and not hasattr(local, '__release_local__'): - # "local" is a callable that is not an instance of Local or - # LocalManager: mark it as a wrapped function. - object.__setattr__(self, '__wrapped__', local) - - def _get_current_object(self): - """Return the current object. This is useful if you want the real - object behind the proxy at a time for performance reasons or because - you want to pass the object into a different context. - """ - if not hasattr(self.__local, '__release_local__'): - return self.__local() - try: - return getattr(self.__local, self.__name__) - except AttributeError: - raise RuntimeError('no object bound to %s' % self.__name__) - - @property - def __dict__(self): - try: - return self._get_current_object().__dict__ - except RuntimeError: - raise AttributeError('__dict__') - - def __repr__(self): - try: - obj = self._get_current_object() - except RuntimeError: - return '<%s unbound>' % self.__class__.__name__ - return repr(obj) - - def __bool__(self): - try: - return bool(self._get_current_object()) - except RuntimeError: - return False - - def __dir__(self): - try: - return dir(self._get_current_object()) - except RuntimeError: - return [] - - def __getattr__(self, name): - if name == '__members__': - return dir(self._get_current_object()) - return getattr(self._get_current_object(), name) - - def __setitem__(self, key, value): - self._get_current_object()[key] = value - - def __delitem__(self, key): - del self._get_current_object()[key] - - __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v) - __delattr__ = lambda x, n: delattr(x._get_current_object(), n) - __str__ = lambda x: str(x._get_current_object()) - __lt__ = lambda x, o: x._get_current_object() < o - __le__ = lambda x, o: x._get_current_object() <= o - __eq__ = lambda x, o: x._get_current_object() == o - __ne__ = lambda x, o: x._get_current_object() != o - __gt__ = lambda x, o: x._get_current_object() > o - __ge__ = lambda x, o: x._get_current_object() >= o - __cmp__ = lambda x, o: cmp(x._get_current_object(), o) # noqa - __hash__ = lambda x: hash(x._get_current_object()) - __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw) - __len__ = lambda x: len(x._get_current_object()) - __getitem__ = lambda x, i: x._get_current_object()[i] - __iter__ = lambda x: iter(x._get_current_object()) - __contains__ = lambda x, i: i in x._get_current_object() - __add__ = lambda x, o: x._get_current_object() + o - __sub__ = lambda x, o: x._get_current_object() - o - __mul__ = lambda x, o: x._get_current_object() * o - __floordiv__ = lambda x, o: x._get_current_object() // o - __mod__ = lambda x, o: x._get_current_object() % o - __divmod__ = lambda x, o: x._get_current_object().__divmod__(o) - __pow__ = lambda x, o: x._get_current_object() ** o - __lshift__ = lambda x, o: x._get_current_object() << o - __rshift__ = lambda x, o: x._get_current_object() >> o - __and__ = lambda x, o: x._get_current_object() & o - __xor__ = lambda x, o: x._get_current_object() ^ o - __or__ = lambda x, o: x._get_current_object() | o - __div__ = lambda x, o: x._get_current_object().__div__(o) - __truediv__ = lambda x, o: x._get_current_object().__truediv__(o) - __neg__ = lambda x: -(x._get_current_object()) - __pos__ = lambda x: +(x._get_current_object()) - __abs__ = lambda x: abs(x._get_current_object()) - __invert__ = lambda x: ~(x._get_current_object()) - __complex__ = lambda x: complex(x._get_current_object()) - __int__ = lambda x: int(x._get_current_object()) - __float__ = lambda x: float(x._get_current_object()) - __oct__ = lambda x: oct(x._get_current_object()) - __hex__ = lambda x: hex(x._get_current_object()) - __index__ = lambda x: x._get_current_object().__index__() - __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o) - __enter__ = lambda x: x._get_current_object().__enter__() - __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw) - __radd__ = lambda x, o: o + x._get_current_object() - __rsub__ = lambda x, o: o - x._get_current_object() - __rmul__ = lambda x, o: o * x._get_current_object() - __rdiv__ = lambda x, o: o / x._get_current_object() - __rtruediv__ = __rdiv__ - __rfloordiv__ = lambda x, o: o // x._get_current_object() - __rmod__ = lambda x, o: o % x._get_current_object() - __rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o) - __copy__ = lambda x: copy.copy(x._get_current_object()) - __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo) +def random_string(length): + import string + import random + charset = string.ascii_letters + string.digits + s = [random.choice(charset) for i in range(length)] + return ''.join(s) diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index aefa19238..fc9467cba 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -51,7 +51,7 @@ class Signer(metaclass=Singleton): try: return s.loads(value) except BadSignature: - return {} + return None def sign_t(self, value, expires_in=3600): s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in) @@ -62,7 +62,7 @@ class Signer(metaclass=Singleton): try: return s.loads(value) except (BadSignature, SignatureExpired): - return {} + return None def ssh_key_string_to_obj(text, password=None): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 0362a68e3..17d660be8 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -344,7 +344,9 @@ defaults = { 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'AUTH_OPENID': False, - 'OTP_VALID_WINDOW': 0, + 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, + 'AUTH_OPENID_SHARE_SESSION': False, + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'Jumpserver', 'EMAIL_SUFFIX': 'jumpserver.org', 'TERMINAL_PASSWORD_AUTH': True, @@ -373,8 +375,8 @@ defaults = { 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, 'LOGIN_LOG_KEEP_DAYS': 90, - 'ASSETS_PERM_CACHE_TIME': 3600, - + 'ASSETS_PERM_CACHE_TIME': 3600*24, + 'SECURITY_MFA_VERIFY_TTL': 3600, } diff --git a/apps/jumpserver/const.py b/apps/jumpserver/const.py index f8f85d7f9..c6b7e0e63 100644 --- a/apps/jumpserver/const.py +++ b/apps/jumpserver/const.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- # -VERSION = '1.5.0' +VERSION = '1.5.1' diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 148611359..91a720fd7 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -17,6 +17,7 @@ def jumpserver_processor(request): 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019', 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, + 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, } return context diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 453b1b9e7..35ffebc25 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -111,6 +111,7 @@ MIDDLEWARE = [ 'orgs.middleware.OrgMiddleware', ] + ROOT_URLCONF = 'jumpserver.urls' TEMPLATES = [ @@ -398,7 +399,7 @@ REST_FRAMEWORK = { 'ORDERING_PARAM': "order", 'SEARCH_PARAM': "search", 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', - 'DATETIME_INPUT_FORMATS': ['%Y-%m-%d %H:%M:%S %z'], + 'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'], # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # 'PAGE_SIZE': 15 } @@ -456,6 +457,8 @@ AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION AUTH_OPENID_BACKENDS = [ 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend', 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend', @@ -564,6 +567,7 @@ SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_NUMBER', 'SECURITY_PASSWORD_SPECIAL_CHAR' ] +SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 96c702acc..f13bb667a 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -7,7 +7,7 @@ from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.views.i18n import JavaScriptCatalog -from .views import IndexView, LunaView, I18NView +from .views import IndexView, LunaView, I18NView, HealthCheckView from .swagger import get_swagger_view api_v1 = [ @@ -44,8 +44,12 @@ app_view_patterns = [ if settings.XPACK_ENABLED: - app_view_patterns.append(path('xpack/', include('xpack.urls.view_urls', namespace='xpack'))) - api_v1.append(path('xpack/v1/', include('xpack.urls.api_urls', namespace='api-xpack'))) + app_view_patterns.append( + path('xpack/', include('xpack.urls.view_urls', namespace='xpack')) + ) + api_v1.append( + path('xpack/v1/', include('xpack.urls.api_urls', namespace='api-xpack')) + ) js_i18n_patterns = i18n_patterns( path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), @@ -63,6 +67,7 @@ urlpatterns = [ path('', IndexView.as_view(), name='index'), path('', include(api_v2_patterns)), path('', include(api_v1_patterns)), + path('api/health/', HealthCheckView.as_view(), name="health"), path('luna/', LunaView.as_view(), name='luna-view'), path('i18n//', I18NView.as_view(), name='i18n-switch'), path('settings/', include('settings.urls.view_urls', namespace='settings')), @@ -92,3 +97,4 @@ if settings.DEBUG: path('docs/v2/', get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"), path('redoc/v2/', get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'), ] + diff --git a/apps/jumpserver/utils.py b/apps/jumpserver/utils.py index 0f7f28eb9..aa808d2f2 100644 --- a/apps/jumpserver/utils.py +++ b/apps/jumpserver/utils.py @@ -1,24 +1,16 @@ # -*- coding: utf-8 -*- # - from functools import partial - -from common.utils import LocalProxy - -try: - from threading import local -except ImportError: - from django.utils._threading_local import local - -_thread_locals = local() +from werkzeug.local import LocalProxy +from common.local import thread_local def set_current_request(request): - setattr(_thread_locals, 'current_request', request) + setattr(thread_local, 'current_request', request) def _find(attr): - return getattr(_thread_locals, attr, None) + return getattr(thread_local, attr, None) def get_current_request(): diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py index f3272fb17..7f7662add 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -1,5 +1,6 @@ import datetime import re +import time from django.http import HttpResponse, HttpResponseRedirect from django.conf import settings @@ -9,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Count from django.shortcuts import redirect from rest_framework.response import Response +from rest_framework.views import APIView from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from django.utils.encoding import iri_to_uri @@ -222,3 +224,10 @@ def redirect_format_api(request, *args, **kwargs): return HttpResponseTemporaryRedirect(_path) else: return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404) + + +class HealthCheckView(APIView): + permission_classes = () + + def get(self, request): + return Response({"status": 1, "time": int(time.time())}) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c4e24380b..1e5e6a70e 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 2780ee85f..725d56bc6 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: 2019-06-25 15:22+0800\n" +"POT-Creation-Date: 2019-07-04 17:13+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -76,9 +76,9 @@ msgstr "运行参数" #: applications/templates/applications/remote_app_list.html:22 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:319 assets/models/authbook.py:27 -#: assets/serializers/admin_user.py:24 assets/serializers/asset_user.py:105 -#: assets/serializers/system_user.py:28 +#: assets/models/asset.py:292 assets/models/authbook.py:24 +#: assets/serializers/admin_user.py:35 assets/serializers/asset_user.py:89 +#: assets/serializers/system_user.py:29 #: assets/templates/assets/admin_user_list.html:49 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 @@ -86,16 +86,16 @@ msgstr "运行参数" #: assets/templates/assets/system_user_list.html:55 audits/models.py:19 #: audits/templates/audits/ftp_log_list.html:41 #: audits/templates/audits/ftp_log_list.html:71 -#: perms/forms/asset_permission.py:46 perms/models/asset_permission.py:37 +#: perms/forms/asset_permission.py:68 perms/models/asset_permission.py:68 #: perms/templates/perms/asset_permission_create_update.html:45 -#: perms/templates/perms/asset_permission_list.html:56 -#: perms/templates/perms/asset_permission_list.html:125 +#: perms/templates/perms/asset_permission_list.html:48 +#: perms/templates/perms/asset_permission_list.html:117 #: terminal/backends/command/models.py:13 terminal/models.py:155 -#: terminal/templates/terminal/command_list.html:40 -#: terminal/templates/terminal/command_list.html:73 -#: terminal/templates/terminal/session_list.html:41 +#: terminal/templates/terminal/command_list.html:30 +#: terminal/templates/terminal/command_list.html:66 +#: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 -#: xpack/plugins/change_auth_plan/forms.py:114 +#: xpack/plugins/change_auth_plan/forms.py:115 #: xpack/plugins/change_auth_plan/models.py:413 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 @@ -112,19 +112,19 @@ msgstr "资产" #: applications/templates/applications/remote_app_detail.html:61 #: applications/templates/applications/remote_app_list.html:23 #: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/user.py:251 assets/templates/assets/user_asset_list.html:172 +#: assets/models/user.py:160 assets/templates/assets/user_asset_list.html:172 #: audits/models.py:20 audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:72 -#: perms/forms/asset_permission.py:52 perms/models/asset_permission.py:39 -#: perms/models/asset_permission.py:59 +#: perms/forms/asset_permission.py:74 perms/models/asset_permission.py:70 +#: perms/models/asset_permission.py:95 #: perms/templates/perms/asset_permission_detail.html:140 -#: perms/templates/perms/asset_permission_list.html:58 -#: perms/templates/perms/asset_permission_list.html:79 -#: perms/templates/perms/asset_permission_list.html:131 templates/_nav.html:25 +#: perms/templates/perms/asset_permission_list.html:50 +#: perms/templates/perms/asset_permission_list.html:71 +#: perms/templates/perms/asset_permission_list.html:123 templates/_nav.html:25 #: terminal/backends/command/models.py:14 terminal/models.py:156 -#: terminal/templates/terminal/command_list.html:48 -#: terminal/templates/terminal/command_list.html:74 -#: terminal/templates/terminal/session_list.html:49 +#: terminal/templates/terminal/command_list.html:31 +#: terminal/templates/terminal/command_list.html:67 +#: terminal/templates/terminal/session_list.html:29 #: terminal/templates/terminal/session_list.html:73 #: xpack/plugins/orgs/templates/orgs/org_list.html:19 msgid "System user" @@ -135,7 +135,7 @@ msgstr "系统用户" #: applications/templates/applications/remote_app_list.html:20 #: applications/templates/applications/user_remote_app_list.html:16 #: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:148 -#: assets/models/asset.py:72 assets/models/base.py:27 +#: assets/models/asset.py:64 assets/models/base.py:28 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:20 #: assets/models/domain.py:20 assets/models/group.py:20 #: assets/models/label.py:18 assets/templates/assets/admin_user_detail.html:56 @@ -149,11 +149,10 @@ msgstr "系统用户" #: assets/templates/assets/system_user_detail.html:58 #: assets/templates/assets/system_user_list.html:51 ops/models/adhoc.py:37 #: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:27 -#: orgs/models.py:12 perms/models/asset_permission.py:22 -#: perms/models/base.py:35 +#: orgs/models.py:11 perms/models/base.py:35 #: perms/templates/perms/asset_permission_detail.html:62 -#: perms/templates/perms/asset_permission_list.html:53 -#: perms/templates/perms/asset_permission_list.html:72 +#: perms/templates/perms/asset_permission_list.html:45 +#: perms/templates/perms/asset_permission_list.html:64 #: perms/templates/perms/asset_permission_user.html:54 #: perms/templates/perms/remote_app_permission_detail.html:62 #: perms/templates/perms/remote_app_permission_list.html:14 @@ -167,14 +166,14 @@ msgstr "系统用户" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:22 #: terminal/models.py:258 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:63 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:64 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 #: users/templates/users/user_list.html:35 #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:53 -#: xpack/plugins/change_auth_plan/forms.py:97 +#: xpack/plugins/change_auth_plan/forms.py:98 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 @@ -206,7 +205,7 @@ msgstr "参数" #: applications/models/remote_app.py:43 #: applications/templates/applications/remote_app_detail.html:77 -#: assets/models/asset.py:132 assets/models/base.py:35 +#: assets/models/asset.py:124 assets/models/base.py:36 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:58 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 @@ -214,11 +213,11 @@ msgstr "参数" #: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 -#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 -#: perms/models/asset_permission.py:62 perms/models/base.py:41 +#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:14 +#: perms/models/asset_permission.py:98 perms/models/base.py:41 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:104 users/serializers/v1.py:73 +#: users/models/user.py:105 users/serializers/v1.py:116 #: users/templates/users/user_detail.html:111 #: xpack/plugins/change_auth_plan/models.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 @@ -230,16 +229,15 @@ msgstr "创建者" # msgstr "创建者" #: applications/models/remote_app.py:46 #: applications/templates/applications/remote_app_detail.html:73 -#: assets/models/asset.py:133 assets/models/base.py:33 +#: assets/models/asset.py:125 assets/models/base.py:34 #: assets/models/cluster.py:26 assets/models/domain.py:23 #: assets/models/group.py:22 assets/models/label.py:25 -#: assets/serializers/admin_user.py:38 #: assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/cmd_filter_detail.html:69 #: assets/templates/assets/domain_detail.html:68 #: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 -#: orgs/models.py:16 perms/models/asset_permission.py:63 +#: orgs/models.py:15 perms/models/asset_permission.py:99 #: perms/models/base.py:42 #: perms/templates/perms/asset_permission_detail.html:94 #: perms/templates/perms/remote_app_permission_detail.html:86 @@ -259,7 +257,7 @@ msgstr "创建日期" #: applications/templates/applications/remote_app_detail.html:81 #: applications/templates/applications/remote_app_list.html:24 #: applications/templates/applications/user_remote_app_list.html:20 -#: assets/models/asset.py:134 assets/models/base.py:32 +#: assets/models/asset.py:126 assets/models/base.py:33 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21 #: assets/models/domain.py:53 assets/models/group.py:23 @@ -275,13 +273,13 @@ msgstr "创建日期" #: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_list.html:59 #: assets/templates/assets/user_asset_list.html:175 ops/models/adhoc.py:43 -#: orgs/models.py:17 perms/models/asset_permission.py:64 +#: orgs/models.py:16 perms/models/asset_permission.py:100 #: perms/models/base.py:43 #: perms/templates/perms/asset_permission_detail.html:102 #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:32 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:96 users/templates/users/user_detail.html:127 +#: users/models/user.py:97 users/templates/users/user_detail.html:127 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:134 @@ -351,7 +349,7 @@ msgstr "重置" #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 #: assets/templates/assets/asset_create.html:82 -#: assets/templates/assets/asset_list.html:125 +#: assets/templates/assets/asset_list.html:117 #: assets/templates/assets/cmd_filter_create_update.html:16 #: assets/templates/assets/cmd_filter_rule_create_update.html:41 #: assets/templates/assets/domain_create_update.html:17 @@ -368,8 +366,8 @@ msgstr "重置" #: settings/templates/settings/replay_storage_create.html:153 #: settings/templates/settings/security_setting.html:74 #: settings/templates/settings/terminal_setting.html:73 -#: terminal/templates/terminal/command_list.html:103 -#: terminal/templates/terminal/session_list.html:127 +#: terminal/templates/terminal/command_list.html:47 +#: terminal/templates/terminal/session_list.html:52 #: terminal/templates/terminal/terminal_update.html:46 #: users/templates/users/_user.html:51 #: users/templates/users/forgot_password.html:42 @@ -391,7 +389,7 @@ msgstr "提交" #: assets/templates/assets/cmd_filter_rule_list.html:19 #: assets/templates/assets/domain_detail.html:18 #: assets/templates/assets/domain_gateway_list.html:20 -#: assets/templates/assets/system_user_asset.html:18 +#: assets/templates/assets/system_user_assets.html:18 #: assets/templates/assets/system_user_detail.html:18 #: ops/templates/ops/adhoc_history.html:130 #: ops/templates/ops/task_adhoc.html:116 @@ -412,13 +410,13 @@ msgstr "详情" #: applications/templates/applications/remote_app_detail.html:21 #: applications/templates/applications/remote_app_list.html:56 -#: assets/templates/assets/_asset_user_list.html:62 +#: assets/templates/assets/_asset_user_list.html:70 #: assets/templates/assets/admin_user_detail.html:24 #: assets/templates/assets/admin_user_list.html:29 -#: assets/templates/assets/admin_user_list.html:112 +#: assets/templates/assets/admin_user_list.html:114 #: assets/templates/assets/asset_detail.html:27 -#: assets/templates/assets/asset_list.html:86 -#: assets/templates/assets/asset_list.html:190 +#: assets/templates/assets/asset_list.html:78 +#: assets/templates/assets/asset_list.html:169 #: assets/templates/assets/cmd_filter_detail.html:29 #: assets/templates/assets/cmd_filter_list.html:58 #: assets/templates/assets/cmd_filter_rule_list.html:86 @@ -429,9 +427,9 @@ msgstr "详情" #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:26 #: assets/templates/assets/system_user_list.html:33 -#: assets/templates/assets/system_user_list.html:118 audits/models.py:33 +#: assets/templates/assets/system_user_list.html:119 audits/models.py:33 #: perms/templates/perms/asset_permission_detail.html:30 -#: perms/templates/perms/asset_permission_list.html:181 +#: perms/templates/perms/asset_permission_list.html:173 #: perms/templates/perms/remote_app_permission_detail.html:30 #: perms/templates/perms/remote_app_permission_list.html:59 #: terminal/templates/terminal/terminal_detail.html:16 @@ -458,9 +456,9 @@ msgstr "更新" #: applications/templates/applications/remote_app_detail.html:25 #: applications/templates/applications/remote_app_list.html:57 #: assets/templates/assets/admin_user_detail.html:28 -#: assets/templates/assets/admin_user_list.html:113 +#: assets/templates/assets/admin_user_list.html:115 #: assets/templates/assets/asset_detail.html:31 -#: assets/templates/assets/asset_list.html:191 +#: assets/templates/assets/asset_list.html:170 #: assets/templates/assets/cmd_filter_detail.html:33 #: assets/templates/assets/cmd_filter_list.html:59 #: assets/templates/assets/cmd_filter_rule_list.html:87 @@ -470,10 +468,10 @@ msgstr "更新" #: assets/templates/assets/domain_list.html:55 #: assets/templates/assets/label_list.html:40 #: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:119 audits/models.py:34 +#: assets/templates/assets/system_user_list.html:120 audits/models.py:34 #: ops/templates/ops/task_list.html:64 #: perms/templates/perms/asset_permission_detail.html:34 -#: perms/templates/perms/asset_permission_list.html:182 +#: perms/templates/perms/asset_permission_list.html:174 #: perms/templates/perms/remote_app_permission_detail.html:34 #: perms/templates/perms/remote_app_permission_list.html:60 #: settings/templates/settings/terminal_setting.html:93 @@ -518,7 +516,7 @@ msgstr "创建远程应用" #: assets/models/cmd_filter.py:54 #: assets/templates/assets/_asset_user_list.html:20 #: assets/templates/assets/admin_user_list.html:54 -#: assets/templates/assets/asset_list.html:108 +#: assets/templates/assets/asset_list.html:100 #: assets/templates/assets/cmd_filter_list.html:28 #: assets/templates/assets/cmd_filter_rule_list.html:63 #: assets/templates/assets/domain_gateway_list.html:73 @@ -530,15 +528,14 @@ msgstr "创建远程应用" #: audits/templates/audits/operate_log_list.html:67 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 #: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:34 -#: perms/forms/asset_permission.py:55 perms/models/asset_permission.py:26 -#: perms/models/asset_permission.py:40 +#: perms/forms/asset_permission.py:21 #: perms/templates/perms/asset_permission_create_update.html:50 -#: perms/templates/perms/asset_permission_list.html:60 -#: perms/templates/perms/asset_permission_list.html:134 +#: perms/templates/perms/asset_permission_list.html:52 +#: perms/templates/perms/asset_permission_list.html:126 #: perms/templates/perms/remote_app_permission_list.html:19 #: settings/templates/settings/terminal_setting.html:85 #: settings/templates/settings/terminal_setting.html:107 -#: terminal/templates/terminal/session_list.html:81 +#: terminal/templates/terminal/session_list.html:36 #: terminal/templates/terminal/terminal_list.html:36 #: users/templates/users/user_group_list.html:38 #: users/templates/users/user_list.html:41 @@ -552,7 +549,8 @@ msgid "Action" msgstr "动作" #: applications/templates/applications/user_remote_app_list.html:57 -#: assets/templates/assets/user_asset_list.html:100 perms/const.py:19 +#: assets/templates/assets/user_asset_list.html:100 +#: perms/models/asset_permission.py:27 msgid "Connect" msgstr "连接" @@ -578,38 +576,57 @@ msgstr "远程应用详情" msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/asset.py:50 +#: assets/api/asset.py:51 #, python-format msgid "%(hostname)s was %(action)s successfully" msgstr "%(hostname)s %(action)s成功" -#: assets/api/asset.py:129 +#: assets/api/asset.py:125 msgid "Please select assets that need to be updated" msgstr "请选择需要更新的资产" -#: assets/api/node.py:60 +#: assets/api/node.py:61 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:285 +#: assets/api/node.py:283 msgid "Update node asset hardware information: {}" msgstr "更新节点资产硬件信息: {}" -#: assets/api/node.py:299 +#: assets/api/node.py:297 msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" -#: assets/forms/asset.py:45 assets/models/asset.py:103 -#: assets/models/user.py:134 assets/templates/assets/asset_detail.html:194 +#: assets/const.py:77 assets/models/utils.py:43 +#: assets/templates/assets/admin_user_list.html:51 +#: assets/templates/assets/system_user_list.html:57 +msgid "Unreachable" +msgstr "不可达" + +#: assets/const.py:78 assets/models/utils.py:44 +#: assets/templates/assets/admin_user_list.html:50 +#: assets/templates/assets/asset_list.html:99 +#: assets/templates/assets/system_user_list.html:56 +#: users/templates/users/user_group_granted_asset.html:47 +msgid "Reachable" +msgstr "可连接" + +#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:9 +#: xpack/plugins/license/models.py:78 +msgid "Unknown" +msgstr "未知" + +#: assets/forms/asset.py:51 assets/models/asset.py:95 assets/models/user.py:107 +#: assets/templates/assets/asset_detail.html:194 #: assets/templates/assets/asset_detail.html:202 -#: assets/templates/assets/system_user_asset.html:83 -#: perms/models/asset_permission.py:38 +#: assets/templates/assets/system_user_assets.html:83 +#: perms/models/asset_permission.py:69 #: xpack/plugins/change_auth_plan/models.py:72 msgid "Nodes" msgstr "节点" -#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:107 -#: assets/models/cluster.py:19 assets/models/user.py:92 +#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:99 +#: assets/models/cluster.py:19 assets/models/user.py:65 #: assets/templates/assets/asset_detail.html:80 templates/_nav.html:24 #: xpack/plugins/cloud/models.py:124 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:65 @@ -617,16 +634,16 @@ msgstr "节点" msgid "Admin user" msgstr "管理用户" -#: assets/forms/asset.py:51 assets/forms/asset.py:86 assets/forms/asset.py:125 +#: assets/forms/asset.py:57 assets/forms/asset.py:92 assets/forms/asset.py:131 #: assets/templates/assets/asset_create.html:48 #: assets/templates/assets/asset_create.html:50 -#: assets/templates/assets/asset_list.html:93 +#: assets/templates/assets/asset_list.html:85 #: assets/templates/assets/user_asset_list.html:33 #: xpack/plugins/orgs/templates/orgs/org_list.html:20 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:102 +#: assets/forms/asset.py:60 assets/forms/asset.py:95 assets/models/asset.py:94 #: assets/models/domain.py:26 assets/models/domain.py:52 #: assets/templates/assets/asset_detail.html:84 #: assets/templates/assets/user_asset_list.html:173 @@ -634,15 +651,15 @@ msgstr "标签" msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93 -#: assets/forms/asset.py:128 assets/models/node.py:31 +#: assets/forms/asset.py:64 assets/forms/asset.py:86 assets/forms/asset.py:99 +#: assets/forms/asset.py:134 assets/models/node.py:249 #: assets/templates/assets/asset_create.html:42 -#: perms/forms/asset_permission.py:49 perms/forms/asset_permission.py:59 -#: perms/models/asset_permission.py:57 -#: perms/templates/perms/asset_permission_list.html:57 -#: perms/templates/perms/asset_permission_list.html:78 -#: perms/templates/perms/asset_permission_list.html:128 -#: xpack/plugins/change_auth_plan/forms.py:115 +#: perms/forms/asset_permission.py:71 perms/forms/asset_permission.py:79 +#: perms/models/asset_permission.py:93 +#: perms/templates/perms/asset_permission_list.html:49 +#: perms/templates/perms/asset_permission_list.html:70 +#: perms/templates/perms/asset_permission_list.html:120 +#: xpack/plugins/change_auth_plan/forms.py:116 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15 #: xpack/plugins/cloud/models.py:123 @@ -651,7 +668,7 @@ msgstr "网域" msgid "Node" msgstr "节点" -#: assets/forms/asset.py:62 assets/forms/asset.py:97 +#: assets/forms/asset.py:68 assets/forms/asset.py:103 msgid "" "root or other NOPASSWD sudo privilege user existed in asset,If asset is " "windows or other set any one, more see admin user left menu" @@ -659,23 +676,27 @@ msgstr "" "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一" "个, 更多信息查看左侧 `管理用户` 菜单" -#: assets/forms/asset.py:65 assets/forms/asset.py:100 +#: assets/forms/asset.py:71 assets/forms/asset.py:106 msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" -#: assets/forms/asset.py:66 assets/forms/asset.py:101 +#: assets/forms/asset.py:72 assets/forms/asset.py:107 msgid "" "If your have some network not connect with each other, you can set domain" msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" -#: assets/forms/asset.py:108 assets/forms/asset.py:112 +#: assets/forms/asset.py:114 assets/forms/asset.py:118 #: assets/forms/domain.py:17 assets/forms/label.py:15 #: perms/templates/perms/asset_permission_asset.html:88 -#: xpack/plugins/change_auth_plan/forms.py:105 +#: xpack/plugins/change_auth_plan/forms.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:84 msgid "Select assets" msgstr "选择资产" +#: assets/forms/cmd_filter.py:37 assets/serializers/cmd_filter.py:34 +msgid "Content should not be contain: {}" +msgstr "内容不能包含: {}" + #: assets/forms/domain.py:51 msgid "Password should not contain special characters" msgstr "不能包含特殊字符" @@ -685,7 +706,7 @@ msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:149 -#: assets/models/base.py:28 +#: assets/models/base.py:29 #: assets/templates/assets/_asset_user_auth_update_modal.html:15 #: assets/templates/assets/_asset_user_auth_view_modal.html:21 #: assets/templates/assets/_asset_user_list.html:16 @@ -697,15 +718,15 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: audits/templates/audits/login_log_list.html:51 authentication/forms.py:11 #: authentication/templates/authentication/login.html:64 #: authentication/templates/authentication/new_login.html:90 -#: ops/models/adhoc.py:164 perms/templates/perms/asset_permission_list.html:74 +#: ops/models/adhoc.py:164 perms/templates/perms/asset_permission_list.html:66 #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 -#: settings/templates/settings/_ldap_list_users_modal.html:37 users/forms.py:13 -#: users/models/user.py:61 users/templates/users/_select_user_modal.html:14 +#: settings/templates/settings/_ldap_list_users_modal.html:37 users/forms.py:14 +#: users/models/user.py:62 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 -#: xpack/plugins/change_auth_plan/forms.py:99 +#: xpack/plugins/change_auth_plan/forms.py:100 #: xpack/plugins/change_auth_plan/models.py:63 #: xpack/plugins/change_auth_plan/models.py:409 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 @@ -719,15 +740,14 @@ msgstr "用户名" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:26 assets/models/base.py:29 -#: assets/serializers/admin_user.py:21 assets/serializers/asset_user.py:33 -#: assets/serializers/asset_user.py:86 assets/serializers/system_user.py:16 +#: assets/forms/user.py:26 assets/models/base.py:30 +#: assets/serializers/asset_user.py:70 #: assets/templates/assets/_asset_user_auth_update_modal.html:21 #: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:13 #: authentication/templates/authentication/login.html:67 #: authentication/templates/authentication/new_login.html:93 -#: settings/forms.py:110 users/forms.py:15 users/forms.py:27 +#: settings/forms.py:110 users/forms.py:16 users/forms.py:28 #: users/templates/users/reset_password.html:53 #: users/templates/users/user_password_authentication.html:18 #: users/templates/users/user_password_update.html:43 @@ -739,10 +759,9 @@ msgstr "密码或密钥密码" msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 assets/serializers/asset_user.py:41 -#: assets/serializers/asset_user.py:94 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:78 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:90 +#: users/models/user.py:91 msgid "Private key" msgstr "ssh私钥" @@ -759,7 +778,7 @@ msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" #: assets/forms/user.py:151 assets/models/cmd_filter.py:31 -#: assets/models/user.py:142 assets/templates/assets/_system_user.html:66 +#: assets/models/user.py:115 assets/templates/assets/_system_user.html:66 #: assets/templates/assets/system_user_detail.html:165 msgid "Command filter" msgstr "命令过滤器" @@ -786,7 +805,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写" msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:73 assets/models/asset.py:98 +#: assets/models/asset.py:65 assets/models/asset.py:90 #: assets/models/domain.py:50 #: assets/templates/assets/domain_gateway_list.html:69 #: assets/templates/assets/user_asset_list.html:168 @@ -794,12 +813,12 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" msgid "Port" msgstr "端口" -#: assets/models/asset.py:93 assets/models/domain.py:49 -#: assets/serializers/asset_user.py:28 +#: assets/models/asset.py:85 assets/models/domain.py:49 +#: assets/serializers/asset_user.py:29 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_list.html:15 #: assets/templates/assets/asset_detail.html:64 -#: assets/templates/assets/asset_list.html:105 +#: assets/templates/assets/asset_list.html:97 #: assets/templates/assets/domain_gateway_list.html:68 #: assets/templates/assets/user_asset_list.html:45 #: assets/templates/assets/user_asset_list.html:167 @@ -811,143 +830,123 @@ msgstr "端口" msgid "IP" msgstr "IP" -#: assets/models/asset.py:94 assets/serializers/asset_user.py:27 +#: assets/models/asset.py:86 assets/serializers/asset_user.py:28 #: assets/templates/assets/_asset_list_modal.html:45 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 #: assets/templates/assets/_asset_user_auth_view_modal.html:15 #: assets/templates/assets/_asset_user_list.html:14 #: assets/templates/assets/asset_detail.html:60 -#: assets/templates/assets/asset_list.html:104 +#: assets/templates/assets/asset_list.html:96 #: assets/templates/assets/user_asset_list.html:44 #: assets/templates/assets/user_asset_list.html:166 #: perms/templates/perms/asset_permission_asset.html:54 -#: perms/templates/perms/asset_permission_list.html:77 settings/forms.py:139 +#: perms/templates/perms/asset_permission_list.html:69 settings/forms.py:139 #: users/templates/users/user_granted_asset.html:44 #: users/templates/users/user_group_granted_asset.html:44 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:97 assets/models/asset.py:100 -#: assets/models/domain.py:51 assets/models/user.py:137 +#: assets/models/asset.py:89 assets/models/asset.py:92 +#: assets/models/domain.py:51 assets/models/user.py:110 #: assets/templates/assets/asset_detail.html:72 #: assets/templates/assets/domain_gateway_list.html:70 #: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_list.html:53 #: assets/templates/assets/user_asset_list.html:169 +#: terminal/templates/terminal/session_list.html:31 #: terminal/templates/terminal/session_list.html:75 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:101 assets/templates/assets/asset_detail.html:108 +#: assets/models/asset.py:93 assets/templates/assets/asset_detail.html:108 #: assets/templates/assets/user_asset_list.html:170 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:104 assets/models/cmd_filter.py:21 +#: assets/models/asset.py:96 assets/models/cmd_filter.py:21 #: assets/models/domain.py:54 assets/models/label.py:22 #: assets/templates/assets/asset_detail.html:116 #: assets/templates/assets/user_asset_list.html:174 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:110 assets/templates/assets/asset_detail.html:68 +#: assets/models/asset.py:102 assets/templates/assets/asset_detail.html:68 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:111 assets/templates/assets/asset_detail.html:124 +#: assets/models/asset.py:103 assets/templates/assets/asset_detail.html:124 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:114 assets/templates/assets/asset_detail.html:88 +#: assets/models/asset.py:106 assets/templates/assets/asset_detail.html:88 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:92 +#: assets/models/asset.py:107 assets/templates/assets/asset_detail.html:92 msgid "Model" msgstr "型号" -#: assets/models/asset.py:116 assets/templates/assets/asset_detail.html:120 +#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:120 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:118 +#: assets/models/asset.py:110 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:119 +#: assets/models/asset.py:111 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:120 +#: assets/models/asset.py:112 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:121 +#: assets/models/asset.py:113 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:122 assets/templates/assets/asset_detail.html:100 +#: assets/models/asset.py:114 assets/templates/assets/asset_detail.html:100 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:123 +#: assets/models/asset.py:115 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:124 +#: assets/models/asset.py:116 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:126 assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:118 assets/templates/assets/asset_detail.html:112 #: assets/templates/assets/user_asset_list.html:171 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:127 +#: assets/models/asset.py:119 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:128 +#: assets/models/asset.py:120 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:129 +#: assets/models/asset.py:121 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:131 assets/templates/assets/asset_create.html:46 +#: assets/models/asset.py:123 assets/templates/assets/asset_create.html:46 #: assets/templates/assets/asset_detail.html:231 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:140 assets/models/base.py:39 -#: assets/serializers/admin_user.py:23 assets/serializers/system_user.py:19 -#: assets/templates/assets/admin_user_list.html:51 -#: assets/templates/assets/system_user_list.html:57 -msgid "Unreachable" -msgstr "不可达" - -#: assets/models/asset.py:141 assets/models/base.py:40 -#: assets/serializers/admin_user.py:25 assets/serializers/system_user.py:27 -#: assets/templates/assets/admin_user_list.html:50 -#: assets/templates/assets/asset_list.html:107 -#: assets/templates/assets/system_user_list.html:56 -#: users/templates/users/user_group_granted_asset.html:47 -msgid "Reachable" -msgstr "可连接" - -#: assets/models/asset.py:142 assets/models/base.py:41 -#: authentication/utils.py:9 xpack/plugins/license/models.py:78 -msgid "Unknown" -msgstr "未知" - -#: assets/models/authbook.py:28 ops/templates/ops/task_detail.html:72 +#: assets/models/authbook.py:25 ops/templates/ops/task_detail.html:72 msgid "Latest version" msgstr "最新版本" -#: assets/models/authbook.py:29 +#: assets/models/authbook.py:26 #: assets/templates/assets/_asset_user_list.html:17 #: ops/templates/ops/adhoc_history.html:58 #: ops/templates/ops/adhoc_history_detail.html:57 @@ -955,22 +954,21 @@ msgstr "最新版本" msgid "Version" msgstr "版本" -#: assets/models/authbook.py:37 +#: assets/models/authbook.py:35 msgid "AuthBook" msgstr "" -#: assets/models/base.py:30 xpack/plugins/change_auth_plan/models.py:97 +#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:97 #: xpack/plugins/change_auth_plan/models.py:271 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:100 +#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:100 #: xpack/plugins/change_auth_plan/models.py:267 msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/base.py:34 assets/serializers/admin_user.py:39 -#: assets/templates/assets/cmd_filter_detail.html:73 +#: assets/models/base.py:35 assets/templates/assets/cmd_filter_detail.html:73 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 msgid "Date updated" msgstr "更新日期" @@ -983,7 +981,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:82 +#: assets/models/cluster.py:22 users/models/user.py:83 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -1005,12 +1003,12 @@ msgid "Operator" msgstr "运营商" #: assets/models/cluster.py:36 assets/models/group.py:34 -#: perms/utils/asset_permission.py:64 +#: perms/utils/asset_permission.py:106 msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:485 +#: users/models/user.py:443 msgid "System" msgstr "系统" @@ -1040,10 +1038,10 @@ msgstr "正则表达式" #: assets/models/cmd_filter.py:39 ops/models/command.py:21 #: ops/templates/ops/command_execution_list.html:61 terminal/models.py:161 -#: terminal/templates/terminal/command_list.html:55 -#: terminal/templates/terminal/command_list.html:71 +#: terminal/templates/terminal/command_list.html:28 +#: terminal/templates/terminal/command_list.html:68 #: terminal/templates/terminal/session_detail.html:48 -#: terminal/templates/terminal/session_list.html:77 +#: terminal/templates/terminal/session_list.html:33 msgid "Command" msgstr "命令" @@ -1070,7 +1068,7 @@ msgstr "过滤器" msgid "Type" msgstr "类型" -#: assets/models/cmd_filter.py:51 assets/models/user.py:136 +#: assets/models/cmd_filter.py:51 assets/models/user.py:109 #: assets/templates/assets/cmd_filter_rule_list.html:60 msgid "Priority" msgstr "优先级" @@ -1117,28 +1115,28 @@ msgstr "默认资产组" #: audits/templates/audits/password_change_log_list.html:50 #: ops/templates/ops/command_execution_list.html:35 #: ops/templates/ops/command_execution_list.html:60 -#: perms/forms/asset_permission.py:40 perms/forms/remote_app_permission.py:31 +#: perms/forms/asset_permission.py:62 perms/forms/remote_app_permission.py:31 #: perms/models/base.py:36 #: perms/templates/perms/asset_permission_create_update.html:41 -#: perms/templates/perms/asset_permission_list.html:54 -#: perms/templates/perms/asset_permission_list.html:119 +#: perms/templates/perms/asset_permission_list.html:46 +#: perms/templates/perms/asset_permission_list.html:111 #: perms/templates/perms/remote_app_permission_create_update.html:43 #: perms/templates/perms/remote_app_permission_list.html:15 #: templates/index.html:87 terminal/backends/command/models.py:12 -#: terminal/models.py:154 terminal/templates/terminal/command_list.html:32 -#: terminal/templates/terminal/command_list.html:72 -#: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:301 -#: users/models/user.py:37 users/models/user.py:473 users/serializers/v1.py:62 +#: terminal/models.py:154 terminal/templates/terminal/command_list.html:29 +#: terminal/templates/terminal/command_list.html:65 +#: terminal/templates/terminal/session_list.html:27 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:316 +#: users/models/user.py:38 users/models/user.py:431 users/serializers/v1.py:105 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:36 users/views/user.py:407 +#: users/templates/users/user_group_list.html:36 users/views/user.py:251 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:21 +#: assets/models/label.py:19 assets/models/node.py:241 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1147,31 +1145,31 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:20 +#: assets/models/node.py:240 msgid "Key" msgstr "键" -#: assets/models/node.py:139 +#: assets/models/node.py:297 msgid "New node" msgstr "新节点" -#: assets/models/user.py:130 +#: assets/models/user.py:103 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:131 +#: assets/models/user.py:104 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:135 +#: assets/models/user.py:108 #: assets/templates/assets/_asset_group_bulk_update_modal.html:11 -#: assets/templates/assets/system_user_asset.html:22 +#: assets/templates/assets/system_user_assets.html:22 #: assets/templates/assets/system_user_detail.html:22 #: assets/views/admin_user.py:30 assets/views/admin_user.py:49 #: assets/views/admin_user.py:66 assets/views/admin_user.py:82 #: assets/views/admin_user.py:107 assets/views/asset.py:52 #: assets/views/asset.py:69 assets/views/asset.py:128 assets/views/asset.py:171 -#: assets/views/asset.py:199 assets/views/asset.py:226 +#: assets/views/asset.py:199 assets/views/asset.py:231 #: assets/views/cmd_filter.py:31 assets/views/cmd_filter.py:48 #: assets/views/cmd_filter.py:65 assets/views/cmd_filter.py:82 #: assets/views/cmd_filter.py:102 assets/views/cmd_filter.py:136 @@ -1181,50 +1179,50 @@ msgstr "手动登录" #: assets/views/domain.py:133 assets/views/domain.py:153 #: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:72 #: assets/views/system_user.py:29 assets/views/system_user.py:46 -#: assets/views/system_user.py:63 assets/views/system_user.py:78 +#: assets/views/system_user.py:63 assets/views/system_user.py:79 #: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:68 msgid "Assets" msgstr "资产管理" -#: assets/models/user.py:138 assets/templates/assets/_system_user.html:59 +#: assets/models/user.py:111 assets/templates/assets/_system_user.html:59 #: assets/templates/assets/system_user_detail.html:122 #: assets/templates/assets/system_user_update.html:10 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:139 assets/templates/assets/system_user_detail.html:74 +#: assets/models/user.py:112 assets/templates/assets/system_user_detail.html:74 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:140 assets/templates/assets/system_user_detail.html:79 +#: assets/models/user.py:113 assets/templates/assets/system_user_detail.html:79 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:141 assets/templates/assets/system_user_detail.html:66 +#: assets/models/user.py:114 assets/templates/assets/system_user_detail.html:66 #: assets/templates/assets/system_user_list.html:54 msgid "Login mode" msgstr "登录模式" -#: assets/models/utils.py:29 +#: assets/models/utils.py:35 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/serializers/asset.py:44 assets/serializers/asset.py:154 -#: assets/templates/assets/asset_create.html:24 -msgid "Protocols" -msgstr "协议组" - -#: assets/serializers/asset.py:71 -msgid "Hardware info" -msgstr "硬件信息" - -#: assets/serializers/asset.py:72 assets/serializers/asset_user.py:29 +#: assets/serializers/admin_user.py:36 assets/serializers/asset.py:46 +#: assets/serializers/asset_user.py:30 assets/serializers/system_user.py:30 #: assets/templates/assets/_asset_user_list.html:18 msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset.py:73 orgs/mixins.py:223 +#: assets/serializers/asset.py:44 assets/templates/assets/asset_create.html:24 +msgid "Protocols" +msgstr "协议组" + +#: assets/serializers/asset.py:72 +msgid "Hardware info" +msgstr "硬件信息" + +#: assets/serializers/asset.py:73 orgs/mixins/serializers.py:26 msgid "Org name" msgstr "组织名称" @@ -1232,9 +1230,16 @@ msgstr "组织名称" msgid "Protocol duplicate: {}" msgstr "协议重复: {}" -#: assets/serializers/asset_user.py:37 assets/serializers/asset_user.py:90 -#: users/forms.py:248 users/models/user.py:93 -#: users/templates/users/first_login.html:42 +#: assets/serializers/asset_user.py:32 +msgid "Backend" +msgstr "后端" + +#: assets/serializers/asset_user.py:57 +msgid "private key invalid" +msgstr "密钥不合法" + +#: assets/serializers/asset_user.py:74 users/forms.py:263 +#: users/models/user.py:94 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 #: users/templates/users/user_profile_update.html:43 @@ -1242,106 +1247,94 @@ msgstr "协议重复: {}" msgid "Public key" msgstr "ssh公钥" -#: assets/serializers/asset_user.py:43 -msgid "Backend" -msgstr "后端" +#: assets/serializers/node.py:33 +msgid "The same level node name cannot be the same" +msgstr "同级别节点名字不能重复" -#: assets/serializers/asset_user.py:65 -msgid "private key invalid" -msgstr "密钥不合法" - -#: assets/serializers/system_user.py:22 -msgid "Unreachable assets" -msgstr "不可达资产" - -#: assets/serializers/system_user.py:25 -msgid "Reachable assets" -msgstr "可连接资产" - -#: assets/serializers/system_user.py:41 +#: assets/serializers/system_user.py:31 msgid "Login mode display" msgstr "登录模式显示" -#: assets/tasks.py:32 +#: assets/tasks.py:33 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:36 +#: assets/tasks.py:37 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:49 +#: assets/tasks.py:50 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:59 +#: assets/tasks.py:60 msgid "No assets matched related system user protocol, stop task" msgstr "没有匹配到与系统用户协议相关的资产,结束任务" -#: assets/tasks.py:85 +#: assets/tasks.py:86 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:135 +#: assets/tasks.py:136 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:152 +#: assets/tasks.py:153 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:177 +#: assets/tasks.py:178 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:233 +#: assets/tasks.py:232 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:275 +#: assets/tasks.py:274 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:282 +#: assets/tasks.py:281 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:355 +#: assets/tasks.py:349 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:362 +#: assets/tasks.py:356 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:375 +#: assets/tasks.py:369 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:476 assets/tasks.py:562 +#: assets/tasks.py:470 assets/tasks.py:556 #: xpack/plugins/change_auth_plan/models.py:522 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks.py:488 +#: assets/tasks.py:482 msgid "" "Push system user task skip, auto push not enable or protocol is not ssh or " "rdp: {}" msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" -#: assets/tasks.py:495 +#: assets/tasks.py:489 msgid "For security, do not push user {}" msgstr "为了安全,禁止推送用户 {}" -#: assets/tasks.py:523 assets/tasks.py:537 +#: assets/tasks.py:517 assets/tasks.py:531 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:529 +#: assets/tasks.py:523 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" -#: assets/tasks.py:619 +#: assets/tasks.py:602 msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" @@ -1400,7 +1393,7 @@ msgid "Update asset user auth" msgstr "更新资产用户认证信息" #: assets/templates/assets/_asset_user_auth_update_modal.html:23 -#: xpack/plugins/change_auth_plan/forms.py:101 +#: xpack/plugins/change_auth_plan/forms.py:102 msgid "Please input password" msgstr "请输入密码" @@ -1436,26 +1429,31 @@ msgstr "关闭" #: audits/templates/audits/operate_log_list.html:71 #: audits/templates/audits/password_change_log_list.html:53 #: ops/templates/ops/task_adhoc.html:63 -#: terminal/templates/terminal/command_list.html:76 +#: terminal/templates/terminal/command_list.html:33 #: terminal/templates/terminal/session_detail.html:50 msgid "Datetime" msgstr "日期" -#: assets/templates/assets/_asset_user_list.html:61 +#: assets/templates/assets/_asset_user_list.html:36 +#: assets/templates/assets/asset_list.html:138 +msgid "Test datetime: " +msgstr "测试日期: " + +#: assets/templates/assets/_asset_user_list.html:69 msgid "View" msgstr "查看" -#: assets/templates/assets/_asset_user_list.html:63 +#: assets/templates/assets/_asset_user_list.html:71 #: assets/templates/assets/admin_user_assets.html:61 #: assets/templates/assets/asset_asset_user_list.html:57 #: assets/templates/assets/asset_detail.html:182 -#: assets/templates/assets/system_user_asset.html:63 +#: assets/templates/assets/system_user_assets.html:63 #: assets/templates/assets/system_user_detail.html:151 msgid "Test" msgstr "测试" -#: assets/templates/assets/_asset_user_list.html:64 -#: assets/templates/assets/system_user_asset.html:72 +#: assets/templates/assets/_asset_user_list.html:72 +#: assets/templates/assets/system_user_assets.html:72 #: assets/templates/assets/system_user_detail.html:142 msgid "Push" msgstr "推送" @@ -1472,6 +1470,34 @@ msgstr "SSH端口" msgid "If use nat, set the ssh real port" msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" +#: assets/templates/assets/_node_tree.html:49 +msgid "Add node" +msgstr "新建节点" + +#: assets/templates/assets/_node_tree.html:50 +msgid "Rename node" +msgstr "重命名节点" + +#: assets/templates/assets/_node_tree.html:51 +msgid "Delete node" +msgstr "删除节点" + +#: assets/templates/assets/_node_tree.html:155 +msgid "Create node failed" +msgstr "创建节点失败" + +#: assets/templates/assets/_node_tree.html:167 +msgid "Have child node, cancel" +msgstr "存在子节点,不能删除" + +#: assets/templates/assets/_node_tree.html:169 +msgid "Have assets, cancel" +msgstr "存在资产,不能删除" + +#: assets/templates/assets/_node_tree.html:243 +msgid "Rename success" +msgstr "重命名成功" + #: assets/templates/assets/_system_user.html:37 #: assets/templates/assets/asset_create.html:16 #: assets/templates/assets/gateway_create_update.html:37 @@ -1513,7 +1539,7 @@ msgstr "更新系统用户" #: assets/templates/assets/_user_asset_detail_modal.html:11 #: assets/templates/assets/asset_asset_user_list.html:13 -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:227 +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:232 msgid "Asset detail" msgstr "资产详情" @@ -1529,7 +1555,7 @@ msgid "Asset list of " msgstr "资产列表" #: assets/templates/assets/admin_user_assets.html:52 -#: assets/templates/assets/system_user_asset.html:54 +#: assets/templates/assets/system_user_assets.html:54 #: assets/templates/assets/system_user_detail.html:116 #: perms/templates/perms/asset_permission_detail.html:114 #: perms/templates/perms/remote_app_permission_detail.html:106 @@ -1548,18 +1574,18 @@ msgstr "替换资产的管理员" #: assets/templates/assets/admin_user_detail.html:91 #: perms/templates/perms/asset_permission_asset.html:116 -#: xpack/plugins/change_auth_plan/forms.py:109 +#: xpack/plugins/change_auth_plan/forms.py:110 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:112 msgid "Select nodes" msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/asset_detail.html:211 -#: assets/templates/assets/asset_list.html:682 +#: assets/templates/assets/asset_list.html:396 #: assets/templates/assets/cmd_filter_detail.html:106 -#: assets/templates/assets/system_user_asset.html:100 +#: assets/templates/assets/system_user_assets.html:100 #: assets/templates/assets/system_user_detail.html:182 -#: assets/templates/assets/system_user_list.html:170 +#: assets/templates/assets/system_user_list.html:172 #: authentication/templates/authentication/_mfa_confirm_modal.html:20 #: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:108 @@ -1570,7 +1596,6 @@ msgstr "选择节点" #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:119 #: users/templates/users/user_list.html:257 -#: users/templates/users/user_profile.html:238 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36 #: xpack/plugins/interface/templates/interface/interface.html:103 @@ -1596,7 +1621,7 @@ msgid "You can set any one for Windows or other hardware." msgstr "Windows或其它硬件可以随意设置一个" #: assets/templates/assets/admin_user_list.html:19 -#: assets/templates/assets/asset_list.html:76 +#: assets/templates/assets/asset_list.html:68 #: assets/templates/assets/system_user_list.html:23 #: audits/templates/audits/login_log_list.html:85 #: users/templates/users/user_group_list.html:10 @@ -1606,7 +1631,7 @@ msgid "Export" msgstr "导出" #: assets/templates/assets/admin_user_list.html:24 -#: assets/templates/assets/asset_list.html:81 +#: assets/templates/assets/asset_list.html:73 #: assets/templates/assets/system_user_list.html:28 #: settings/templates/settings/_ldap_list_users_modal.html:100 #: users/templates/users/user_group_list.html:15 @@ -1628,12 +1653,12 @@ msgstr "创建管理用户" msgid "Ratio" msgstr "比例" -#: assets/templates/assets/admin_user_list.html:160 -#: assets/templates/assets/admin_user_list.html:191 -#: assets/templates/assets/asset_list.html:492 -#: assets/templates/assets/asset_list.html:529 -#: assets/templates/assets/system_user_list.html:223 -#: assets/templates/assets/system_user_list.html:254 +#: assets/templates/assets/admin_user_list.html:165 +#: assets/templates/assets/admin_user_list.html:196 +#: assets/templates/assets/asset_list.html:268 +#: assets/templates/assets/asset_list.html:305 +#: assets/templates/assets/system_user_list.html:225 +#: assets/templates/assets/system_user_list.html:256 #: users/templates/users/user_group_list.html:163 #: users/templates/users/user_group_list.html:194 #: users/templates/users/user_list.html:158 @@ -1687,7 +1712,7 @@ msgstr "创建日期" #: assets/templates/assets/asset_detail.html:154 #: assets/templates/assets/user_asset_list.html:46 -#: perms/models/asset_permission.py:60 perms/models/base.py:38 +#: perms/models/asset_permission.py:96 perms/models/base.py:38 #: perms/templates/perms/asset_permission_create_update.html:55 #: perms/templates/perms/asset_permission_detail.html:120 #: perms/templates/perms/remote_app_permission_create_update.html:54 @@ -1718,100 +1743,64 @@ msgstr "" "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的," "右侧是属于该节点下的资产" -#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:129 +#: assets/templates/assets/asset_list.html:61 assets/views/asset.py:129 msgid "Create asset" msgstr "创建资产" -#: assets/templates/assets/asset_list.html:106 +#: assets/templates/assets/asset_list.html:98 msgid "Hardware" msgstr "硬件" -#: assets/templates/assets/asset_list.html:117 +#: assets/templates/assets/asset_list.html:109 #: users/templates/users/user_list.html:50 msgid "Delete selected" msgstr "批量删除" -#: assets/templates/assets/asset_list.html:118 +#: assets/templates/assets/asset_list.html:110 #: users/templates/users/user_list.html:51 msgid "Update selected" msgstr "批量更新" -#: assets/templates/assets/asset_list.html:119 +#: assets/templates/assets/asset_list.html:111 msgid "Remove from this node" msgstr "从节点移除" -#: assets/templates/assets/asset_list.html:120 +#: assets/templates/assets/asset_list.html:112 #: users/templates/users/user_list.html:52 msgid "Deactive selected" msgstr "禁用所选" -#: assets/templates/assets/asset_list.html:121 +#: assets/templates/assets/asset_list.html:113 #: users/templates/users/user_list.html:53 msgid "Active selected" msgstr "激活所选" -#: assets/templates/assets/asset_list.html:138 -msgid "Add node" -msgstr "新建节点" - -#: assets/templates/assets/asset_list.html:139 -msgid "Rename node" -msgstr "重命名节点" - -#: assets/templates/assets/asset_list.html:140 -msgid "Delete node" -msgstr "删除节点" - -#: assets/templates/assets/asset_list.html:142 +#: assets/templates/assets/asset_list.html:191 msgid "Add assets to node" msgstr "添加资产到节点" -#: assets/templates/assets/asset_list.html:143 +#: assets/templates/assets/asset_list.html:192 msgid "Move assets to node" msgstr "移动资产到节点" -#: assets/templates/assets/asset_list.html:145 +#: assets/templates/assets/asset_list.html:194 msgid "Refresh node hardware info" msgstr "更新节点资产硬件信息" -#: assets/templates/assets/asset_list.html:146 +#: assets/templates/assets/asset_list.html:195 msgid "Test node connective" msgstr "测试节点资产可连接性" -#: assets/templates/assets/asset_list.html:148 -msgid "Refresh all node assets amount" -msgstr "刷新所有节点资产数量" - -#: assets/templates/assets/asset_list.html:150 +#: assets/templates/assets/asset_list.html:197 msgid "Display only current node assets" msgstr "仅显示当前节点资产" -#: assets/templates/assets/asset_list.html:151 +#: assets/templates/assets/asset_list.html:198 msgid "Displays all child node assets" msgstr "显示所有子节点资产" -#: assets/templates/assets/asset_list.html:229 -msgid "Create node failed" -msgstr "创建节点失败" - -#: assets/templates/assets/asset_list.html:241 -msgid "Have child node, cancel" -msgstr "存在子节点,不能删除" - -#: assets/templates/assets/asset_list.html:243 -msgid "Have assets, cancel" -msgstr "存在资产,不能删除" - -#: assets/templates/assets/asset_list.html:314 -msgid "Rename success" -msgstr "重命名成功" - -#: assets/templates/assets/asset_list.html:315 -msgid "Rename failed, do not change the root node name" -msgstr "重命名失败,不能更改root节点的名称" - -#: assets/templates/assets/asset_list.html:676 -#: assets/templates/assets/system_user_list.html:164 +#: assets/templates/assets/asset_list.html:390 +#: assets/templates/assets/system_user_list.html:166 #: users/templates/users/user_detail.html:382 #: users/templates/users/user_detail.html:408 #: users/templates/users/user_detail.html:476 @@ -1821,12 +1810,12 @@ msgstr "重命名失败,不能更改root节点的名称" msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:677 +#: assets/templates/assets/asset_list.html:391 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:680 -#: assets/templates/assets/system_user_list.html:168 +#: assets/templates/assets/asset_list.html:394 +#: assets/templates/assets/system_user_list.html:170 #: settings/templates/settings/terminal_setting.html:166 #: users/templates/users/user_detail.html:386 #: users/templates/users/user_detail.html:412 @@ -1839,16 +1828,16 @@ msgstr "删除选择资产" msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:693 +#: assets/templates/assets/asset_list.html:407 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:694 -#: assets/templates/assets/asset_list.html:698 +#: assets/templates/assets/asset_list.html:408 +#: assets/templates/assets/asset_list.html:412 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:697 +#: assets/templates/assets/asset_list.html:411 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1965,21 +1954,21 @@ msgstr "创建网域" msgid "Create label" msgstr "创建标签" -#: assets/templates/assets/system_user_asset.html:31 +#: assets/templates/assets/system_user_assets.html:31 msgid "Assets of " msgstr "资产" -#: assets/templates/assets/system_user_asset.html:60 +#: assets/templates/assets/system_user_assets.html:60 #: assets/templates/assets/system_user_detail.html:148 msgid "Test assets connective" msgstr "测试资产可连接性" -#: assets/templates/assets/system_user_asset.html:69 +#: assets/templates/assets/system_user_assets.html:69 #: assets/templates/assets/system_user_detail.html:139 msgid "Push system user now" msgstr "立刻推送系统" -#: assets/templates/assets/system_user_asset.html:91 +#: assets/templates/assets/system_user_assets.html:91 msgid "Add to node" msgstr "添加到节点" @@ -2028,20 +2017,20 @@ msgstr "" msgid "Create system user" msgstr "创建系统用户" -#: assets/templates/assets/system_user_list.html:165 +#: assets/templates/assets/system_user_list.html:167 msgid "This will delete the selected System Users !!!" msgstr "删除选择系统用户" -#: assets/templates/assets/system_user_list.html:174 +#: assets/templates/assets/system_user_list.html:176 msgid "System Users Deleted." msgstr "已被删除" -#: assets/templates/assets/system_user_list.html:175 -#: assets/templates/assets/system_user_list.html:180 +#: assets/templates/assets/system_user_list.html:177 +#: assets/templates/assets/system_user_list.html:182 msgid "System Users Delete" msgstr "删除系统用户" -#: assets/templates/assets/system_user_list.html:179 +#: assets/templates/assets/system_user_list.html:181 msgid "System Users Deleting failed." msgstr "系统用户删除失败" @@ -2069,10 +2058,6 @@ msgstr "批量更新资产" msgid "Update asset" msgstr "更新资产" -#: assets/views/asset.py:347 -msgid "already exists" -msgstr "已经存在" - #: assets/views/cmd_filter.py:32 msgid "Command filter list" msgstr "命令过滤器列表" @@ -2129,15 +2114,15 @@ msgstr "更新标签" msgid "System user list" msgstr "系统用户列表" -#: assets/views/system_user.py:79 +#: assets/views/system_user.py:80 msgid "System user detail" msgstr "系统用户详情" -#: assets/views/system_user.py:102 +#: assets/views/system_user.py:106 msgid "assets" msgstr "资产管理" -#: assets/views/system_user.py:103 +#: assets/views/system_user.py:107 msgid "System user asset" msgstr "系统用户资产" @@ -2145,7 +2130,8 @@ msgstr "系统用户资产" #: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/operate_log_list.html:70 #: audits/templates/audits/password_change_log_list.html:52 -#: terminal/models.py:158 terminal/templates/terminal/session_list.html:74 +#: terminal/models.py:158 terminal/templates/terminal/session_list.html:30 +#: terminal/templates/terminal/session_list.html:74 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" @@ -2238,7 +2224,7 @@ msgstr "Agent" #: audits/models.py:99 audits/templates/audits/login_log_list.html:56 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:160 users/models/user.py:85 +#: users/forms.py:175 users/models/user.py:86 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" @@ -2269,7 +2255,7 @@ msgstr "登录日期" #: ops/templates/ops/task_history.html:58 perms/models/base.py:39 #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 -#: terminal/models.py:165 terminal/templates/terminal/session_list.html:78 +#: terminal/models.py:165 terminal/templates/terminal/session_list.html:34 #: xpack/plugins/change_auth_plan/models.py:250 #: xpack/plugins/change_auth_plan/models.py:420 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 @@ -2291,8 +2277,6 @@ msgstr "选择用户" #: ops/templates/ops/command_execution_list.html:48 #: ops/templates/ops/task_list.html:13 ops/templates/ops/task_list.html:18 #: templates/_base_list.html:41 templates/_header_bar.html:8 -#: terminal/templates/terminal/command_list.html:60 -#: terminal/templates/terminal/session_list.html:61 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:52 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48 msgid "Search" @@ -2302,7 +2286,7 @@ msgstr "搜索" #: ops/templates/ops/adhoc_detail.html:49 #: ops/templates/ops/adhoc_history_detail.html:49 #: ops/templates/ops/task_detail.html:56 -#: terminal/templates/terminal/session_list.html:70 +#: terminal/templates/terminal/session_list.html:26 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:64 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:60 msgid "ID" @@ -2430,7 +2414,7 @@ msgstr "请输入正确的用户名和密码. 注意它们是区分大小写." msgid "This account is inactive." msgstr "此账户无效" -#: authentication/forms.py:37 users/forms.py:21 +#: authentication/forms.py:37 users/forms.py:22 msgid "MFA code" msgstr "MFA 验证码" @@ -2570,8 +2554,8 @@ msgstr "欢迎回来,请输入用户名和密码登录" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:172 users/views/user.py:555 -#: users/views/user.py:580 +#: authentication/views/login.py:172 users/views/user.py:399 +#: users/views/user.py:424 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -2629,27 +2613,27 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins.py:36 -msgid "is discard" -msgstr "" - -#: common/mixins.py:37 -msgid "discard time" -msgstr "" - -#: common/mixins.py:210 +#: common/mixins/api.py:62 #, python-format msgid "%(name)s was %(action)s successfully" msgstr "%(name)s %(action)s成功" -#: common/mixins.py:211 +#: common/mixins/api.py:63 msgid "create" msgstr "创建" -#: common/mixins.py:211 +#: common/mixins/api.py:63 msgid "update" msgstr "更新" +#: common/mixins/models.py:31 +msgid "is discard" +msgstr "" + +#: common/mixins/models.py:32 +msgid "discard time" +msgstr "" + #: common/validators.py:11 msgid "Special char not allowed" msgstr "不能包含特殊字符" @@ -2658,7 +2642,7 @@ msgstr "不能包含特殊字符" msgid "This field must be unique." msgstr "字段必须唯一" -#: jumpserver/views.py:188 +#: jumpserver/views.py:190 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " "configure nginx for url distribution,
If you see this page, " @@ -3009,59 +2993,67 @@ msgstr "命令执行列表" msgid "Command execution" msgstr "命令执行" -#: orgs/mixins.py:85 orgs/mixins.py:222 orgs/models.py:24 +#: orgs/mixins/models.py:61 orgs/mixins/serializers.py:25 orgs/models.py:27 msgid "Organization" msgstr "组织" -#: perms/const.py:18 settings/forms.py:143 -msgid "All" -msgstr "全部" - -#: perms/const.py:20 -msgid "Upload file" -msgstr "上传文件" - -#: perms/const.py:21 -msgid "Download file" -msgstr "下载文件" - -#: perms/forms/asset_permission.py:43 perms/forms/remote_app_permission.py:34 -#: perms/models/asset_permission.py:58 perms/models/base.py:37 -#: perms/templates/perms/asset_permission_list.html:55 -#: perms/templates/perms/asset_permission_list.html:75 -#: perms/templates/perms/asset_permission_list.html:122 +#: perms/forms/asset_permission.py:65 perms/forms/remote_app_permission.py:34 +#: perms/models/asset_permission.py:94 perms/models/base.py:37 +#: perms/templates/perms/asset_permission_list.html:47 +#: perms/templates/perms/asset_permission_list.html:67 +#: perms/templates/perms/asset_permission_list.html:114 #: perms/templates/perms/remote_app_permission_list.html:16 -#: templates/_nav.html:14 users/forms.py:271 users/models/group.py:26 -#: users/models/user.py:69 users/templates/users/_select_user_modal.html:16 +#: templates/_nav.html:14 users/forms.py:286 users/models/group.py:26 +#: users/models/user.py:70 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User group" msgstr "用户组" -#: perms/forms/asset_permission.py:62 +#: perms/forms/asset_permission.py:82 msgid "" "Tips: The RDP protocol does not support separate controls for uploading or " "downloading files" msgstr "提示:RDP 协议不支持单独控制上传或下载文件" -#: perms/forms/asset_permission.py:72 perms/forms/remote_app_permission.py:47 +#: perms/forms/asset_permission.py:92 perms/forms/remote_app_permission.py:47 msgid "User or group at least one required" msgstr "用户和用户组至少选一个" -#: perms/forms/asset_permission.py:81 +#: perms/forms/asset_permission.py:101 msgid "Asset or group at least one required" msgstr "资产和节点至少选一个" -#: perms/models/asset_permission.py:44 perms/models/asset_permission.py:70 +#: perms/models/asset_permission.py:26 settings/forms.py:143 +msgid "All" +msgstr "全部" + +#: perms/models/asset_permission.py:28 +msgid "Upload file" +msgstr "上传文件" + +#: perms/models/asset_permission.py:29 +msgid "Download file" +msgstr "下载文件" + +#: perms/models/asset_permission.py:30 +msgid "Upload download" +msgstr "上传下载" + +#: perms/models/asset_permission.py:72 +msgid "Actions" +msgstr "动作" + +#: perms/models/asset_permission.py:76 perms/models/asset_permission.py:106 #: templates/_nav.html:44 msgid "Asset permission" msgstr "资产授权" -#: perms/models/asset_permission.py:61 perms/models/base.py:40 +#: perms/models/asset_permission.py:97 perms/models/base.py:40 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:101 users/templates/users/user_detail.html:107 +#: users/models/user.py:102 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:116 msgid "Date expired" msgstr "失效日期" @@ -3151,13 +3143,13 @@ msgstr "系统用户数量" msgid "Select system users" msgstr "选择系统用户" -#: perms/templates/perms/asset_permission_list.html:46 +#: perms/templates/perms/asset_permission_list.html:38 #: perms/templates/perms/remote_app_permission_list.html:6 msgid "Create permission" msgstr "创建授权规则" -#: perms/templates/perms/asset_permission_list.html:59 -#: perms/templates/perms/asset_permission_list.html:73 +#: perms/templates/perms/asset_permission_list.html:51 +#: perms/templates/perms/asset_permission_list.html:65 #: perms/templates/perms/remote_app_permission_list.html:18 #: users/templates/users/user_list.html:40 xpack/plugins/cloud/models.py:53 #: xpack/plugins/cloud/templates/cloud/account_detail.html:58 @@ -3207,9 +3199,13 @@ msgstr "添加用户" msgid "Add user group to this permission" msgstr "添加用户组" -#: perms/views/asset_permission.py:34 perms/views/asset_permission.py:67 -#: perms/views/asset_permission.py:83 perms/views/asset_permission.py:99 -#: perms/views/asset_permission.py:136 perms/views/asset_permission.py:169 +#: perms/utils/asset_permission.py:115 +msgid "Empty" +msgstr "空" + +#: perms/views/asset_permission.py:33 perms/views/asset_permission.py:64 +#: perms/views/asset_permission.py:81 perms/views/asset_permission.py:98 +#: perms/views/asset_permission.py:135 perms/views/asset_permission.py:168 #: perms/views/remote_app_permission.py:33 #: perms/views/remote_app_permission.py:49 #: perms/views/remote_app_permission.py:65 @@ -3220,27 +3216,27 @@ msgstr "添加用户组" msgid "Perms" msgstr "权限管理" -#: perms/views/asset_permission.py:35 +#: perms/views/asset_permission.py:34 msgid "Asset permission list" msgstr "资产授权列表" -#: perms/views/asset_permission.py:68 +#: perms/views/asset_permission.py:65 msgid "Create asset permission" msgstr "创建权限规则" -#: perms/views/asset_permission.py:84 +#: perms/views/asset_permission.py:82 msgid "Update asset permission" msgstr "更新资产授权" -#: perms/views/asset_permission.py:100 +#: perms/views/asset_permission.py:99 msgid "Asset permission detail" msgstr "资产授权详情" -#: perms/views/asset_permission.py:137 +#: perms/views/asset_permission.py:136 msgid "Asset permission user list" msgstr "资产授权用户列表" -#: perms/views/asset_permission.py:170 +#: perms/views/asset_permission.py:169 msgid "Asset permission asset list" msgstr "资产授权资产列表" @@ -3597,7 +3593,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:39 -#: users/models/user.py:65 users/templates/users/user_detail.html:71 +#: users/models/user.py:66 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3830,14 +3826,14 @@ msgstr "文档" msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:89 templates/_nav_user.html:32 users/forms.py:139 +#: templates/_header_bar.html:89 templates/_nav_user.html:32 users/forms.py:154 #: users/templates/users/_user.html:43 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:388 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:232 msgid "Profile" msgstr "个人信息" @@ -3932,13 +3928,13 @@ msgstr "" #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:45 #: users/views/group.py:62 users/views/group.py:79 users/views/group.py:96 -#: users/views/login.py:154 users/views/user.py:70 users/views/user.py:87 -#: users/views/user.py:131 users/views/user.py:211 users/views/user.py:374 -#: users/views/user.py:426 users/views/user.py:467 +#: users/views/login.py:154 users/views/user.py:68 users/views/user.py:85 +#: users/views/user.py:129 users/views/user.py:196 users/views/user.py:218 +#: users/views/user.py:270 users/views/user.py:311 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:13 users/views/user.py:71 +#: templates/_nav.html:13 users/views/user.py:69 msgid "User list" msgstr "用户列表" @@ -3947,8 +3943,8 @@ msgid "Command filters" msgstr "命令过滤" #: templates/_nav.html:55 templates/_nav_audits.html:11 -#: terminal/views/command.py:51 terminal/views/session.py:74 -#: terminal/views/session.py:92 terminal/views/session.py:116 +#: terminal/views/command.py:21 terminal/views/session.py:43 +#: terminal/views/session.py:54 terminal/views/session.py:78 #: terminal/views/terminal.py:31 terminal/views/terminal.py:47 #: terminal/views/terminal.py:60 msgid "Sessions" @@ -3959,7 +3955,7 @@ msgid "Session online" msgstr "在线会话" #: templates/_nav.html:59 templates/_nav_audits.html:15 -#: terminal/views/session.py:93 +#: terminal/views/session.py:55 msgid "Session offline" msgstr "历史会话" @@ -4187,7 +4183,7 @@ msgid "Input" msgstr "输入" #: terminal/backends/command/models.py:17 -#: terminal/templates/terminal/command_list.html:75 +#: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/terminal_list.html:33 msgid "Session" msgstr "会话" @@ -4237,7 +4233,7 @@ msgstr "线程数" msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:160 terminal/templates/terminal/session_list.html:104 +#: terminal/models.py:160 terminal/templates/terminal/session_list.html:136 msgid "Replay" msgstr "回放" @@ -4253,21 +4249,21 @@ msgstr "结束日期" msgid "Args" msgstr "参数" -#: terminal/templates/terminal/command_list.html:88 -msgid "Goto" -msgstr "转到" - -#: terminal/templates/terminal/command_list.html:99 +#: terminal/templates/terminal/command_list.html:43 msgid "Export command" msgstr "导出命令" +#: terminal/templates/terminal/command_list.html:189 +msgid "Goto" +msgstr "转到" + #: terminal/templates/terminal/session_detail.html:17 -#: terminal/views/session.py:117 +#: terminal/views/session.py:79 msgid "Session detail" msgstr "会话详情" #: terminal/templates/terminal/session_detail.html:28 -#: terminal/views/command.py:52 +#: terminal/views/command.py:22 msgid "Command list" msgstr "命令记录列表" @@ -4287,32 +4283,31 @@ msgstr "监控" msgid "Terminate session" msgstr "终止会话" -#: terminal/templates/terminal/session_list.html:76 +#: terminal/templates/terminal/session_list.html:32 msgid "Login from" msgstr "登录来源" -#: terminal/templates/terminal/session_list.html:80 +#: terminal/templates/terminal/session_list.html:35 msgid "Duration" msgstr "时长" -#: terminal/templates/terminal/session_list.html:107 -#: terminal/templates/terminal/session_list.html:109 -msgid "Terminate" -msgstr "终断" - -#: terminal/templates/terminal/session_list.html:122 +#: terminal/templates/terminal/session_list.html:47 msgid "Terminate selected" msgstr "终断所选" -#: terminal/templates/terminal/session_list.html:123 +#: terminal/templates/terminal/session_list.html:48 msgid "Confirm finished" msgstr "确认已完成" -#: terminal/templates/terminal/session_list.html:144 +#: terminal/templates/terminal/session_list.html:91 msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" -#: terminal/templates/terminal/session_list.html:157 +#: terminal/templates/terminal/session_list.html:142 +msgid "Terminate" +msgstr "终断" + +#: terminal/templates/terminal/session_list.html:173 msgid "Finish session success" msgstr "标记会话完成成功" @@ -4353,7 +4348,7 @@ msgstr "接受终端注册" msgid "Info" msgstr "信息" -#: terminal/views/session.py:75 +#: terminal/views/session.py:44 msgid "Session online list" msgstr "在线会话" @@ -4378,15 +4373,15 @@ msgid "" "You should use your ssh client tools connect terminal: {}

{}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/user.py:78 users/api/user.py:89 users/api/user.py:115 +#: users/api/user.py:93 msgid "You do not have permission." msgstr "你没有权限" -#: users/api/user.py:219 +#: users/api/user.py:186 msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:32 users/models/user.py:73 +#: users/forms.py:33 users/models/user.py:74 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4394,39 +4389,43 @@ msgstr "不能再该页面重置MFA, 请去个人信息页面重置" msgid "Role" msgstr "角色" -#: users/forms.py:35 users/forms.py:218 +#: users/forms.py:36 users/forms.py:233 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:36 users/forms.py:219 +#: users/forms.py:37 users/forms.py:234 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:37 +#: users/forms.py:38 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:51 users/templates/users/user_detail.html:221 +#: users/forms.py:52 users/templates/users/user_detail.html:221 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:86 users/forms.py:233 +#: users/forms.py:87 users/forms.py:248 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:90 users/forms.py:237 users/serializers/v1.py:48 +#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:91 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:110 +#: users/forms.py:104 users/views/login.py:114 users/views/user.py:293 +msgid "* Your password does not meet the requirements" +msgstr "* 您的密码不符合要求" + +#: users/forms.py:125 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms.py:111 +#: users/forms.py:126 msgid "Set password" msgstr "设置密码" -#: users/forms.py:118 xpack/plugins/change_auth_plan/models.py:86 +#: users/forms.py:133 xpack/plugins/change_auth_plan/models.py:86 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -4434,7 +4433,7 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/forms.py:145 +#: users/forms.py:160 msgid "" "Tip: when enabled, you will enter the MFA binding process the next time you " "log in. you can also directly bind in \"personal information -> quick " @@ -4443,11 +4442,11 @@ msgstr "" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "改->更改MFA设置)中直接绑定!" -#: users/forms.py:155 +#: users/forms.py:170 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全." -#: users/forms.py:165 +#: users/forms.py:180 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -4456,127 +4455,135 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:172 users/templates/users/first_login.html:48 +#: users/forms.py:187 users/templates/users/first_login.html:48 #: users/templates/users/first_login.html:107 #: users/templates/users/first_login.html:130 msgid "Finish" msgstr "完成" -#: users/forms.py:178 +#: users/forms.py:193 msgid "Old password" msgstr "原来密码" -#: users/forms.py:183 +#: users/forms.py:198 msgid "New password" msgstr "新密码" -#: users/forms.py:188 +#: users/forms.py:203 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:198 +#: users/forms.py:213 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:206 +#: users/forms.py:221 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:216 +#: users/forms.py:231 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:220 +#: users/forms.py:235 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:254 users/forms.py:259 users/forms.py:305 +#: users/forms.py:269 users/forms.py:274 users/forms.py:320 #: xpack/plugins/orgs/forms.py:30 msgid "Select users" msgstr "选择用户" -#: users/models/user.py:36 users/models/user.py:481 +#: users/models/user.py:37 users/models/user.py:439 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:38 +#: users/models/user.py:39 msgid "Application" msgstr "应用程序" -#: users/models/user.py:39 +#: users/models/user.py:40 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:42 users/templates/users/user_profile.html:92 +#: users/models/user.py:43 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:159 #: users/templates/users/user_profile.html:162 msgid "Disable" msgstr "禁用" -#: users/models/user.py:43 users/templates/users/user_profile.html:90 +#: users/models/user.py:44 users/templates/users/user_profile.html:90 #: users/templates/users/user_profile.html:166 msgid "Enable" msgstr "启用" -#: users/models/user.py:44 users/templates/users/user_profile.html:88 +#: users/models/user.py:45 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:76 +#: users/models/user.py:77 msgid "Avatar" msgstr "头像" -#: users/models/user.py:79 users/templates/users/user_detail.html:82 +#: users/models/user.py:80 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:108 users/templates/users/user_detail.html:103 +#: users/models/user.py:109 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:112 +#: users/models/user.py:113 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:138 users/templates/users/user_update.html:22 -#: users/views/login.py:46 users/views/login.py:107 users/views/user.py:439 +#: users/models/user.py:139 users/templates/users/user_update.html:22 +#: users/views/login.py:46 users/views/login.py:107 users/views/user.py:283 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:484 +#: users/models/user.py:442 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:29 +#: users/serializers/v1.py:41 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:30 +#: users/serializers/v1.py:42 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:31 +#: users/serializers/v1.py:43 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:32 +#: users/serializers/v1.py:44 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:33 +#: users/serializers/v1.py:45 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:34 +#: users/serializers/v1.py:46 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:35 +#: users/serializers/v1.py:47 msgid "Avatar url" msgstr "头像路径" +#: users/serializers/v1.py:55 +msgid "Role limit to {}" +msgstr "角色只能为 {}" + +#: users/serializers/v1.py:63 +msgid "Password does not match security rules" +msgstr "密码不满足安全规则" + #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -4627,7 +4634,7 @@ msgid "Import users" msgstr "导入用户" #: users/templates/users/_user_update_modal.html:4 -#: users/templates/users/user_update.html:4 users/views/user.py:132 +#: users/templates/users/user_update.html:4 users/views/user.py:130 msgid "Update user" msgstr "更新用户" @@ -4765,12 +4772,12 @@ msgid "Very strong" msgstr "很强" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:28 users/views/user.py:88 +#: users/templates/users/user_list.html:28 users/views/user.py:86 msgid "Create user" msgstr "创建用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:212 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:197 msgid "User detail" msgstr "用户详情" @@ -4852,14 +4859,11 @@ msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" #: users/templates/users/user_detail.html:427 -#: users/templates/users/user_profile.html:227 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" #: users/templates/users/user_detail.html:428 #: users/templates/users/user_detail.html:432 -#: users/templates/users/user_profile.html:228 -#: users/templates/users/user_profile.html:233 msgid "User SSH public key update" msgstr "ssh密钥" @@ -4973,8 +4977,7 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 msgid "Administrator Settings force MFA login" msgstr "管理员设置强制使用MFA登录" -#: users/templates/users/user_profile.html:120 users/views/user.py:248 -#: users/views/user.py:303 +#: users/templates/users/user_profile.html:120 msgid "User groups" msgstr "用户组" @@ -4998,10 +5001,6 @@ msgstr "更改SSH密钥" msgid "Reset public key and download" msgstr "重置并下载SSH密钥" -#: users/templates/users/user_profile.html:231 -msgid "Failed to update SSH public key." -msgstr "更新密钥失败" - #: users/templates/users/user_pubkey_update.html:51 msgid "Old public key" msgstr "原来ssh密钥" @@ -5253,59 +5252,51 @@ msgstr "Token错误或失效" msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:114 users/views/user.py:146 users/views/user.py:449 -msgid "* Your password does not meet the requirements" -msgstr "* 您的密码不符合要求" - #: users/views/login.py:154 msgid "First login" msgstr "首次登录" -#: users/views/user.py:163 +#: users/views/user.py:148 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:191 +#: users/views/user.py:176 msgid "Bulk update user" msgstr "批量更新用户" -#: users/views/user.py:278 -msgid "Invalid file." -msgstr "文件不合法" - -#: users/views/user.py:375 +#: users/views/user.py:219 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:408 +#: users/views/user.py:252 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:427 +#: users/views/user.py:271 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:468 +#: users/views/user.py:312 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:510 +#: users/views/user.py:354 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:610 +#: users/views/user.py:454 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:611 +#: users/views/user.py:455 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:613 +#: users/views/user.py:457 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:614 +#: users/views/user.py:458 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -5314,25 +5305,26 @@ msgid "Password length" msgstr "密码长度" #: xpack/plugins/change_auth_plan/forms.py:45 -msgid "* For security, please do not change root user's password" -msgstr "* 为了安全,请不要更改root用户的密码" +#: xpack/plugins/change_auth_plan/models.py:213 +msgid "* For security, do not change {} user's password" +msgstr "* 为了安全,禁止更改 {} 用户的密码" -#: xpack/plugins/change_auth_plan/forms.py:54 +#: xpack/plugins/change_auth_plan/forms.py:55 msgid "* Please enter custom password" msgstr "* 请输入自定义密码" -#: xpack/plugins/change_auth_plan/forms.py:63 +#: xpack/plugins/change_auth_plan/forms.py:64 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: xpack/plugins/change_auth_plan/forms.py:116 +#: xpack/plugins/change_auth_plan/forms.py:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:60 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:81 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 msgid "Periodic perform" msgstr "定时执行" -#: xpack/plugins/change_auth_plan/forms.py:120 +#: xpack/plugins/change_auth_plan/forms.py:121 msgid "" "Tips: The username of the user on the asset to be modified. if the user " "exists, change the password; If the user does not exist, create the user." @@ -5340,11 +5332,11 @@ msgstr "" "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果" "用户不存在,则创建用户。" -#: xpack/plugins/change_auth_plan/forms.py:124 +#: xpack/plugins/change_auth_plan/forms.py:125 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" -#: xpack/plugins/change_auth_plan/forms.py:125 +#: xpack/plugins/change_auth_plan/forms.py:126 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions ( -1) { assetsNodeId.push(node.id); assetsNode.push(node) diff --git a/apps/ops/views/command.py b/apps/ops/views/command.py index 6275f0f3f..a825464fc 100644 --- a/apps/ops/views/command.py +++ b/apps/ops/views/command.py @@ -59,6 +59,11 @@ class CommandExecutionStartView(PermissionsMixin, TemplateView): form_class = CommandExecutionForm permission_classes = [IsValidUser] + def get_permissions(self): + if not settings.SECURITY_COMMAND_EXECUTION: + return [IsOrgAdmin] + return super().get_permissions() + def get_user_system_users(self): from perms.utils import AssetPermissionUtil user = self.request.user diff --git a/apps/orgs/hands.py b/apps/orgs/hands.py index f3dbd3211..226db55f6 100644 --- a/apps/orgs/hands.py +++ b/apps/orgs/hands.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # from assets.models import Node -from orgs.utils import set_current_org, current_org +from orgs.utils import set_current_org, current_org, get_current_org diff --git a/apps/orgs/mixins.py b/apps/orgs/mixins.py deleted file mode 100644 index e683eb90e..000000000 --- a/apps/orgs/mixins.py +++ /dev/null @@ -1,247 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from werkzeug.local import Local -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import redirect, get_object_or_404 -from django.forms import ModelForm -from django.http.response import HttpResponseForbidden -from django.core.exceptions import ValidationError -from rest_framework import serializers -from rest_framework.validators import UniqueTogetherValidator - -from common.utils import get_logger -from common.validators import ProjectUniqueValidator -from common.mixins import BulkSerializerMixin -from .utils import ( - current_org, set_current_org, set_to_root_org, get_current_org_id -) -from .models import Organization - -logger = get_logger(__file__) -tl = Local() - -__all__ = [ - 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', - 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', - 'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin', - 'BulkOrgResourceSerializerMixin', 'BulkOrgResourceModelSerializer', -] - - -class OrgManager(models.Manager): - - def get_queryset(self): - queryset = super(OrgManager, self).get_queryset() - kwargs = {} - # if not hasattr(tl, 'times'): - # tl.times = 0 - # logger.debug("[{}]>>>>>>>>>> Get query set".format(tl.times)) - if not current_org: - kwargs['id'] = None - elif current_org.is_real(): - kwargs['org_id'] = current_org.id - elif current_org.is_default(): - queryset = queryset.filter(org_id="") - queryset = queryset.filter(**kwargs) - # tl.times += 1 - return queryset - - def filter_by_fullname(self, fullname, field=None): - ori_org = current_org - value, org = self.model.split_fullname(fullname) - set_current_org(org) - if not field: - if hasattr(self.model, 'name'): - field = 'name' - elif hasattr(self.model, 'hostname'): - field = 'hostname' - queryset = self.get_queryset().filter(**{field: value}) - set_current_org(ori_org) - return queryset - - def get_object_by_fullname(self, fullname, field=None): - queryset = self.filter_by_fullname(fullname, field=field) - if len(queryset) == 1: - return queryset[0] - return None - - def all(self): - if not current_org: - msg = 'You can `objects.set_current_org(org).all()` then run it' - return self - else: - return super(OrgManager, self).all() - - def set_current_org(self, org): - if isinstance(org, str): - org = Organization.objects.get(name=org) - set_current_org(org) - return self - - -class OrgModelMixin(models.Model): - org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True) - objects = OrgManager() - - sep = '@' - - def save(self, *args, **kwargs): - if current_org and current_org.is_real(): - self.org_id = current_org.id - return super().save(*args, **kwargs) - - @classmethod - def split_fullname(cls, fullname, sep=None): - if not sep: - sep = cls.sep - index = fullname.rfind(sep) - if index == -1: - value = fullname - org = Organization.default() - else: - value = fullname[:index] - org = Organization.get_instance(fullname[index + 1:]) - return value, org - - @property - def org(self): - from orgs.models import Organization - org = Organization.get_instance(self.org_id) - return org - - @property - def org_name(self): - return self.org.name - - @property - def fullname(self, attr=None): - name = '' - if attr and hasattr(self, attr): - name = getattr(self, attr) - elif hasattr(self, 'name'): - name = self.name - elif hasattr(self, 'hostname'): - name = self.hostname - if self.org.is_real(): - return name + self.sep + self.org_name - else: - return name - - def validate_unique(self, exclude=None): - """ - Check unique constraints on the model and raise ValidationError if any - failed. - """ - self.org_id = current_org.id if current_org.is_real() else '' - if exclude and 'org_id' in exclude: - exclude.remove('org_id') - unique_checks, date_checks = self._get_unique_checks(exclude=exclude) - - errors = self._perform_unique_checks(unique_checks) - date_errors = self._perform_date_checks(date_checks) - - for k, v in date_errors.items(): - errors.setdefault(k, []).extend(v) - - if errors: - raise ValidationError(errors) - - class Meta: - abstract = True - - -class OrgViewGenericMixin: - def dispatch(self, request, *args, **kwargs): - if not current_org: - return redirect('orgs:switch-a-org') - - if not current_org.can_admin_by(request.user): - print("{} cannot admin {}".format(request.user, current_org)) - if request.user.is_org_admin: - return redirect('orgs:switch-a-org') - return HttpResponseForbidden() - else: - print(current_org.can_admin_by(request.user)) - return super().dispatch(request, *args, **kwargs) - - -class RootOrgViewMixin: - def dispatch(self, request, *args, **kwargs): - set_to_root_org() - return super().dispatch(request, *args, **kwargs) - - -class OrgModelForm(ModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # if 'initial' not in kwargs: - # return - for name, field in self.fields.items(): - if not hasattr(field, 'queryset'): - continue - model = field.queryset.model - field.queryset = model.objects.all() - - -class OrgMembershipSerializerMixin: - def run_validation(self, initial_data=None): - initial_data['organization'] = str(self.context['org'].id) - return super().run_validation(initial_data) - - -class OrgMembershipModelViewSetMixin: - org = None - membership_class = None - lookup_field = 'user' - lookup_url_kwarg = 'user_id' - http_method_names = ['get', 'post', 'delete', 'head', 'options'] - - def dispatch(self, request, *args, **kwargs): - self.org = get_object_or_404(Organization, pk=kwargs.get('org_id')) - return super().dispatch(request, *args, **kwargs) - - def get_serializer_context(self): - context = super().get_serializer_context() - context['org'] = self.org - return context - - def get_queryset(self): - queryset = self.membership_class.objects.filter(organization=self.org) - return queryset - - -class OrgResourceSerializerMixin(serializers.Serializer): - """ - 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id - (同时为serializer.is_valid()对Model的unique_together校验做准备) - 由于HiddenField字段不可读,API获取资产信息时获取不到org_id, - 但是coco需要资产的org_id字段,所以修改为CharField类型 - """ - org_id = serializers.ReadOnlyField(default=get_current_org_id, label=_("Organization")) - org_name = serializers.ReadOnlyField(label=_("Org name")) - - def get_validators(self): - _validators = super().get_validators() - validators = [] - - for v in _validators: - if isinstance(v, UniqueTogetherValidator) \ - and "org_id" in v.fields: - v = ProjectUniqueValidator(v.queryset, v.fields) - validators.append(v) - return validators - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(["org_id", "org_name"]) - return fields - - -class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerMixin): - pass - - -class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): - pass diff --git a/apps/orgs/mixins/__init__.py b/apps/orgs/mixins/__init__.py new file mode 100644 index 000000000..3d1a4a7db --- /dev/null +++ b/apps/orgs/mixins/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# + +from .models import * +from .serializers import * +from .forms import * +from .api import * diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py new file mode 100644 index 000000000..5bdaf2342 --- /dev/null +++ b/apps/orgs/mixins/api.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +from django.shortcuts import get_object_or_404 +from rest_framework.viewsets import ModelViewSet +from rest_framework_bulk import BulkModelViewSet +from common.mixins import IDInCacheFilterMixin + +from ..utils import set_to_root_org +from ..models import Organization + +__all__ = [ + 'RootOrgViewMixin', 'OrgMembershipModelViewSetMixin', 'OrgModelViewSet', + 'OrgBulkModelViewSet', +] + + +class RootOrgViewMixin: + def dispatch(self, request, *args, **kwargs): + set_to_root_org() + return super().dispatch(request, *args, **kwargs) + + +class OrgModelViewSet(IDInCacheFilterMixin, ModelViewSet): + def get_queryset(self): + return super().get_queryset().all() + + +class OrgBulkModelViewSet(IDInCacheFilterMixin, BulkModelViewSet): + def get_queryset(self): + queryset = super().get_queryset().all() + if hasattr(self, 'action') and self.action == 'list' and \ + hasattr(self, 'serializer_class') and \ + hasattr(self.serializer_class, 'setup_eager_loading'): + queryset = self.serializer_class.setup_eager_loading(queryset) + return queryset + + +class OrgMembershipModelViewSetMixin: + org = None + membership_class = None + lookup_field = 'user' + lookup_url_kwarg = 'user_id' + http_method_names = ['get', 'post', 'delete', 'head', 'options'] + + def dispatch(self, request, *args, **kwargs): + self.org = get_object_or_404(Organization, pk=kwargs.get('org_id')) + return super().dispatch(request, *args, **kwargs) + + def get_serializer_context(self): + context = super().get_serializer_context() + context['org'] = self.org + return context + + def get_queryset(self): + queryset = self.membership_class.objects.filter(organization=self.org) + return queryset diff --git a/apps/orgs/mixins/forms.py b/apps/orgs/mixins/forms.py new file mode 100644 index 000000000..49ec106f1 --- /dev/null +++ b/apps/orgs/mixins/forms.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# + +from django import forms + +__all__ = ['OrgModelForm'] + + +class OrgModelForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + if not hasattr(field, 'queryset'): + continue + model = field.queryset.model + field.queryset = model.objects.all() diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py new file mode 100644 index 000000000..3f54e14a7 --- /dev/null +++ b/apps/orgs/mixins/models.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# + +import traceback +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError + +from common.utils import get_logger +from ..utils import ( + set_current_org, get_current_org, current_org, +) +from ..models import Organization + +logger = get_logger(__file__) + +__all__ = [ + 'OrgManager', 'OrgModelMixin', +] + + +class OrgManager(models.Manager): + + def get_queryset(self): + queryset = super(OrgManager, self).get_queryset() + kwargs = {} + + _current_org = get_current_org() + if _current_org is None: + kwargs['id'] = None + elif _current_org.is_real(): + kwargs['org_id'] = _current_org.id + elif _current_org.is_default(): + queryset = queryset.filter(org_id="") + # + # lines = traceback.format_stack() + # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + # for line in lines[-10:-1]: + # print(line) + # print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + + queryset = queryset.filter(**kwargs) + return queryset + + def all(self): + if not current_org: + msg = 'You can `objects.set_current_org(org).all()` then run it' + return self + else: + return super(OrgManager, self).all() + + def set_current_org(self, org): + if isinstance(org, str): + org = Organization.get_instance(org) + set_current_org(org) + return self + + +class OrgModelMixin(models.Model): + org_id = models.CharField(max_length=36, blank=True, default='', + verbose_name=_("Organization"), db_index=True) + objects = OrgManager() + + sep = '@' + + def save(self, *args, **kwargs): + if current_org is not None and current_org.is_real(): + self.org_id = current_org.id + return super().save(*args, **kwargs) + + @property + def org(self): + from orgs.models import Organization + org = Organization.get_instance(self.org_id) + return org + + @property + def org_name(self): + return self.org.name + + @property + def fullname(self, attr=None): + name = '' + if attr and hasattr(self, attr): + name = getattr(self, attr) + elif hasattr(self, 'name'): + name = self.name + elif hasattr(self, 'hostname'): + name = self.hostname + if self.org.is_real(): + return name + self.sep + self.org_name + else: + return name + + def validate_unique(self, exclude=None): + """ + Check unique constraints on the model and raise ValidationError if any + failed. + Form 提交时会使用这个检验 + """ + self.org_id = current_org.id if current_org.is_real() else '' + if exclude and 'org_id' in exclude: + exclude.remove('org_id') + unique_checks, date_checks = self._get_unique_checks(exclude=exclude) + + errors = self._perform_unique_checks(unique_checks) + date_errors = self._perform_date_checks(date_checks) + + for k, v in date_errors.items(): + errors.setdefault(k, []).extend(v) + + if errors: + raise ValidationError(errors) + + class Meta: + abstract = True diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py new file mode 100644 index 000000000..ea4e4bd6b --- /dev/null +++ b/apps/orgs/mixins/serializers.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from common.validators import ProjectUniqueValidator +from common.mixins import BulkSerializerMixin +from ..utils import get_current_org_id_for_serializer + + +__all__ = [ + "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", + "BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin" +] + + +class OrgResourceSerializerMixin(serializers.Serializer): + """ + 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id + (同时为serializer.is_valid()对Model的unique_together校验做准备) + 由于HiddenField字段不可读,API获取资产信息时获取不到org_id, + 但是coco需要资产的org_id字段,所以修改为CharField类型 + """ + org_id = serializers.ReadOnlyField(default=get_current_org_id_for_serializer, label=_("Organization")) + org_name = serializers.ReadOnlyField(label=_("Org name")) + + def get_validators(self): + _validators = super().get_validators() + validators = [] + + for v in _validators: + if isinstance(v, UniqueTogetherValidator) \ + and "org_id" in v.fields: + v = ProjectUniqueValidator(v.queryset, v.fields) + validators.append(v) + return validators + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(["org_id", "org_name"]) + return fields + + +class BulkOrgResourceSerializerMixin(OrgResourceSerializerMixin, BulkSerializerMixin): + pass + + +class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): + pass + + +class OrgMembershipSerializerMixin: + def run_validation(self, initial_data=None): + initial_data['organization'] = str(self.context['org'].id) + return super().run_validation(initial_data) diff --git a/apps/orgs/models.py b/apps/orgs/models.py index e00270f72..e46c2b55d 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -1,7 +1,6 @@ import uuid from django.db import models -from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from common.utils import is_uuid @@ -16,9 +15,13 @@ class Organization(models.Model): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) + orgs = None CACHE_PREFIX = 'JMS_ORG_{}' - ROOT_ID_NAME = 'ROOT' - DEFAULT_ID_NAME = 'DEFAULT' + ROOT_ID = '00000000-0000-0000-0000-000000000000' + ROOT_NAME = 'ROOT' + DEFAULT_ID = 'DEFAULT' + DEFAULT_NAME = 'DEFAULT' + _user_admin_orgs = None class Meta: verbose_name = _("Organization") @@ -27,33 +30,30 @@ class Organization(models.Model): return self.name def set_to_cache(self): - key_id = self.CACHE_PREFIX.format(self.id) - key_name = self.CACHE_PREFIX.format(self.name) - cache.set(key_id, self, 3600) - cache.set(key_name, self, 3600) + if self.__class__.orgs is None: + self.__class__.orgs = {} + self.__class__.orgs[str(self.id)] = self def expire_cache(self): - key_id = self.CACHE_PREFIX.format(self.id) - key_name = self.CACHE_PREFIX.format(self.name) - cache.delete(key_id) - cache.delete(key_name) + self.__class__.orgs.pop(str(self.id), None) @classmethod def get_instance_from_cache(cls, oid): - key = cls.CACHE_PREFIX.format(oid) - return cache.get(key, None) + if not cls.orgs or not isinstance(cls.orgs, dict): + return None + return cls.orgs.get(str(oid)) @classmethod - def get_instance(cls, id_or_name, default=True): + def get_instance(cls, id_or_name, default=False): cached = cls.get_instance_from_cache(id_or_name) if cached: return cached - if not id_or_name: + if id_or_name is None: return cls.default() if default else None - elif id_or_name == cls.DEFAULT_ID_NAME: + elif id_or_name in [cls.DEFAULT_ID, cls.DEFAULT_NAME, '']: return cls.default() - elif id_or_name == cls.ROOT_ID_NAME: + elif id_or_name in [cls.ROOT_ID, cls.ROOT_NAME]: return cls.root() try: @@ -89,7 +89,7 @@ class Organization(models.Model): return False def is_real(self): - return len(str(self.id)) == 36 + return self.id not in (self.DEFAULT_NAME, self.ROOT_ID) @classmethod def get_user_admin_orgs(cls, user): @@ -105,20 +105,20 @@ class Organization(models.Model): @classmethod def default(cls): - return cls(id=cls.DEFAULT_ID_NAME, name=cls.DEFAULT_ID_NAME) + return cls(id=cls.DEFAULT_ID, name=cls.DEFAULT_NAME) @classmethod def root(cls): - return cls(id=cls.ROOT_ID_NAME, name=cls.ROOT_ID_NAME) + return cls(id=cls.ROOT_ID, name=cls.ROOT_NAME) def is_root(self): - if self.id is self.ROOT_ID_NAME: + if self.id is self.ROOT_ID: return True else: return False def is_default(self): - if self.id is self.DEFAULT_ID_NAME: + if self.id is self.DEFAULT_ID: return True else: return False diff --git a/apps/orgs/signals_handler.py b/apps/orgs/signals_handler.py index a60ab3b7d..9d3c8882f 100644 --- a/apps/orgs/signals_handler.py +++ b/apps/orgs/signals_handler.py @@ -6,7 +6,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from .models import Organization -from .hands import set_current_org, current_org, Node +from .hands import set_current_org, current_org, Node, get_current_org from perms.models import AssetPermission from users.models import UserGroup @@ -14,7 +14,7 @@ from users.models import UserGroup @receiver(post_save, sender=Organization) def on_org_create_or_update(sender, instance=None, created=False, **kwargs): if instance: - old_org = current_org + old_org = get_current_org() set_current_org(instance) node_root = Node.root() if node_root.value != instance.name: @@ -41,3 +41,8 @@ def on_org_user_changed(sender, instance=None, **kwargs): for user_group in user_groups: user_group.users.remove(user) set_current_org(old_org) + + +@receiver(m2m_changed, sender=Organization.admins.through) +def on_org_admin_change(sender, **kwargs): + Organization._user_admin_orgs = None diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 808536984..8f380b169 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- # -from functools import partial -from werkzeug.local import Local +from werkzeug.local import LocalProxy -from common.utils import LocalProxy +from common.local import thread_local from .models import Organization -_thread_locals = Local() - - def get_org_from_request(request): oid = request.session.get("oid") if not oid: oid = request.META.get("HTTP_X_JMS_ORG") + if not oid: + oid = Organization.DEFAULT_ID org = Organization.get_instance(oid) return org def set_current_org(org): - setattr(_thread_locals, 'current_org', org) + setattr(thread_local, 'current_org_id', org.id) def set_to_default_org(): @@ -31,17 +29,27 @@ def set_to_root_org(): def _find(attr): - return getattr(_thread_locals, attr, None) + return getattr(thread_local, attr, None) def get_current_org(): - return _find('current_org') + org_id = get_current_org_id() + if org_id is None: + return None + org = Organization.get_instance(org_id) + return org def get_current_org_id(): - org = get_current_org() - org_id = str(org.id) if org.is_real() else '' + org_id = _find('current_org_id') return org_id -current_org = LocalProxy(partial(_find, 'current_org')) +def get_current_org_id_for_serializer(): + org_id = get_current_org_id() + if org_id == Organization.DEFAULT_ID: + org_id = '' + return org_id + + +current_org = LocalProxy(get_current_org) diff --git a/apps/orgs/views.py b/apps/orgs/views.py index 29bd231a7..e7bf9fc62 100644 --- a/apps/orgs/views.py +++ b/apps/orgs/views.py @@ -13,7 +13,8 @@ class SwitchOrgView(DetailView): def get(self, request, *args, **kwargs): pk = kwargs.get('pk') self.object = Organization.get_instance(pk) - request.session['oid'] = self.object.id.__str__() + oid = str(self.object.id) + request.session['oid'] = oid host = request.get_host() referer = request.META.get('HTTP_REFERER') if referer.find(host) != -1: diff --git a/apps/perms/api/__init__.py b/apps/perms/api/__init__.py index aebfb2d33..00dd28776 100644 --- a/apps/perms/api/__init__.py +++ b/apps/perms/api/__init__.py @@ -5,3 +5,4 @@ from .asset_permission import * from .user_permission import * from .user_group_permission import * from .remote_app_permission import * +from .user_remote_app_permission import * diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py index a0381e698..38e44abd0 100644 --- a/apps/perms/api/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -10,7 +10,7 @@ from rest_framework.pagination import LimitOffsetPagination from common.permissions import IsOrgAdmin from common.utils import get_object_or_none -from ..models import AssetPermission, Action +from ..models import AssetPermission from ..hands import ( User, UserGroup, Asset, Node, SystemUser, ) @@ -20,16 +20,10 @@ from .. import serializers __all__ = [ 'AssetPermissionViewSet', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi', 'AssetPermissionRemoveAssetApi', - 'AssetPermissionAddAssetApi', 'ActionViewSet', + 'AssetPermissionAddAssetApi', ] -class ActionViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Action.objects.all() - serializer_class = serializers.ActionSerializer - permission_classes = (IsOrgAdmin,) - - class AssetPermissionViewSet(viewsets.ModelViewSet): """ 资产授权列表的增删改查api @@ -41,7 +35,8 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): permission_classes = (IsOrgAdmin,) def get_serializer_class(self): - if self.action in ("list", 'retrieve'): + if self.action in ("list", 'retrieve') and \ + self.request.query_params.get("display"): return serializers.AssetPermissionListSerializer return self.serializer_class @@ -160,7 +155,9 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): return queryset def get_queryset(self): - return self.queryset.all() + return self.queryset.all().prefetch_related( + "nodes", "assets", "users", "user_groups", "system_users" + ) class AssetPermissionRemoveUserApi(RetrieveUpdateAPIView): diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index 841a82096..4d59c13a7 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -8,14 +8,12 @@ from rest_framework.generics import ( from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.tree import TreeNodeSerializer -from orgs.utils import set_to_root_org from ..utils import ( AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node, RemoteAppPermissionUtil, ) from ..hands import ( - AssetGrantedSerializer, UserGroup, Node, NodeSerializer, - RemoteAppSerializer, + UserGroup, Node, NodeSerializer, RemoteAppSerializer, ) from .. import serializers, const @@ -30,7 +28,7 @@ __all__ = [ class UserGroupGrantedAssetsApi(ListAPIView): permission_classes = (IsOrgAdmin,) - serializer_class = AssetGrantedSerializer + serializer_class = serializers.AssetGrantedSerializer def get_queryset(self): user_group_id = self.kwargs.get('pk', '') @@ -120,7 +118,7 @@ class UserGroupGrantedNodesWithAssetsAsTreeApi(ListAPIView): class UserGroupGrantedNodeAssetsApi(ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = AssetGrantedSerializer + serializer_class = serializers.AssetGrantedSerializer def get_queryset(self): user_group_id = self.kwargs.get('pk', '') diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index 8cf5dc3f3..3eafc28e0 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # - +import time +import traceback from hashlib import md5 from django.core.cache import cache from django.conf import settings 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, + ListAPIView, get_object_or_404, RetrieveAPIView ) from rest_framework.pagination import LimitOffsetPagination @@ -16,17 +17,10 @@ from common.tree import TreeNodeSerializer from common.utils import get_logger from ..utils import ( AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node, - check_system_user_action, RemoteAppPermissionUtil, - construct_remote_apps_tree_root, parse_remote_app_to_tree_node, -) -from ..hands import ( - User, Asset, Node, SystemUser, RemoteApp, AssetGrantedSerializer, - NodeSerializer, RemoteAppSerializer, ) +from ..hands import User, Asset, Node, SystemUser, NodeSerializer from .. import serializers, const -from ..mixins import ( - AssetsFilterMixin, RemoteAppFilterMixin -) +from ..mixins import AssetsFilterMixin from ..models import Action logger = get_logger(__name__) @@ -36,8 +30,6 @@ __all__ = [ 'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi', 'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi', 'UserGrantedNodesWithAssetsAsTreeApi', 'GetUserAssetPermissionActionsApi', - 'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi', - 'UserGrantedRemoteAppsAsTreeApi', ] @@ -63,7 +55,11 @@ class UserPermissionCacheMixin: return None def get_request_md5(self): - full_path = self.request.get_full_path() + path = self.request.path + query = {k: v for k, v in self.request.GET.items()} + query.pop("_", None) + query = "&".join(["{}={}".format(k, v) for k, v in query.items()]) + full_path = "{}?{}".format(path, query) return md5(full_path.encode()).hexdigest() def get_meta_cache_id(self): @@ -80,15 +76,16 @@ class UserPermissionCacheMixin: return resp_cache_id def get_response_from_cache(self): - resp_cache_id = self.get_response_cache_id() # 没有数据缓冲 meta_cache_id = self.get_meta_cache_id() if not meta_cache_id: + logger.debug("Not get meta id: {}".format(meta_cache_id)) return None # 从响应缓冲里获取响应 - key = self.RESP_CACHE_KEY.format(resp_cache_id) + key = self.get_response_key() data = cache.get(key) if not data: + logger.debug("Not get response from cache: {}".format(key)) return None logger.debug("Get user permission from cache: {}".format(self.get_object())) response = Response(data) @@ -100,23 +97,32 @@ class UserPermissionCacheMixin: key = self.RESP_CACHE_KEY.format(expire_cache_id) cache.delete_pattern(key) - def set_response_to_cache(self, response): + def get_response_key(self): resp_cache_id = self.get_response_cache_id() key = self.RESP_CACHE_KEY.format(resp_cache_id) + return key + + def set_response_to_cache(self, response): + key = self.get_response_key() cache.set(key, response.data, self.CACHE_TIME) + logger.debug("Set response to cache: {}".format(key)) def get(self, request, *args, **kwargs): self.cache_policy = request.GET.get('cache_policy', '0') obj = self._get_object() if obj is None: + logger.debug("Not get response from cache: obj is none") return super().get(request, *args, **kwargs) if AssetPermissionUtil.is_not_using_cache(self.cache_policy): + logger.debug("Not get resp from cache: {}".format(self.cache_policy)) return super().get(request, *args, **kwargs) elif AssetPermissionUtil.is_refresh_cache(self.cache_policy): + logger.debug("Not get resp from cache: {}".format(self.cache_policy)) self.expire_response_cache() + logger.debug("Try get response from cache") resp = self.get_response_from_cache() if not resp: resp = super().get(request, *args, **kwargs) @@ -129,7 +135,7 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV 用户授权的所有资产 """ permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = AssetGrantedSerializer + serializer_class = serializers.AssetGrantedSerializer pagination_class = LimitOffsetPagination def get_object(self): @@ -145,10 +151,13 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV user = self.get_object() util = AssetPermissionUtil(user, cache_policy=self.cache_policy) assets = util.get_assets() - for k, v in assets.items(): - system_users_granted = [s for s in v if k.has_protocol(s.protocol)] - k.system_users_granted = system_users_granted - queryset.append(k) + for asset, system_users in assets.items(): + system_users_granted = [] + for system_user, actions in system_users.items(): + system_user.actions = actions + system_users_granted.append(system_user) + asset.system_users_granted = system_users_granted + queryset.append(asset) return queryset def get_permissions(self): @@ -159,7 +168,7 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView): """ - 查询用户授权的所有节点的API, 如果是超级用户或者是 app,切换到root org + 查询用户授权的所有节点的API """ permission_classes = (IsOrgAdminOrAppUser,) serializer_class = NodeSerializer @@ -175,8 +184,8 @@ class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView): def get_queryset(self): user = self.get_object() util = AssetPermissionUtil(user, cache_policy=self.cache_policy) - nodes = util.get_nodes_with_assets() - return nodes.keys() + nodes = util.get_nodes() + return nodes def get_permissions(self): if self.kwargs.get('pk') is None: @@ -207,8 +216,7 @@ class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, for node, _assets in nodes.items(): assets = _assets.keys() for k, v in _assets.items(): - system_users_granted = [s for s in v if k.has_protocol(s.protocol)] - k.system_users_granted = system_users_granted + k.system_users_granted = v node.assets_granted = assets queryset.append(node) return queryset @@ -243,10 +251,6 @@ class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionCacheMixin, ListAPIView) user = get_object_or_404(User, id=user_id) return user - def list(self, request, *args, **kwargs): - resp = super().list(request, *args, **kwargs) - return resp - def get_queryset(self): queryset = [] self.show_assets = self.request.query_params.get('show_assets', '1') == '1' @@ -275,7 +279,7 @@ class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, List 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 """ permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = AssetGrantedSerializer + serializer_class = serializers.AssetGrantedSerializer pagination_class = LimitOffsetPagination def get_object(self): @@ -291,15 +295,17 @@ class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, List user = self.get_object() node_id = self.kwargs.get('node_id') util = AssetPermissionUtil(user, cache_policy=self.cache_policy) + nodes = util.get_nodes_with_assets() if str(node_id) == const.UNGROUPED_NODE_ID: node = util.tree.ungrouped_node + elif str(node_id) == const.EMPTY_NODE_ID: + node = util.tree.empty_node else: node = get_object_or_404(Node, id=node_id) if node == util.tree.root_node: assets = util.get_assets() else: - nodes = util.get_nodes_with_assets() - assets = nodes.get(node, []) + assets = nodes.get(node, {}) for asset, system_users in assets.items(): asset.system_users_granted = system_users @@ -400,7 +406,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView): class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView): permission_classes = (IsOrgAdminOrAppUser,) - + def get(self, request, *args, **kwargs): user_id = request.query_params.get('user_id', '') asset_id = request.query_params.get('asset_id', '') @@ -410,29 +416,30 @@ class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView): user = get_object_or_404(User, id=user_id) asset = get_object_or_404(Asset, id=asset_id) su = get_object_or_404(SystemUser, id=system_id) - action = get_object_or_404(Action, name=action_name) util = AssetPermissionUtil(user, cache_policy=self.cache_policy) granted_assets = util.get_assets() - granted_system_users = granted_assets.get(asset, []) + granted_system_users = granted_assets.get(asset, {}) if su not in granted_system_users: return Response({'msg': False}, status=403) - _su = next((s for s in granted_system_users if s.id == su.id), None) - if not check_system_user_action(_su, action): + action = granted_system_users[su] + choices = Action.value_to_choices(action) + if action_name not in choices: return Response({'msg': False}, status=403) return Response({'msg': True}, status=200) -class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, APIView): +class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, RetrieveAPIView): permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.ActionsSerializer - def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id', '') - asset_id = request.query_params.get('asset_id', '') - system_id = request.query_params.get('system_user_id', '') + def get_object(self): + user_id = self.request.query_params.get('user_id', '') + asset_id = self.request.query_params.get('asset_id', '') + system_id = self.request.query_params.get('system_user_id', '') user = get_object_or_404(User, id=user_id) asset = get_object_or_404(Asset, id=asset_id) @@ -440,86 +447,11 @@ class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, APIView): util = AssetPermissionUtil(user, cache_policy=self.cache_policy) granted_assets = util.get_assets() - granted_system_users = granted_assets.get(asset, []) - _su = next((s for s in granted_system_users if s.id == su.id), None) - if not _su: - return Response({'actions': []}, status=403) + granted_system_users = granted_assets.get(asset, {}) - actions = [action.name for action in getattr(_su, 'actions', [])] - return Response({'actions': actions}, status=200) - - -# RemoteApp permission - -class UserGrantedRemoteAppsApi(RemoteAppFilterMixin, ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = RemoteAppSerializer - pagination_class = LimitOffsetPagination - - def get_object(self): - user_id = self.kwargs.get('pk', '') - if user_id: - user = get_object_or_404(User, id=user_id) + _object = {} + if su not in granted_system_users: + _object['actions'] = 0 else: - user = self.request.user - return user - - def get_queryset(self): - util = RemoteAppPermissionUtil(self.get_object()) - queryset = util.get_remote_apps() - queryset = list(queryset) - return queryset - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - -class UserGrantedRemoteAppsAsTreeApi(ListAPIView): - serializer_class = TreeNodeSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def get_object(self): - user_id = self.kwargs.get('pk', '') - if not user_id: - user = self.request.user - else: - user = get_object_or_404(User, id=user_id) - return user - - def get_queryset(self): - queryset = [] - tree_root = construct_remote_apps_tree_root() - queryset.append(tree_root) - - util = RemoteAppPermissionUtil(self.get_object()) - remote_apps = util.get_remote_apps() - for remote_app in remote_apps: - node = parse_remote_app_to_tree_node(tree_root, remote_app) - queryset.append(node) - - queryset = sorted(queryset) - return queryset - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - -class ValidateUserRemoteAppPermissionApi(APIView): - permission_classes = (IsOrgAdminOrAppUser,) - - def get(self, request, *args, **kwargs): - self.change_org_if_need(request, kwargs) - user_id = request.query_params.get('user_id', '') - remote_app_id = request.query_params.get('remote_app_id', '') - user = get_object_or_404(User, id=user_id) - remote_app = get_object_or_404(RemoteApp, id=remote_app_id) - - util = RemoteAppPermissionUtil(user) - remote_apps = util.get_remote_apps() - if remote_app not in remote_apps: - return Response({'msg': False}, status=403) - return Response({'msg': True}, status=200) + _object['actions'] = granted_system_users[su] + return _object diff --git a/apps/perms/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py new file mode 100644 index 000000000..4cbfcd9b4 --- /dev/null +++ b/apps/perms/api/user_remote_app_permission.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +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, +) +from rest_framework.pagination import LimitOffsetPagination + +from common.permissions import IsValidUser, IsOrgAdminOrAppUser +from common.tree import TreeNodeSerializer +from ..utils import ( + RemoteAppPermissionUtil, construct_remote_apps_tree_root, + parse_remote_app_to_tree_node, +) +from ..hands import User, RemoteApp, RemoteAppSerializer +from ..mixins import RemoteAppFilterMixin + + +__all__ = [ + 'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi', + 'UserGrantedRemoteAppsAsTreeApi', +] + + +class UserGrantedRemoteAppsApi(RemoteAppFilterMixin, ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = RemoteAppSerializer + pagination_class = LimitOffsetPagination + + def get_object(self): + user_id = self.kwargs.get('pk', '') + if user_id: + user = get_object_or_404(User, id=user_id) + else: + user = self.request.user + return user + + def get_queryset(self): + util = RemoteAppPermissionUtil(self.get_object()) + queryset = util.get_remote_apps() + queryset = list(queryset) + return queryset + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class UserGrantedRemoteAppsAsTreeApi(ListAPIView): + serializer_class = TreeNodeSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + def get_object(self): + user_id = self.kwargs.get('pk', '') + if not user_id: + user = self.request.user + else: + user = get_object_or_404(User, id=user_id) + return user + + def get_queryset(self): + queryset = [] + tree_root = construct_remote_apps_tree_root() + queryset.append(tree_root) + + util = RemoteAppPermissionUtil(self.get_object()) + remote_apps = util.get_remote_apps() + for remote_app in remote_apps: + node = parse_remote_app_to_tree_node(tree_root, remote_app) + queryset.append(node) + + queryset = sorted(queryset) + return queryset + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class ValidateUserRemoteAppPermissionApi(APIView): + permission_classes = (IsOrgAdminOrAppUser,) + + def get(self, request, *args, **kwargs): + user_id = request.query_params.get('user_id', '') + remote_app_id = request.query_params.get('remote_app_id', '') + user = get_object_or_404(User, id=user_id) + remote_app = get_object_or_404(RemoteApp, id=remote_app_id) + + util = RemoteAppPermissionUtil(user) + remote_apps = util.get_remote_apps() + if remote_app not in remote_apps: + return Response({'msg': False}, status=403) + return Response({'msg': True}, status=200) diff --git a/apps/perms/const.py b/apps/perms/const.py index 457db580c..b18580747 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ - -__all__ = [ - 'PERMS_ACTION_NAME_ALL', 'PERMS_ACTION_NAME_CONNECT', - 'PERMS_ACTION_NAME_DOWNLOAD_FILE', 'PERMS_ACTION_NAME_UPLOAD_FILE', - 'PERMS_ACTION_NAME_CHOICES' -] - -PERMS_ACTION_NAME_ALL = 'all' -PERMS_ACTION_NAME_CONNECT = 'connect' -PERMS_ACTION_NAME_UPLOAD_FILE = 'upload_file' -PERMS_ACTION_NAME_DOWNLOAD_FILE = 'download_file' - -PERMS_ACTION_NAME_CHOICES = ( - (PERMS_ACTION_NAME_ALL, _('All')), - (PERMS_ACTION_NAME_CONNECT, _('Connect')), - (PERMS_ACTION_NAME_UPLOAD_FILE, _('Upload file')), - (PERMS_ACTION_NAME_DOWNLOAD_FILE, _('Download file')), -) - -UNGROUPED_NODE_ID = "00000000-0000-0000-0000-000000000000" +UNGROUPED_NODE_ID = "00000000-0000-0000-0000-000000000002" +EMPTY_NODE_ID = "00000000-0000-0000-0000-000000000003" diff --git a/apps/perms/forms/asset_permission.py b/apps/perms/forms/asset_permission.py index 81845fb77..d0b362a1b 100644 --- a/apps/perms/forms/asset_permission.py +++ b/apps/perms/forms/asset_permission.py @@ -6,29 +6,51 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins import OrgModelForm from orgs.utils import current_org -from perms.models import AssetPermission -from assets.models import Asset +from assets.models import Asset, Node +from ..models import AssetPermission, Action __all__ = [ 'AssetPermissionForm', ] +class ActionField(forms.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Action.CHOICES + kwargs['initial'] = Action.ALL + kwargs['label'] = _("Action") + kwargs['widget'] = forms.CheckboxSelectMultiple() + super().__init__(*args, **kwargs) + + def to_python(self, value): + value = super().to_python(value) + return Action.choices_to_value(value) + + def prepare_value(self, value): + if value is None: + return value + value = Action.value_to_choices(value) + return value + + class AssetPermissionForm(OrgModelForm): + actions = ActionField() + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) users_field = self.fields.get('users') - if hasattr(users_field, 'queryset'): - users_field.queryset = current_org.get_org_users() - assets_field = self.fields.get('assets') + users_field.queryset = current_org.get_org_users() # 前端渲染优化, 防止过多资产 if not self.data: instance = kwargs.get('instance') + assets_field = self.fields['assets'] if instance: assets_field.queryset = instance.assets.all() else: assets_field.queryset = Asset.objects.none() + nodes_field = self.fields['nodes'] + nodes_field._queryset = Node.get_queryset() class Meta: model = AssetPermission @@ -51,16 +73,13 @@ class AssetPermissionForm(OrgModelForm): 'system_users': forms.SelectMultiple( attrs={'class': 'select2', 'data-placeholder': _('System user')} ), - 'actions': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Action')} - ) } labels = { 'nodes': _("Node"), } help_texts = { 'actions': _('Tips: The RDP protocol does not support separate ' - 'controls for uploading or downloading files') + 'controls for uploading or downloading files') } def clean_user_groups(self): diff --git a/apps/perms/hands.py b/apps/perms/hands.py index 1195abf35..bbdc01e1e 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -3,9 +3,7 @@ from users.models import User, UserGroup from assets.models import Asset, SystemUser, Node -from assets.serializers import ( - AssetGrantedSerializer, NodeSerializer -) +from assets.serializers import NodeSerializer from applications.serializers import RemoteAppSerializer from applications.models import RemoteApp diff --git a/apps/perms/migrations/0003_action.py b/apps/perms/migrations/0003_action.py index 41c0f9f39..c9dea7736 100644 --- a/apps/perms/migrations/0003_action.py +++ b/apps/perms/migrations/0003_action.py @@ -4,14 +4,6 @@ from django.db import migrations, models import uuid -def add_default_actions(apps, schema_editor): - from ..const import PERMS_ACTION_NAME_CHOICES - action_model = apps.get_model('perms', 'Action') - db_alias = schema_editor.connection.alias - for action, _ in PERMS_ACTION_NAME_CHOICES: - action_model.objects.using(db_alias).update_or_create(name=action) - - class Migration(migrations.Migration): dependencies = [ @@ -29,5 +21,4 @@ class Migration(migrations.Migration): 'verbose_name': 'Action', }, ), - migrations.RunPython(add_default_actions) ] diff --git a/apps/perms/migrations/0003_auto_20180225_1815.py b/apps/perms/migrations/0003_auto_20180225_1815.py deleted file mode 100644 index 46d850f1c..000000000 --- a/apps/perms/migrations/0003_auto_20180225_1815.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-02-25 10:15 -from __future__ import unicode_literals - -import common.utils -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0004_auto_20180125_1218'), - ('assets', '0007_auto_20180225_1815'), - ('perms', '0002_auto_20171228_0025'), - ] - - operations = [ - migrations.CreateModel( - name='NodePermission', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('is_active', models.BooleanField(default=True, verbose_name='Active')), - ('date_expired', models.DateTimeField(default=common.utils.date_expired_default, verbose_name='Date expired')), - ('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Node', verbose_name='Node')), - ('system_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='System user')), - ('user_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.UserGroup', verbose_name='User group')), - ], - options={ - 'verbose_name': 'Asset permission', - }, - ), - migrations.AlterUniqueTogether( - name='nodepermission', - unique_together=set([('node', 'user_group', 'system_user')]), - ), - ] diff --git a/apps/perms/migrations/0004_assetpermission_actions.py b/apps/perms/migrations/0004_assetpermission_actions.py index be468e7f0..dfe07f317 100644 --- a/apps/perms/migrations/0004_assetpermission_actions.py +++ b/apps/perms/migrations/0004_assetpermission_actions.py @@ -3,18 +3,6 @@ from django.db import migrations, models -def set_default_action_to_existing_perms(apps, schema_editor): - from orgs.utils import set_to_root_org - from ..models import Action - set_to_root_org() - perm_model = apps.get_model('perms', 'AssetPermission') - db_alias = schema_editor.connection.alias - perms = perm_model.objects.using(db_alias).all() - default_action = Action.get_action_all() - for perm in perms: - perm.actions.add(default_action.id) - - class Migration(migrations.Migration): dependencies = [ @@ -27,5 +15,4 @@ class Migration(migrations.Migration): name='actions', field=models.ManyToManyField(blank=True, related_name='permissions', to='perms.Action', verbose_name='Action'), ), - migrations.RunPython(set_default_action_to_existing_perms) ] diff --git a/apps/perms/migrations/0004_auto_20180411_1135.py b/apps/perms/migrations/0004_auto_20180411_1135.py deleted file mode 100644 index 182ee7917..000000000 --- a/apps/perms/migrations/0004_auto_20180411_1135.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-11 03:35 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0013_auto_20180411_1135'), - ('perms', '0003_auto_20180225_1815'), - ] - - operations = [ - migrations.RemoveField( - model_name='assetpermission', - name='asset_groups', - ), - migrations.AddField( - model_name='assetpermission', - name='date_start', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date start'), - ), - migrations.AddField( - model_name='assetpermission', - name='nodes', - field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Nodes'), - ), - ] diff --git a/apps/perms/migrations/0005_migrate_data_20180411_1144.py b/apps/perms/migrations/0005_migrate_data_20180411_1144.py deleted file mode 100644 index 0a013467e..000000000 --- a/apps/perms/migrations/0005_migrate_data_20180411_1144.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-11 03:35 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.utils.timezone - - -def migrate_node_permissions(apps, schema_editor): - node_perm_model = apps.get_model("perms", "NodePermission") - asset_perm_model = apps.get_model("perms", "AssetPermission") - db_alias = schema_editor.connection.alias - for old in node_perm_model.objects.using(db_alias).all(): - perm = asset_perm_model.objects.using(db_alias).create( - name="{}-{}-{}".format( - old.node.value, - old.user_group.name, - old.system_user.name - ), - is_active=old.is_active, - date_expired=old.date_expired, - created_by=old.date_expired, - date_created=old.date_created, - comment=old.comment, - ) - perm.user_groups.add(old.user_group) - perm.nodes.add(old.node) - perm.system_users.add(old.system_user) - - -def migrate_system_assets_relation(apps, schema_editor): - system_user_model = apps.get_model("assets", "SystemUser") - db_alias = schema_editor.connection.alias - for s in system_user_model.objects.using(db_alias).all(): - nodes = list(s.nodes.all()) - s.nodes.set([]) - s.nodes.set(nodes) - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0004_auto_20180411_1135'), - ] - - operations = [ - migrations.RunPython(migrate_node_permissions), - migrations.RunPython(migrate_system_assets_relation), - ] diff --git a/apps/perms/migrations/0006_auto_20180606_1505.py b/apps/perms/migrations/0006_auto_20180606_1505.py deleted file mode 100644 index 915769ddb..000000000 --- a/apps/perms/migrations/0006_auto_20180606_1505.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-06-06 07:05 -from __future__ import unicode_literals - -import common.utils -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0005_migrate_data_20180411_1144'), - ] - - operations = [ - migrations.AlterField( - model_name='assetpermission', - name='date_expired', - field=models.DateTimeField(db_index=True, default=common.utils.date_expired_default, verbose_name='Date expired'), - ), - migrations.AlterField( - model_name='assetpermission', - name='date_start', - field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start'), - ), - ] diff --git a/apps/perms/migrations/0006_auto_20190628_1921.py b/apps/perms/migrations/0006_auto_20190628_1921.py new file mode 100644 index 000000000..f0faec017 --- /dev/null +++ b/apps/perms/migrations/0006_auto_20190628_1921.py @@ -0,0 +1,43 @@ +# Generated by Django 2.1.7 on 2019-06-28 11:47 + +from django.db import migrations, models +from functools import reduce + + +def migrate_old_actions(apps, schema_editor): + from orgs.utils import set_to_root_org + set_to_root_org() + perm_model = apps.get_model('perms', 'AssetPermission') + db_alias = schema_editor.connection.alias + perms = perm_model.objects.using(db_alias).all() + actions_map = { + "all": 0b11111111, + "connect": 0b00000001, + "upload_file": 0b00000010, + "download_file": 0b00000100, + } + + for perm in perms: + actions = perm.actions.all() + if not actions: + continue + new_actions = [actions_map.get(action.name, 0b11111111) for action in actions] + new_action = reduce(lambda x, y: x | y, new_actions) + perm.action = new_action + perm.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0005_auto_20190521_1619'), + ] + + operations = [ + migrations.AddField( + model_name='assetpermission', + name='action', + field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download')], default=255, verbose_name='Actions'), + ), + migrations.RunPython(migrate_old_actions), + ] diff --git a/apps/perms/migrations/0007_auto_20180807_1116.py b/apps/perms/migrations/0007_auto_20180807_1116.py deleted file mode 100644 index e97a5ec6b..000000000 --- a/apps/perms/migrations/0007_auto_20180807_1116.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 2.0.7 on 2018-08-07 03:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0006_auto_20180606_1505'), - ] - - operations = [ - migrations.AddField( - model_name='assetpermission', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AddField( - model_name='nodepermission', - name='org_id', - field=models.CharField(blank=True, default=None, max_length=36, null=True), - ), - migrations.AlterField( - model_name='assetpermission', - name='name', - field=models.CharField(max_length=128, verbose_name='Name'), - ), - migrations.AlterUniqueTogether( - name='assetpermission', - unique_together={('org_id', 'name')}, - ), - migrations.AlterUniqueTogether( - name='nodepermission', - unique_together=set(), - ), - - ] diff --git a/apps/perms/migrations/0007_remove_assetpermission_actions.py b/apps/perms/migrations/0007_remove_assetpermission_actions.py new file mode 100644 index 000000000..31e083c88 --- /dev/null +++ b/apps/perms/migrations/0007_remove_assetpermission_actions.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.7 on 2019-06-28 12:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0006_auto_20190628_1921'), + ] + + operations = [ + migrations.RemoveField( + model_name='assetpermission', + name='actions', + ), + migrations.RenameField( + model_name='assetpermission', + old_name='action', + new_name='actions', + ), + migrations.DeleteModel('action'), + ] diff --git a/apps/perms/migrations/0008_auto_20180816_1652.py b/apps/perms/migrations/0008_auto_20180816_1652.py deleted file mode 100644 index 0d0f2e57e..000000000 --- a/apps/perms/migrations/0008_auto_20180816_1652.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.7 on 2018-08-16 08:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0007_auto_20180807_1116'), - ] - - operations = [ - migrations.AlterField( - model_name='assetpermission', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AlterField( - model_name='nodepermission', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - ] diff --git a/apps/perms/migrations/0009_auto_20180903_1132.py b/apps/perms/migrations/0009_auto_20180903_1132.py deleted file mode 100644 index 5c658fda1..000000000 --- a/apps/perms/migrations/0009_auto_20180903_1132.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.1 on 2018-09-03 03:32 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0008_auto_20180816_1652'), - ] - - operations = [ - migrations.AlterModelOptions( - name='assetpermission', - options={'verbose_name': 'Asset permission'}, - ), - ] diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index ed0aaa535..d5fafc63a 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,4 +1,5 @@ import uuid +from functools import reduce from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -6,43 +7,86 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import date_expired_default, set_or_append_attr_bulk from orgs.mixins import OrgModelMixin -from ..const import PERMS_ACTION_NAME_CHOICES, PERMS_ACTION_NAME_ALL from .base import BasePermission __all__ = [ - 'Action', 'AssetPermission', 'NodePermission', + 'AssetPermission', 'NodePermission', 'Action', ] -class Action(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField( - max_length=128, unique=True, choices=PERMS_ACTION_NAME_CHOICES, - verbose_name=_('Name') +class Action: + NONE = 0 + CONNECT = 0b00000001 + UPLOAD = 0b00000010 + DOWNLOAD = 0b00000100 + UPDOWNLOAD = UPLOAD | DOWNLOAD + ALL = 0b11111111 + + DB_CHOICES = ( + (ALL, _('All')), + (CONNECT, _('Connect')), + (UPLOAD, _('Upload file')), + (DOWNLOAD, _('Download file')), + (UPDOWNLOAD, _("Upload download")), ) - class Meta: - verbose_name = _('Action') + NAME_MAP = { + ALL: "all", + CONNECT: "connect", + UPLOAD: "upload_file", + DOWNLOAD: "download_file", + UPDOWNLOAD: "updownload", + } - def __str__(self): - return self.get_name_display() + NAME_MAP_REVERSE = dict({v: k for k, v in NAME_MAP.items()}) + CHOICES = [] + for i, j in DB_CHOICES: + CHOICES.append((NAME_MAP[i], j)) @classmethod - def get_action_all(cls): - return cls.objects.get(name=PERMS_ACTION_NAME_ALL) + def value_to_choices(cls, value): + value = int(value) + choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] + return choices + + @classmethod + def choices_to_value(cls, value): + if not isinstance(value, list): + return cls.NONE + db_value = [ + cls.NAME_MAP_REVERSE[v] for v in value + if v in cls.NAME_MAP_REVERSE.keys() + ] + if not db_value: + return cls.NONE + + def to_choices(x, y): + return x | y + + result = reduce(to_choices, db_value) + return result + + @classmethod + def choices(cls): + return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] class AssetPermission(BasePermission): assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user")) - actions = models.ManyToManyField('Action', related_name='permissions', blank=True, verbose_name=_('Action')) + # actions = models.ManyToManyField(Action, related_name='permissions', blank=True, verbose_name=_('Action')) + actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset permission") + @classmethod + def get_queryset_with_prefetch(cls): + return cls.objects.all().valid().prefetch_related('nodes', 'assets', 'system_users') + def get_all_assets(self): assets = set(self.assets.all()) for node in self.nodes.all(): diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 129901afc..1d099cb33 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -2,4 +2,5 @@ # from .asset_permission import * +from .user_permission import * from .remote_app_permission import * diff --git a/apps/perms/serializers/asset_permission.py b/apps/perms/serializers/asset_permission.py index 842c719e8..ecbed669b 100644 --- a/apps/perms/serializers/asset_permission.py +++ b/apps/perms/serializers/asset_permission.py @@ -6,24 +6,38 @@ from rest_framework import serializers from common.fields import StringManyToManyField from orgs.mixins import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action -from assets.models import Node -from assets.serializers import AssetGrantedSerializer __all__ = [ 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', - 'AssetPermissionNodeSerializer', 'GrantedNodeSerializer', - 'ActionSerializer', 'NodeGrantedSerializer', + 'ActionsField', ] -class ActionSerializer(serializers.ModelSerializer): - class Meta: - model = Action - fields = '__all__' +class ActionsField(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Action.CHOICES + super().__init__(*args, **kwargs) + + def to_representation(self, value): + return Action.value_to_choices(value) + + def to_internal_value(self, data): + if data is None: + return data + return Action.choices_to_value(data) + + +class ActionsDisplayField(ActionsField): + def to_representation(self, value): + values = super().to_representation(value) + choices = dict(Action.CHOICES) + return [choices.get(i) for i in values] class AssetPermissionCreateUpdateSerializer(BulkOrgResourceModelSerializer): + actions = ActionsField(required=False, allow_null=True) + class Meta: model = AssetPermission exclude = ('created_by', 'date_created') @@ -35,7 +49,7 @@ class AssetPermissionListSerializer(BulkOrgResourceModelSerializer): assets = StringManyToManyField(many=True, read_only=True) nodes = StringManyToManyField(many=True, read_only=True) system_users = StringManyToManyField(many=True, read_only=True) - actions = StringManyToManyField(many=True, read_only=True) + actions = ActionsDisplayField() is_valid = serializers.BooleanField() is_expired = serializers.BooleanField() @@ -56,87 +70,3 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): class Meta: model = AssetPermission fields = ['id', 'assets'] - - -class AssetPermissionNodeSerializer(serializers.ModelSerializer): - asset = AssetGrantedSerializer(required=False) - assets_amount = serializers.SerializerMethodField() - - tree_id = serializers.SerializerMethodField() - tree_parent = serializers.SerializerMethodField() - - class Meta: - model = Node - fields = [ - 'id', 'key', 'value', 'asset', 'is_node', 'org_id', - 'tree_id', 'tree_parent', 'assets_amount', - ] - - @staticmethod - def get_assets_amount(obj): - return obj.assets_amount - - @staticmethod - def get_tree_id(obj): - return obj.key - - @staticmethod - def get_tree_parent(obj): - return obj.parent_key - - -class NodeGrantedSerializer(serializers.ModelSerializer): - """ - 授权资产组 - """ - assets_granted = AssetGrantedSerializer(many=True, read_only=True) - assets_amount = serializers.SerializerMethodField() - parent = serializers.SerializerMethodField() - name = serializers.SerializerMethodField() - - class Meta: - model = Node - fields = [ - 'id', 'key', 'name', 'value', 'parent', - 'assets_granted', 'assets_amount', 'org_id', - ] - - @staticmethod - def get_assets_amount(obj): - return len(obj.assets_granted) - - @staticmethod - def get_name(obj): - return obj.name - - @staticmethod - def get_parent(obj): - return obj.parent.id - - -class GrantedNodeSerializer(serializers.ModelSerializer): - class Meta: - model = Node - fields = [ - 'id', 'name', 'key', 'value', - ] - - -# class GrantedAssetSerializer(serializers.ModelSerializer): -# protocols = ProtocolSerializer(many=True) -# -# class Meta: -# model = Asset -# fields = [ -# 'id', 'hostname', 'ip', 'protocols', 'port', 'protocol', -# 'platform', 'domain', 'is_active', 'comment' -# ] - - -# class GrantedSystemUserSerializer(serializers.ModelSerializer): -# class Meta: -# model = SystemUser -# fields = [ -# 'id', 'name', 'username', 'protocol', 'priority', -# 'login_mode', 'comment' -# ] diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py new file mode 100644 index 000000000..639ba7140 --- /dev/null +++ b/apps/perms/serializers/user_permission.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import serializers + +from assets.models import Node, SystemUser +from assets.serializers import AssetSerializer + +from .asset_permission import ActionsField + +__all__ = [ + 'AssetPermissionNodeSerializer', 'GrantedNodeSerializer', + 'NodeGrantedSerializer', 'AssetGrantedSerializer', + 'ActionsSerializer', +] + + +class AssetSystemUserSerializer(serializers.ModelSerializer): + """ + 查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少 + """ + actions = ActionsField(read_only=True) + + class Meta: + model = SystemUser + fields = ( + 'id', 'name', 'username', 'priority', "actions", + 'protocol', 'login_mode', + ) + + +class AssetGrantedSerializer(AssetSerializer): + """ + 被授权资产的数据结构 + """ + system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) + system_users_join = serializers.SerializerMethodField() + + @staticmethod + def get_system_users_join(obj): + system_users = [s.username for s in obj.system_users_granted] + return ', '.join(system_users) + + def get_field_names(self, declared_fields, info): + fields = ( + "id", "hostname", "ip", "protocols", + "system_users_granted", "is_active", "system_users_join", "os", + 'domain', "platform", "comment", "org_id", "org_name", + ) + return fields + + +class AssetPermissionNodeSerializer(serializers.ModelSerializer): + asset = AssetGrantedSerializer(required=False) + assets_amount = serializers.SerializerMethodField() + + tree_id = serializers.SerializerMethodField() + tree_parent = serializers.SerializerMethodField() + + class Meta: + model = Node + fields = [ + 'id', 'key', 'value', 'asset', 'is_node', 'org_id', + 'tree_id', 'tree_parent', 'assets_amount', + ] + + @staticmethod + def get_assets_amount(obj): + return obj.assets_amount + + @staticmethod + def get_tree_id(obj): + return obj.key + + @staticmethod + def get_tree_parent(obj): + return obj.parent_key + + +class NodeGrantedSerializer(serializers.ModelSerializer): + """ + 授权资产组 + """ + assets_granted = AssetGrantedSerializer(many=True, read_only=True) + assets_amount = serializers.SerializerMethodField() + parent = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + + class Meta: + model = Node + fields = [ + 'id', 'key', 'name', 'value', 'parent', + 'assets_granted', 'assets_amount', 'org_id', + ] + + @staticmethod + def get_assets_amount(obj): + return len(obj.assets_granted) + + @staticmethod + def get_name(obj): + return obj.name + + @staticmethod + def get_parent(obj): + return obj.parent.id + + +class GrantedNodeSerializer(serializers.ModelSerializer): + class Meta: + model = Node + fields = [ + 'id', 'name', 'key', 'value', + ] + + +class ActionsSerializer(serializers.Serializer): + actions = ActionsField(read_only=True) diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index 3ec712c75..e8f2eecbf 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -6,7 +6,7 @@ from django.db import transaction from common.utils import get_logger from .utils import AssetPermissionUtil -from .models import AssetPermission, Action +from .models import AssetPermission logger = get_logger(__file__) @@ -25,13 +25,6 @@ def on_transaction_commit(func): @on_transaction_commit def on_permission_created(sender, instance=None, created=False, **kwargs): AssetPermissionUtil.expire_all_cache() - actions = instance.actions.all() - if created and not actions: - default_action = Action.get_action_all() - instance.actions.add(default_action) - logger.debug( - "Set default action to perms: {}".format(default_action, instance) - ) @receiver(post_save, sender=AssetPermission) diff --git a/apps/perms/templates/perms/asset_permission_create_update.html b/apps/perms/templates/perms/asset_permission_create_update.html index cf7a46eff..5ada7f99b 100644 --- a/apps/perms/templates/perms/asset_permission_create_update.html +++ b/apps/perms/templates/perms/asset_permission_create_update.html @@ -110,6 +110,7 @@ var dateOptions = { format: 'YYYY-MM-DD HH:mm' } }; +var api_action = "{{ api_action }}"; $(document).ready(function () { $('.select2').select2({ closeOnSelect: false @@ -143,6 +144,29 @@ $(document).ready(function () { $('#id_assets').val(assets).trigger('change'); $("#asset_list_modal").modal('hide'); -}); +}) +.on("submit", "form", function (evt) { + evt.preventDefault(); + var the_url = '{% url 'api-perms:asset-permission-list' %}'; + var method = "POST"; + {% if api_action == "update" %} + the_url = '{% url 'api-perms:asset-permission-detail' pk=object.id %}'; + method = "PUT"; + {% endif %} + var redirect_to = '{% url "perms:asset-permission-list" %}'; + var form = $("form"); + var data = form.serializeObject(); + objectAttrsIsList(data, ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'actions']); + objectAttrsIsDatetime(data, ['date_start', 'date_expired']); + objectAttrsIsBool(data, ['is_active']); + var props = { + url: the_url, + data: data, + method: method, + form: form, + redirect_to: redirect_to + }; + formSubmit(props); + }) {% endblock %} \ No newline at end of file diff --git a/apps/perms/templates/perms/asset_permission_list.html b/apps/perms/templates/perms/asset_permission_list.html index b7b6d4d6f..5a10a22c2 100644 --- a/apps/perms/templates/perms/asset_permission_list.html +++ b/apps/perms/templates/perms/asset_permission_list.html @@ -24,15 +24,7 @@
-
-
-
-
-
-
-
-
-
+ {% include 'assets/_node_tree.html' %}
@@ -86,7 +78,7 @@ {% endblock %} -{% block content_left_head %} +{% block table_pagination %} {% endblock %} {% block table_search %} -
-
-
- - - to - -
-
-
- -
-
- -
-
- -
-
- -
-
-
- -
-
-
{% endblock %} + {% block table_container %} - +
- + - - {% for command in command_list %} - - - - - - - - - - - {% endfor %}
ID {% trans 'Command' %} {% trans 'User' %} {% trans 'Asset' %} {% trans 'System user'%} {% trans 'Session' %} {% trans 'Datetime' %}
{{ forloop.counter }}{{ command.input }}{{ command.user }}{{ command.asset }}{{ command.system_user }}{% trans "Goto" %}{{ command.timestamp|ts_to_date }}
{{ command.output }}
-
+
- +
-{% endblock %} +
+
+
+ + + to + +
+
+
+ +
+{% endblock %} +{% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} - - + + {% endblock %} diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index 44bbaa32b..79daeb37b 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -7,116 +7,41 @@ - {% endblock %} {% block content_left_head %} {% endblock %} +{% block table_pagination %} +{% endblock %} {% block table_search %} -
-
-
- - - to - -
-
-
- -
-
- -
-
- -
-{#
#} -{# #} -{#
#} -
-
- -
-
-
{% endblock %} -{% block table_head %} - - {% trans 'ID' %} - {% trans 'User' %} - {% trans 'Asset' %} - {% trans 'System user' %} - {% trans 'Remote addr' %} - {% trans 'Protocol' %} - {% trans 'Login from' %} - {% trans 'Command' %} - {% trans 'Date start' %} -{# {% trans 'Date last active' %}#} - {% trans 'Duration' %} - {% trans 'Action' %} -{% endblock %} - -{% block table_body %} - {% for session in session_list %} - - - - {{ forloop.counter }} - - {{ session.user }} - {{ session.asset }} - {{ session.system_user }} - {{ session.remote_addr|default:"" }} - {{ session.protocol }} - {{ session.get_login_from_display }} - {{ session.command_amount }} - - {{ session.date_start }} -{# {{ session.date_last_active }}#} - {{ session.date_start|time_util_with_seconds:session.date_end }} - - {% if session.is_finished %} - {% trans "Replay" %} - {% else %} - {% if session.protocol == 'ssh' and request.user.is_org_admin%} - {% trans "Terminate" %} - {% else %} - {% trans "Terminate" %} - {% endif %} - {% endif %} - +{% block table_container %} + + + + + + + + + + + + + + + - {% endfor %} -{% endblock %} + + + +
{% trans 'ID' %}{% trans 'User' %}{% trans 'Asset' %}{% trans 'System user' %}{% trans 'Remote addr' %}{% trans 'Protocol' %}{% trans 'Login from' %}{% trans 'Command' %}{% trans 'Date start' %}{% trans 'Duration' %}{% trans 'Action' %}
-{% block content_bottom_left %} - {% if request.user.is_org_admin %} - + + {% endblock %} + {% block custom_foot_js %} - - + + + dateToRef.datepicker(options).on("changeDate", function () { + if (!$(this).val()) { + return + } + var value = $(this).val() + ' 23:59:59'; + var date = new Date(value); + var url = table.ajax.url(); + url = setUrlParam(url, "date_to", date.toISOString()); + table.ajax.url(url); + table.ajax.reload(); + }); + }); + +}).on('click', '.btn-term', function () { + var $this = $(this); + var session_id = $this.attr('value'); + var data = [ + session_id + ]; + terminateSession(data) +}).on('click', '.btn-replay', function () { + var sessionID = $(this).data("session"); + var replayUrl = "/luna/replay/" + sessionID; + window.open(replayUrl, "height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no"); +}) +.on("click", '#session_table_filter input', function (e) { + e.preventDefault(); + e.stopPropagation(); + var offset1 = $('#session_table_filter input').offset(); + var x = offset1.left; + var y = offset1.top; + var offset = $(".search-help").parent().offset(); + x -= offset.left; + y -= offset.top; + x += 18; + y += 80; + $('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"}); + $('.dropdown-menu.search-help').show(); +}) +.on('click', '.search-item', function (e) { + e.preventDefault(); + e.stopPropagation(); + var keyword = $("#session_table_filter input"); + var value = $(this).data('value'); + var old_value = keyword.val(); + var new_value = old_value + ' ' + value + ':'; + keyword.val(new_value.trim()); + $('.dropdown-menu.search-help').hide(); + keyword.focus() +}) +.on('click', 'body', function (e) { + $('.dropdown-menu.search-help').hide() +}) +.on('click', '#btn_bulk_update', function () { + var action = $('#slct_bulk_update').val(); + var idList = table.selected; + + if (idList.length === 0) { + return false; + } + + function doTerminate() { + terminateSession(idList) + } + + function doFinishSession() { + var data = []; + $.each(idList, function (i, v) { + data.push({ + "id": v, + "is_finished": true + }) + }); + finishedSession(data) + } + switch(action) { + case 'terminate': + doTerminate(); + break; + case "finished": + doFinishSession(); + break; + default: + break; + } +}); + {% endblock %} diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 54724c7ad..95ccc85e2 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('terminal//access-key/', api.TerminalTokenApi.as_view(), name='terminal-access-key'), path('terminal/config/', api.TerminalConfig.as_view(), name='terminal-config'), + path('commands/export/', api.CommandExportApi.as_view(), name="command-export") # v2: get session's replay # path('v2/sessions//replay/', # api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}), diff --git a/apps/terminal/urls/views_urls.py b/apps/terminal/urls/views_urls.py index 3e144da85..61db49c45 100644 --- a/apps/terminal/urls/views_urls.py +++ b/apps/terminal/urls/views_urls.py @@ -25,6 +25,5 @@ urlpatterns = [ # Command view path('command/', views.CommandListView.as_view(), name='command-list'), - path('command/export/', views.CommandExportView.as_view(), name='command-export') ] diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index 143625d44..d7dace0a9 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -13,7 +13,7 @@ def get_session_asset_list(): def get_session_user_list(): - return User.objects.values_list('username', flat=True) + return User.objects.exclude(role=User.ROLE_APP).values_list('username', flat=True) def get_session_system_user_list(): diff --git a/apps/terminal/views/command.py b/apps/terminal/views/command.py index c1a5eb393..b1b4b98a5 100644 --- a/apps/terminal/views/command.py +++ b/apps/terminal/views/command.py @@ -1,107 +1,28 @@ # -*- coding: utf-8 -*- # -from django.views.generic import ListView, View -from django.conf import settings +from django.views.generic import TemplateView from django.utils.translation import ugettext as _ -from django.http import HttpResponse -from django.template import loader -import time +from django.utils import timezone -from common.mixins import DatetimeSearchMixin from common.permissions import PermissionsMixin, IsOrgAdmin, IsAuditor -from ..models import Command -from .. import utils -from ..backends import get_multi_command_storage -__all__ = ['CommandListView', 'CommandExportView'] -common_storage = get_multi_command_storage() +__all__ = ['CommandListView'] -class CommandListView(DatetimeSearchMixin, PermissionsMixin, ListView): - model = Command +class CommandListView(PermissionsMixin, TemplateView): template_name = "terminal/command_list.html" - context_object_name = 'command_list' - paginate_by = settings.DISPLAY_PER_PAGE - command = user = asset = system_user = "" - date_from = date_to = None permission_classes = [IsOrgAdmin | IsAuditor] - - def get_queryset(self): - self.command = self.request.GET.get('command', '') - self.user = self.request.GET.get("user", '') - self.asset = self.request.GET.get('asset', '') - self.system_user = self.request.GET.get('system_user', '') - filter_kwargs = dict() - filter_kwargs['date_from'] = self.date_from - filter_kwargs['date_to'] = self.date_to - if self.user: - filter_kwargs['user'] = self.user - if self.asset: - filter_kwargs['asset'] = self.asset - if self.system_user: - filter_kwargs['system_user'] = self.system_user - if self.command: - filter_kwargs['input'] = self.command - queryset = common_storage.filter(**filter_kwargs) - return queryset + default_days_ago = 5 def get_context_data(self, **kwargs): + now = timezone.now() context = { 'app': _('Sessions'), 'action': _('Command list'), - 'user_list': utils.get_session_user_list(), - 'asset_list': utils.get_session_asset_list(), - 'system_user_list': utils.get_session_system_user_list(), - 'command': self.command, - 'date_from': self.date_from, - 'date_to': self.date_to, - 'user': self.user, - 'asset': self.asset, - 'system_user': self.system_user, + 'date_from': now - timezone.timedelta(days=self.default_days_ago), + 'date_to': now, } kwargs.update(context) return super().get_context_data(**kwargs) - -class CommandExportView(DatetimeSearchMixin, PermissionsMixin, View): - model = Command - command = user = asset = system_user = action = '' - date_from = date_to = None - permission_classes = [IsOrgAdmin | IsAuditor] - - def get(self, request, *args, **kwargs): - queryset = self.get_queryset() - template = 'terminal/command_report.html' - context = { - 'queryset': queryset, - 'total_count': len(queryset), - 'now': time.time(), - } - content = loader.render_to_string(template, context, request) - content_type = 'application/octet-stream' - response = HttpResponse(content, content_type) - filename = 'command-report-{}.html'.format(int(time.time())) - response['Content-Disposition'] = 'attachment; filename="%s"' % filename - return response - - def get_queryset(self): - self.get_date_range() - self.action = self.request.GET.get('action', '') - self.command = self.request.GET.get('command', '') - self.user = self.request.GET.get("user", '') - self.asset = self.request.GET.get('asset', '') - self.system_user = self.request.GET.get('system_user', '') - filter_kwargs = dict() - filter_kwargs['date_from'] = self.date_from - filter_kwargs['date_to'] = self.date_to - if self.user: - filter_kwargs['user'] = self.user - if self.asset: - filter_kwargs['asset'] = self.asset - if self.system_user: - filter_kwargs['system_user'] = self.system_user - if self.command: - filter_kwargs['input'] = self.command - queryset = common_storage.filter(**filter_kwargs) - return queryset diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index cf4484c06..af401996a 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -from django.views.generic import ListView +from django.views.generic import ListView, TemplateView from django.views.generic.edit import SingleObjectMixin from django.utils.translation import ugettext as _ from django.utils import timezone @@ -20,78 +20,40 @@ __all__ = [ ] -class SessionListView(PermissionsMixin, DatetimeSearchMixin, ListView): +class SessionListView(PermissionsMixin, TemplateView): model = Session template_name = 'terminal/session_list.html' - context_object_name = 'session_list' - paginate_by = settings.DISPLAY_PER_PAGE - user = asset = system_user = '' date_from = date_to = None permission_classes = [IsOrgAdmin | IsAuditor] - - def get_queryset(self): - self.queryset = super().get_queryset() - self.user = self.request.GET.get('user') - self.asset = self.request.GET.get('asset') - self.system_user = self.request.GET.get('system_user') - - filter_kwargs = dict() - filter_kwargs['date_start__gt'] = self.date_from - filter_kwargs['date_start__lt'] = self.date_to - if self.user: - filter_kwargs['user'] = self.user - if self.asset: - filter_kwargs['asset'] = self.asset - if self.system_user: - filter_kwargs['system_user'] = self.system_user - if filter_kwargs: - self.queryset = self.queryset.filter(**filter_kwargs) - return self.queryset + default_days_ago = 5 def get_context_data(self, **kwargs): + now = timezone.now() context = { - 'user_list': utils.get_session_user_list(), - 'asset_list': utils.get_session_asset_list(), - 'system_user_list': utils.get_session_system_user_list(), - 'date_from': self.date_from, - 'date_to': self.date_to, - 'user': self.user, - 'asset': self.asset, - 'system_user': self.system_user, + 'date_from': now - timezone.timedelta(days=self.default_days_ago), + 'date_to': now, } kwargs.update(context) return super().get_context_data(**kwargs) class SessionOnlineListView(SessionListView): - - def get_queryset(self): - queryset = super().get_queryset().filter(is_finished=False) - return queryset - def get_context_data(self, **kwargs): context = { 'app': _('Sessions'), 'action': _('Session online list'), 'type': 'online', - 'now': timezone.now(), } kwargs.update(context) return super().get_context_data(**kwargs) class SessionOfflineListView(SessionListView): - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.filter(is_finished=True) - return queryset - def get_context_data(self, **kwargs): context = { 'app': _('Sessions'), 'action': _('Session offline'), - 'now': timezone.now(), + 'type': 'offline', } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/users/api/group.py b/apps/users/api/group.py index fc9a84928..e1abc4402 100644 --- a/apps/users/api/group.py +++ b/apps/users/api/group.py @@ -5,8 +5,11 @@ from rest_framework import generics from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination -from ..serializers import UserGroupSerializer, \ - UserGroupUpdateMemberSerializer +from ..serializers import ( + UserGroupSerializer, + UserGroupListSerializer, + UserGroupUpdateMemberSerializer, +) from ..models import UserGroup from common.permissions import IsOrgAdmin from common.mixins import IDInCacheFilterMixin @@ -23,6 +26,12 @@ class UserGroupViewSet(IDInCacheFilterMixin, BulkModelViewSet): permission_classes = (IsOrgAdmin,) pagination_class = LimitOffsetPagination + def get_serializer_class(self): + if self.action in ("list", 'retrieve') and \ + self.request.query_params.get("display"): + return UserGroupListSerializer + return self.serializer_class + class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): queryset = UserGroup.objects.all() diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 116819770..c79b9e865 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -13,7 +13,8 @@ from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination from common.permissions import ( - IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser + IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser, + CanUpdateSuperUser, ) from common.mixins import IDInCacheFilterMixin from common.utils import get_logger @@ -37,7 +38,7 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): search_fields = filter_fields queryset = User.objects.exclude(role=User.ROLE_APP) serializer_class = UserSerializer - permission_classes = (IsOrgAdmin,) + permission_classes = (IsOrgAdmin, CanUpdateSuperUser) pagination_class = LimitOffsetPagination def send_created_signal(self, users): @@ -48,13 +49,14 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): def perform_create(self, serializer): users = serializer.save() - for user in users: - if current_org and current_org.is_real(): - user.orgs.add(current_org.id) + if isinstance(users, User): + users = [users] + if current_org and current_org.is_real(): + current_org.users.add(*users) self.send_created_signal(users) def get_queryset(self): - queryset = current_org.get_org_users() + queryset = current_org.get_org_users().prefetch_related('groups') return queryset def get_permissions(self): @@ -67,29 +69,11 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): check current user has permission to handle instance (update, destroy, bulk_update, bulk destroy) """ - return not self.request.user.is_superuser and instance.is_superuser - - def destroy(self, request, *args, **kwargs): - """ - rewrite because limit org_admin destroy superuser - """ - instance = self.get_object() - if self._deny_permission(instance): - data = {'msg': _("You do not have permission.")} - return Response(data=data, status=status.HTTP_403_FORBIDDEN) - - return super().destroy(request, *args, **kwargs) - - def update(self, request, *args, **kwargs): - """ - rewrite because limit org_admin update superuser - """ - instance = self.get_object() - if self._deny_permission(instance): - data = {'msg': _("You do not have permission.")} - return Response(data=data, status=status.HTTP_403_FORBIDDEN) - - return super().update(request, *args, **kwargs) + if not self.request.user.is_superuser and instance.is_superuser: + return True + if self.request.user == instance: + return True + return False def _bulk_deny_permission(self, instances): deny_instances = [i for i in instances if self._deny_permission(i)] @@ -107,26 +91,12 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): """ rewrite because limit org_admin update superuser """ - partial = kwargs.pop('partial', False) - # restrict the update to the filtered queryset queryset = self.filter_queryset(self.get_queryset()) if self._bulk_deny_permission(queryset): data = {'msg': _("You do not have permission.")} return Response(data=data, status=status.HTTP_403_FORBIDDEN) - - serializer = self.get_serializer( - queryset, data=request.data, many=True, partial=partial, - ) - - try: - serializer.is_valid(raise_exception=True) - except Exception as e: - data = {'error': str(e)} - return Response(data=data, status=status.HTTP_400_BAD_REQUEST) - - self.perform_bulk_update(serializer) - return Response(serializer.data, status=status.HTTP_200_OK) + return super().bulk_update(request, *args, **kwargs) class UserChangePasswordApi(generics.RetrieveUpdateAPIView): @@ -174,6 +144,7 @@ class UserResetPKApi(generics.UpdateAPIView): send_reset_ssh_key_mail(user) +# 废弃 class UserUpdatePKApi(generics.UpdateAPIView): queryset = User.objects.all() serializer_class = UserPKUpdateSerializer @@ -181,7 +152,7 @@ class UserUpdatePKApi(generics.UpdateAPIView): def perform_update(self, serializer): user = self.get_object() - user.public_key = serializer.validated_data['_public_key'] + user.public_key = serializer.validated_data['public_key'] user.save() diff --git a/apps/users/forms.py b/apps/users/forms.py index 423aa81e1..96357c7b5 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -7,6 +7,7 @@ from common.utils import validate_ssh_public_key from orgs.mixins import OrgModelForm from orgs.utils import current_org from .models import User, UserGroup +from .utils import check_password_rules class UserCheckPasswordForm(forms.Form): @@ -90,6 +91,20 @@ class UserCreateUpdateFormMixin(OrgModelForm): raise forms.ValidationError(_('Not a valid ssh public key')) return public_key + def clean_password(self): + password_strategy = self.data.get('password_strategy') + # 创建-不设置密码 + if password_strategy == '0': + return + password = self.data.get('password') + # 更新-密码为空 + if password_strategy is None and not password: + return + if not check_password_rules(password): + msg = _('* Your password does not meet the requirements') + raise forms.ValidationError(msg) + return password + def save(self, commit=True): password = self.cleaned_data.get('password') otp_level = self.cleaned_data.get('otp_level') diff --git a/apps/users/migrations/0002_auto_20171225_1157_squashed_0019_auto_20190304_1459.py b/apps/users/migrations/0002_auto_20171225_1157_squashed_0019_auto_20190304_1459.py new file mode 100644 index 000000000..399605915 --- /dev/null +++ b/apps/users/migrations/0002_auto_20171225_1157_squashed_0019_auto_20190304_1459.py @@ -0,0 +1,197 @@ +# Generated by Django 2.1.7 on 2019-07-02 09:54 + +import common.utils.django +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +# Functions from the following migrations need manual copying. +# Move them and any dependencies into this file, then update the +# RunPython operations to refer to the local versions: +# users.migrations.0010_auto_20180606_1505 + +def remove_deleted_group(apps, schema_editor): + db_alias = schema_editor.connection.alias + group_model = apps.get_model("users", "UserGroup") + group_model.objects.using(db_alias).filter(is_discard=True).delete() + + +class Migration(migrations.Migration): + + replaces = [('users', '0002_auto_20171225_1157'), ('users', '0003_auto_20180101_0046'), ('users', '0004_auto_20180125_1218'), ('users', '0005_auto_20180306_1804'), ('users', '0006_auto_20180411_1135'), ('users', '0007_auto_20180419_1036'), ('users', '0008_auto_20180425_1516'), ('users', '0009_auto_20180517_1537'), ('users', '0010_auto_20180606_1505'), ('users', '0011_user_source'), ('users', '0012_auto_20180710_1641'), ('users', '0013_auto_20180807_1116'), ('users', '0014_auto_20180816_1652'), ('users', '0015_auto_20181105_1112'), ('users', '0016_auto_20181109_1505'), ('users', '0017_auto_20181123_1113'), ('users', '0018_auto_20190107_1912'), ('users', '0019_auto_20190304_1459')] + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=128, unique=True, verbose_name='Email'), + ), + migrations.AlterField( + model_name='user', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(max_length=128, unique=True, verbose_name='Username'), + ), + migrations.AlterField( + model_name='user', + name='wechat', + field=models.CharField(blank=True, max_length=128, verbose_name='Wechat'), + ), + migrations.AlterField( + model_name='user', + name='is_first_login', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='usergroup', + name='created_by', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AlterModelOptions( + name='user', + options={'ordering': ['username'], 'verbose_name': 'User'}, + ), + migrations.AlterModelOptions( + name='usergroup', + options={'ordering': ['name'], 'verbose_name': 'User group'}, + ), + migrations.RenameField( + model_name='user', + old_name='secret_key_otp', + new_name='otp_secret_key', + ), + migrations.RemoveField( + model_name='user', + name='enable_otp', + ), + migrations.AddField( + model_name='user', + name='otp_level', + field=models.SmallIntegerField(choices=[(0, 'Disable'), (1, 'Enable'), (2, 'Force enable')], default=0, verbose_name='MFA'), + ), + migrations.RemoveField( + model_name='user', + name='otp_secret_key', + ), + migrations.AddField( + model_name='user', + name='_otp_secret_key', + field=models.CharField(blank=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='usergroup', + name='name', + field=models.CharField(max_length=128, unique=True, verbose_name='Name'), + ), + migrations.RunPython( + code=remove_deleted_group, + ), + migrations.RemoveField( + model_name='usergroup', + name='discard_time', + ), + migrations.RemoveField( + model_name='usergroup', + name='is_discard', + ), + migrations.AlterField( + model_name='user', + name='date_expired', + field=models.DateTimeField(blank=True, db_index=True, default=common.utils.django.date_expired_default, null=True, verbose_name='Date expired'), + ), + migrations.AddField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius')], default='local', max_length=30, verbose_name='Source'), + ), + migrations.AddField( + model_name='loginlog', + name='mfa', + field=models.SmallIntegerField(choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')], default=2, verbose_name='MFA'), + ), + migrations.AddField( + model_name='loginlog', + name='reason', + field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist'), (4, 'Password expired')], default=0, verbose_name='Reason'), + ), + migrations.AddField( + model_name='loginlog', + name='status', + field=models.BooleanField(choices=[(True, 'Success'), (False, 'Failed')], default=True, max_length=2, verbose_name='Status'), + ), + migrations.AddField( + model_name='usergroup', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AlterField( + model_name='user', + name='last_name', + field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + ), + migrations.AlterField( + model_name='usergroup', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterUniqueTogether( + name='usergroup', + unique_together={('org_id', 'name')}, + ), + migrations.AlterField( + model_name='usergroup', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AlterField( + model_name='loginlog', + name='username', + field=models.CharField(max_length=128, verbose_name='Username'), + ), + migrations.AddField( + model_name='user', + name='date_password_last_updated', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date password last updated'), + ), + migrations.AlterField( + model_name='accesskey', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_keys', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.AlterModelTable( + name='accesskey', + table='authentication_accesskey', + ), + migrations.AlterModelTable( + name='privatetoken', + table='authentication_privatetoken', + ), + migrations.AlterModelTable( + name='loginlog', + table='audits_userloginlog', + ), + ], + state_operations=[ + migrations.DeleteModel( + name='accesskey', + ), + migrations.DeleteModel( + name='privatetoken', + ), + migrations.DeleteModel( + name='loginlog', + ), + ], + ), + ] diff --git a/apps/users/migrations/0021_auto_20190625_1104.py b/apps/users/migrations/0021_auto_20190625_1104.py new file mode 100644 index 000000000..f14e72d02 --- /dev/null +++ b/apps/users/migrations/0021_auto_20190625_1104.py @@ -0,0 +1,34 @@ +# Generated by Django 2.1.7 on 2019-06-25 03:04 + +import common.fields.model +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0020_auto_20190612_1825'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='_otp_secret_key', + field=common.fields.model.EncryptCharField(blank=True, max_length=128, null=True), + ), + migrations.AlterField( + model_name='user', + name='_private_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Private key'), + ), + migrations.AlterField( + model_name='user', + name='_public_key', + field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Public key'), + ), + migrations.AlterField( + model_name='user', + name='comment', + field=models.TextField(blank=True, null=True, verbose_name='Comment'), + ), + ] diff --git a/apps/users/migrations/0022_auto_20190625_1105.py b/apps/users/migrations/0022_auto_20190625_1105.py new file mode 100644 index 000000000..1735e8376 --- /dev/null +++ b/apps/users/migrations/0022_auto_20190625_1105.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.7 on 2019-06-25 03:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0021_auto_20190625_1104'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='_otp_secret_key', + new_name='otp_secret_key', + ), + migrations.RenameField( + model_name='user', + old_name='_private_key', + new_name='private_key', + ), + migrations.RenameField( + model_name='user', + old_name='_public_key', + new_name='public_key', + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index a0ceb6c51..dbc336443 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -17,6 +17,7 @@ from django.utils import timezone from django.shortcuts import reverse from common.utils import get_signer, date_expired_default, get_logger +from common import fields __all__ = ['User'] @@ -84,16 +85,16 @@ class User(AbstractUser): otp_level = models.SmallIntegerField( default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('MFA') ) - _otp_secret_key = models.CharField(max_length=128, blank=True, null=True) + otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) # Todo: Auto generate key, let user download - _private_key = models.CharField( - max_length=5000, blank=True, verbose_name=_('Private key') + private_key = fields.EncryptTextField( + blank=True, null=True, verbose_name=_('Private key') ) - _public_key = models.CharField( - max_length=5000, blank=True, verbose_name=_('Public key') + public_key = fields.EncryptTextField( + blank=True, null=True, verbose_name=_('Public key') ) comment = models.TextField( - max_length=200, blank=True, verbose_name=_('Comment') + blank=True, null=True, verbose_name=_('Comment') ) is_first_login = models.BooleanField(default=True) date_expired = models.DateTimeField( @@ -141,14 +142,6 @@ class User(AbstractUser): def can_update_password(self): return self.is_local - @property - def otp_secret_key(self): - return signer.unsign(self._otp_secret_key) - - @otp_secret_key.setter - def otp_secret_key(self, item): - self._otp_secret_key = signer.sign(item) - def check_otp(self, code): from ..utils import check_otp_code return check_otp_code(self.otp_secret_key, code) @@ -161,13 +154,13 @@ class User(AbstractUser): Check if the user's ssh public key is valid. This function is used in base.html. """ - if self._public_key: + if self.public_key: return True return False @property def groups_display(self): - return ' '.join(self.groups.all().values_list('name', flat=True)) + return ' '.join([group.name for group in self.groups.all()]) @property def role_display(self): @@ -190,22 +183,6 @@ class User(AbstractUser): return True return False - @property - def private_key(self): - return signer.unsign(self._private_key) - - @private_key.setter - def private_key(self, private_key_raw): - self._private_key = signer.sign(private_key_raw) - - @property - def public_key(self): - return signer.unsign(self._public_key) - - @public_key.setter - def public_key(self, public_key_raw): - self._public_key = signer.sign(public_key_raw) - @property def public_key_obj(self): class PubKey(object): @@ -249,6 +226,16 @@ class User(AbstractUser): def is_auditor(self): return self.role == 'Auditor' + @property + def is_common_user(self): + if self.is_org_admin: + return False + if self.is_auditor: + return False + if self.is_app: + return False + return True + @property def is_app(self): return self.role == 'App' @@ -299,7 +286,6 @@ class User(AbstractUser): self.role = 'Admin' self.is_active = True super().save(*args, **kwargs) - self.expire_user_cache() @property def private_token(self): @@ -364,7 +350,7 @@ class User(AbstractUser): def generate_reset_token(self): letter = string.ascii_letters + string.digits - token =''.join([random.choice(letter) for _ in range(50)]) + token = ''.join([random.choice(letter) for _ in range(50)]) self.set_cache(token) return token @@ -448,26 +434,8 @@ class User(AbstractUser): def delete(self, using=None, keep_parents=False): if self.pk == 1 or self.username == 'admin': return - self.expire_user_cache() return super(User, self).delete() - def expire_user_cache(self): - key = self.user_cache_key_prefix.format(self.id) - cache.delete(key) - - @classmethod - def get_user_or_from_cache(cls, uid): - key = cls.user_cache_key_prefix.format(uid) - user = cache.get(key) - if user: - return user - try: - user = cls.objects.get(id=uid) - cache.set(key, user, 3600) - except cls.DoesNotExist: - user = None - return user - class Meta: ordering = ['username'] verbose_name = _("User") diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/v1.py index 672d7b62a..63fe52699 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/v1.py @@ -6,10 +6,19 @@ from rest_framework import serializers from common.utils import get_signer, validate_ssh_public_key from common.mixins import BulkSerializerMixin +from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from orgs.mixins import BulkOrgResourceModelSerializer from ..models import User, UserGroup + +__all__ = [ + 'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer', + 'UserGroupSerializer', 'UserGroupListSerializer', + 'UserGroupUpdateMemberSerializer', 'ChangeUserPasswordSerializer' +] + + signer = get_signer() @@ -19,13 +28,16 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): model = User list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'username', 'email', 'groups', 'groups_display', + 'id', 'name', 'username', 'password', 'email', 'public_key', + 'groups', 'groups_display', 'role', 'role_display', 'wechat', 'phone', 'otp_level', 'comment', 'source', 'source_display', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'date_password_last_updated', 'date_expired', 'avatar_url', ] extra_kwargs = { + 'password': {'write_only': True, 'required': False}, + 'public_key': {'write_only': True}, 'groups_display': {'label': _('Groups name')}, 'source_display': {'label': _('Source name')}, 'is_first_login': {'label': _('Is first login'), 'read_only': True}, @@ -36,14 +48,45 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): 'created_by': {'read_only': True}, 'source': {'read_only': True} } + def validate_role(self, value): + request = self.context.get('request') + if not request.user.is_superuser and value != User.ROLE_USER: + role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER] + msg = _("Role limit to {}".format(role_display)) + raise serializers.ValidationError(msg) + return value + + @staticmethod + def validate_password(value): + from ..utils import check_password_rules + if not check_password_rules(value): + msg = _('Password does not match security rules') + raise serializers.ValidationError(msg) + return value + + @staticmethod + def change_password_to_raw(validated_data): + password = validated_data.pop('password', None) + if password: + validated_data['password_raw'] = password + return validated_data + + def create(self, validated_data): + validated_data = self.change_password_to_raw(validated_data) + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data = self.change_password_to_raw(validated_data) + return super().update(instance, validated_data) + class UserPKUpdateSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', '_public_key'] + fields = ['id', 'public_key'] @staticmethod - def validate__public_key(value): + def validate_public_key(value): if not validate_ssh_public_key(value): raise serializers.ValidationError(_('Not a valid ssh public key')) return value @@ -66,7 +109,7 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): model = UserGroup list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'org_id', 'name', 'users', 'comment', 'date_created', + 'id', 'name', 'users', 'comment', 'date_created', 'created_by', ] extra_kwargs = { @@ -74,6 +117,10 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): } +class UserGroupListSerializer(UserGroupSerializer): + users = StringManyToManyField(many=True, read_only=True) + + class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all()) diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index c2839cad9..c5b5d75bc 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -2,27 +2,27 @@ {% load i18n static %} {% block table_search %} + +
{% endblock %} {% block table_container %} @@ -79,7 +79,7 @@ function initTable() { } }} ], - ajax_url: '{% url "api-users:user-group-list" %}', + ajax_url: '{% url "api-users:user-group-list" %}?display=1', columns: [{data: function(){return ""}}, {data: "name" }, {data: "users"}, {data: "comment"}, {data: "id" }], order: [], diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index cf2a764b4..78a29fec2 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -1,28 +1,28 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} - + {% endblock %} {% block table_container %} diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index 49478de60..7a06df4c9 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -217,33 +217,9 @@ {% endblock %} {% block custom_foot_js %}