Unverified Commit 52d52861 authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #2893 from jumpserver/dev

Dev
parents 9ca4a8c9 0809916b
......@@ -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
......
......@@ -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):
"""
......
# -*- 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):
......
......@@ -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):
......
......@@ -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
"""
......
......@@ -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:
......
......@@ -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: <Asset>对象
:param latest: 是否是最新记录
:param prefer: 优先使用
:param prefer_id: 使用id
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
"""
pass
......
......@@ -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)
......
......@@ -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()
......
# -*- 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")),
)
......@@ -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
......
# -*- 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
# -*- 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'},
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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',
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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'),
),
]
# -*- 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),
]
# 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')},
),
]
# 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')},
),
]
......@@ -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),
]
# 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'),
),
]
# 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',
),
]
# 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'),
]
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 *
......@@ -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, ''
if warning:
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
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:
......
......@@ -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):
......
......@@ -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_{}"
_prefer = "system_user"
@property
def password(self):
if self._password:
return signer.unsign(self._password)
else:
return None
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
@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)
_prefer = "system_user"
@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()
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 get_connectivity_of(self, asset):
i = self.generate_id_with_asset(asset)
key = self.CONNECTIVITY_CACHE_KEY.format(i)
return cache.get(key)
@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 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_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
def load_specific_asset_auth(self, asset):
@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
# -*- 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']
@property
def children(self):
if self._children:
return self._children
pattern = r'^{0}:[0-9]+$'.format(self.key)
return Node.objects.filter(key__regex=pattern)
def __str__(self):
return self.full_value
@children.setter
def children(self, value):
self._children = value
def __eq__(self, other):
if not other:
return False
return self.id == other.id
@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 __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]
def get_children(self, with_self=False):
children = list(self.children)
if with_self:
children.append(self)
return children
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 get_all_children(self, with_self=False):
children = self.all_children
if with_self:
children = list(children)
children.append(self)
return children
def __lt__(self, other):
return not self.__gt__(other)
@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 name(self):
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
@classmethod
def refresh_nodes(cls):
from ..utils import NodeUtil
util = NodeUtil(with_assets_amount=True)
util.set_assets_amount()
util.set_full_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+'*')
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))
......@@ -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')]
......
......@@ -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()
# -*- 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):
......
# -*- 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 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
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):
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):
......
......@@ -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
......
......@@ -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
# -*- 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
# -*- 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
......
......@@ -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):
"""
......
......@@ -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):
......
......@@ -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: <AuthBook>对象
: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
......
......@@ -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);
......
......@@ -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('<i class="fa fa-circle text-navy"></i>')
} else if (cellData == 0) {
$(td).html('<i class="fa fa-circle text-danger"></i>')
var innerHtml = "";
if (cellData.status == 1) {
innerHtml = '<i class="fa fa-circle text-navy"></i>'
} else if (cellData.status == 0) {
innerHtml = '<i class="fa fa-circle text-danger"></i>'
} else {
$(td).html('<i class="fa fa-circle text-warning"></i>')
innerHtml = '<i class="fa fa-circle text-warning"></i>'
}
var date = new Date(cellData.datetime);
var dateManual = date.toLocaleString();
var dataContent = testDatetime + dateManual;
innerHtml = "<a data-toggle='popover' data-content='" + dataContent + "'" + 'data-placement="auto bottom"' + ">" + innerHtml + "</a>";
$(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 {
......
{% load static %}
{% load i18n %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
{# <link href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" rel="stylesheet">#}
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<style type="text/css">
div#rMenu {
position: absolute;
visibility: hidden;
text-align: left;
{#top: 100%;#}
top: 0;
left: 0;
z-index: 999;
{#float: left;#}
padding: 0 0;
margin: 2px 0 0;
list-style: none;
background-clip: padding-box;
}
.dataTables_wrapper .dataTables_processing {
opacity: .9;
border: none;
}
div#rMenu li{
margin: 1px 0;
cursor: pointer;
list-style: none outside none;
}
.dropdown a:hover {
background-color: #f1f1f1
}
</style>
<div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager" id="tree-node-id">
<div id="{% block treeID %}nodeTree{% endblock %}" class="ztree">
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
<div id="rMenu">
<ul class="dropdown-menu menu-actions">
<li class="divider"></li>
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
</ul>
</div>
<script>
var zTree, rMenu = null;
var current_node_id = null;
var current_node = null;
var showMenu = false;
var treeUrl = '{% url 'api-assets:node-children-tree' %}?assets=0';
// options:
// {
// "onSelected": func,
// "showAssets": false,
// "beforeAsync": func()
// "showMenu": false,
// "otherMenu": "",
// "showAssets": false,
// }
function initNodeTree(options) {
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
async: {
enable: true,
url: treeUrl,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onRightClick: OnRightClick,
beforeClick: beforeClick,
onRename: onRename,
onSelected: options.onSelected || defaultCallback("On selected"),
beforeDrag: beforeDrag,
onDrag: onDrag,
beforeDrop: beforeDrop,
onDrop: onDrop,
beforeAsync: options.beforeAsync || defaultCallback("Before async")
}
};
if (options.showAssets) {
treeUrl = setUrlParam(treeUrl, 'assets', '1')
}
$.get(treeUrl, function(data, status){
zNodes = data;
zTree = $.fn.zTree.init($("#nodeTree"), setting, zNodes);
rootNodeAddDom(zTree, function () {
treeUrl = setUrlParam(treeUrl, 'refresh', '1');
initTree();
treeUrl = setUrlParam(treeUrl, 'refresh', '0')
});
});
if (options.showMenu) {
showMenu = true;
rMenu = $("#rMenu");
}
if (options.otherMenu) {
$(".menu-actions").append(options.otherMenu)
}
return zTree
}
function addTreeNode() {
hideRMenu();
var parentNode = zTree.getSelectedNodes()[0];
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
pId: parentNode.id,
meta: {
"node": data
}
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
}
});
}
function removeTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({
url: url,
method: "DELETE",
success: function () {
zTree.removeNode(current_node);
}
});
}
}
function editTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node) {
current_node.name = current_node.meta.node.value;
}
zTree.editName(current_node);
}
function OnRightClick(event, treeId, treeNode) {
if (!showMenu) {
return
}
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
zTree.cancelSelectedNode();
showRMenu("root", event.clientX, event.clientY);
} else if (treeNode && !treeNode.noR) {
zTree.selectNode(treeNode);
showRMenu("node", event.clientX, event.clientY);
}
}
function showRMenu(type, x, y) {
var offset = $("#tree-node-id").offset();
x -= offset.left;
y -= offset.top;
x += document.body.scrollLeft;
y += document.body.scrollTop + document.documentElement.scrollTop;
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
$("#rMenu ul").show();
$("body").bind("mousedown", onBodyMouseDown);
}
function beforeClick(treeId, treeNode, clickFlag) {
return true;
}
function hideRMenu() {
if (rMenu) rMenu.css({"visibility": "hidden"});
$("body").unbind("mousedown", onBodyMouseDown);
}
function onBodyMouseDown(event){
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
rMenu.css({"visibility" : "hidden"});
}
}
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
}
APIUpdateAttr({
url: url,
body: JSON.stringify(data),
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')';
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
})
}
function beforeDrag() {
return true
}
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = [];
$.each(treeNodes, function (index, value) {
treeNodesNames.push(value.name);
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
return confirm(msg);
}
function onDrag(event, treeId, treeNodes) {
}
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.meta.node.id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
var body = {nodes: treeNodesIds};
APIUpdateAttr({
url: the_url,
method: "PUT",
body: JSON.stringify(body)
})
}
function defaultCallback(action) {
function logging() {
console.log(action)
}
return logging
}
$(document).ready(function () {
})
.on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
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')
}
APIUpdateAttr({
url: the_url,
method: "GET",
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
if (!current_node_id) {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
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')
}
APIUpdateAttr({
url: the_url,
method: "GET",
success: success,
flash_message: false
});
})
.on('click', '.btn-show-current-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_all_asset').css('display', 'inline-block');
setCookie('show_current_asset', '1');
location.reload()
})
.on('click', '.btn-show-all-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_current_asset').css('display', 'inline-block');
setCookie('show_current_asset', '');
location.reload();
})
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '#menu_refresh_assets_amount', function () {
hideRMenu();
var url = "{% url 'api-assets:refresh-assets-amount' %}";
APIUpdateAttr({
'url': url,
'method': 'GET'
});
window.location.reload();
})
</script>
\ No newline at end of file
......@@ -46,7 +46,7 @@
</div>
</div>
</div>
<div class="col-sm-3" style="padding-left: 0;padding-right: 0">
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% 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();
})
</script>
{% endblock %}
......@@ -75,27 +75,29 @@ function initTable() {
}},
{targets: 4, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
var data = cellData.reachable;
if (data !== 0) {
innerHtml = "<span class='text-navy'>" + data + "</span>";
} else {
innerHtml = "<span>" + cellData + "</span>";
innerHtml = "<span>" + data + "</span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
$(td).html(innerHtml)
}},
{targets: 5, createdCell: function (td, cellData) {
var data = cellData.unreachable;
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
if (data !== 0) {
innerHtml = "<span class='text-danger'>" + data + "</span>";
} else {
innerHtml = "<span>" + cellData + "</span>";
innerHtml = "<span>" + data + "</span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');
}},
{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 () {
......
......@@ -70,11 +70,7 @@
</tr>
<tr>
<td>{% trans 'Protocol' %}</td>
<td>
{% for protocol in asset.protocols.all %}
<b>{{ protocol }}</b>
{% endfor %}
</td>
<td>{{ asset.protocols }}</td>
</tr>
<tr>
<td>{% trans 'Admin user' %}:</td>
......
......@@ -49,15 +49,7 @@
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
<div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager ">
<div id="assetTree" class="ztree">
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
{% include 'assets/_node_tree.html' %}
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
......@@ -132,26 +124,7 @@
</div>
</div>
<div id="rMenu">
<ul class="dropdown-menu">
<li class="divider"></li>
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
<li class="divider"></li>
<li id="menu_asset_add" class="btn-add-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-copy"></i> {% trans 'Add assets to node' %}</a></li>
<li id="menu_asset_move" class="btn-move-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-cut"></i> {% trans 'Move assets to node' %}</a></li>
<li class="divider"></li>
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
<li class="divider"></li>
<li id="menu_refresh_assets_amount" class="btn-refresh-assets-amount" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh all node assets amount' %}</a></li>
<li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
{# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#}
</ul>
</div>
{% 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 %}
<script>
var zTree, rMenu, asset_table, show = 0;
var asset_table, show = 0;
var update_node_action = "";
var current_node_id = null;
var current_node = null;
var testDatetime = "{% trans 'Test datetime: ' %}";
function initTable() {
var options = {
ele: $('#asset_list_table'),
......@@ -176,16 +150,21 @@ function initTable() {
{targets: 3, createdCell: function (td, cellData, rowData) {
$(td).html(rowData.hardware_info)
}},
{targets: 4, createdCell: function (td, cellData) {
if (cellData === 1){
$(td).html('<i class="fa fa-circle text-navy"></i>')
} else if (cellData === 0) {
$(td).html('<i class="fa fa-circle text-danger"></i>')
{targets: 4, createdCell: function (td, cellData, rowData) {
var innerHtml = "";
if (cellData.status == 1) {
innerHtml = '<i class="fa fa-circle text-navy"></i>'
} else if (cellData.status == 0) {
innerHtml = '<i class="fa fa-circle text-danger"></i>'
} else {
$(td).html('<i class="fa fa-circle text-warning"></i>')
innerHtml = '<i class="fa fa-circle text-warning"></i>'
}
var date = new Date(cellData.datetime);
var dateManual = date.toLocaleString();
var dataContent = testDatetime + dateManual;
innerHtml = "<a data-toggle='popover' data-content='" + dataContent + "'" + 'data-placement="auto bottom"' + ">" + innerHtml + "</a>";
$(td).html(innerHtml);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:asset-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
......@@ -203,239 +182,24 @@ function initTable() {
asset_table = jumpserver.initServerSideDataTable(options);
return asset_table
}
function addTreeNode() {
hideRMenu();
var parentNode = zTree.getSelectedNodes()[0];
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
pId: parentNode.id,
meta: {
"node": data
}
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
}
});
}
function removeTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({
url: url,
method: "DELETE",
success: function () {
zTree.removeNode(current_node);
}
});
}
}
function editTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node) {
current_node.name = current_node.meta.node.value;
}
zTree.editName(current_node);
}
function OnRightClick(event, treeId, treeNode) {
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
zTree.cancelSelectedNode();
showRMenu("root", event.clientX, event.clientY);
} else if (treeNode && !treeNode.noR) {
zTree.selectNode(treeNode);
showRMenu("node", event.clientX, event.clientY);
}
}
function showRMenu(type, x, y) {
$("#rMenu ul").show();
x -= 220;
x += document.body.scrollLeft;
y += document.body.scrollTop+document.documentElement.scrollTop;
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
$("body").bind("mousedown", onBodyMouseDown);
}
function beforeClick(treeId, treeNode, clickFlag) {
return true;
}
function hideRMenu() {
if (rMenu) rMenu.css({"visibility": "hidden"});
$("body").unbind("mousedown", onBodyMouseDown);
}
function onBodyMouseDown(event){
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
rMenu.css({"visibility" : "hidden"});
}
}
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
}
APIUpdateAttr({
url: url,
body: JSON.stringify(data),
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
})
}
function onSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
zTree.expandNode(current_node, true);
var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url);
asset_table.ajax.reload();
}
function selectQueryNode() {
// TODO: 是否应该添加
// 暂时忽略之前选中的内容
return
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
var node_id;
if (query_node_id !== null) {
node_id = query_node_id
} else if (cookie_node_id !== null) {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("node_id", node_id, null);
if (node){
zTree.selectNode(node[0]);
}
}
function beforeDrag() {
return true
}
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = [];
$.each(treeNodes, function (index, value) {
treeNodesNames.push(value.name);
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
return confirm(msg);
}
function onDrag(event, treeId, treeNodes) {
}
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.meta.node.id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
var body = {nodes: treeNodesIds};
APIUpdateAttr({
url: the_url,
method: "PUT",
body: JSON.stringify(body)
function initTree() {
initNodeTree({
onSelected: onNodeSelected,
showMenu: true,
otherMenu: `
<li class="divider"></li>
<li id="menu_asset_add" class="btn-add-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-copy"></i> {% trans 'Add assets to node' %}</a></li>
<li id="menu_asset_move" class="btn-move-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-cut"></i> {% trans 'Move assets to node' %}</a></li>
<li class="divider"></li>
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
<li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
`
})
}
function initTree() {
if (zTree) {
return
}
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
var showCurrentAsset = getCookie('show_current_asset');
if (!showCurrentAsset) {
url += '1'
}
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onRightClick: OnRightClick,
beforeClick: beforeClick,
onRename: onRename,
onSelected: onSelected,
beforeDrag: beforeDrag,
onDrag: onDrag,
beforeDrop: beforeDrop,
onDrop: onDrop
}
};
var zNodes = [];
zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
rMenu = $("#rMenu");
}
function toggle() {
if (show === 0) {
......@@ -452,6 +216,18 @@ function toggle() {
}
}
function onNodeSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
zTree.expandNode(current_node, true);
var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url);
asset_table.ajax.reload();
}
$(document).ready(function(){
initTable();
initTree();
......@@ -549,69 +325,7 @@ $(document).ready(function(){
}
window.open(url, '_self');
})
.on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
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')
}
APIUpdateAttr({
url: the_url,
method: "GET",
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
if (!current_node_id) {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
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')
}
APIUpdateAttr({
url: the_url,
method: "GET",
success: success,
flash_message: false
});
})
.on('click', '.btn-show-current-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_all_asset').css('display', 'inline-block');
setCookie('show_current_asset', '1');
location.reload();
})
.on('click', '.btn-show-all-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_current_asset').css('display', 'inline-block');
setCookie('show_current_asset', '');
location.reload();
})
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '#menu_refresh_assets_amount', function () {
hideRMenu();
var url = "{% url 'api-assets:refresh-assets-amount' %}";
APIUpdateAttr({
'url': url,
'method': 'GET'
});
window.location.reload();
})
.on('click', '.btn_asset_delete', function () {
var $this = $(this);
var $data_table = $("#asset_list_table").DataTable();
......
......@@ -169,40 +169,6 @@ $(document).ready(function () {
initAssetUserTable();
})
.on('click', '.btn-push', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success: success
});
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success: success
});
})
.on('click', '.btn-remove-from-node', function() {
var $this = $(this);
var $tr = $this.closest('tr');
......@@ -230,6 +196,23 @@ $(document).ready(function () {
});
updateSystemUserNode(nodes);
})
.on('click', '.btn-push', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success: success
});
})
.on('click', '.btn-push-auth', function () {
var $this = $(this);
var asset_id = $this.data('asset');
......@@ -250,6 +233,23 @@ $(document).ready(function () {
error: error
})
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success: success
});
})
</script>
......
......@@ -46,7 +46,7 @@
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
<input type="checkbox" id="check_all" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
......@@ -80,28 +80,30 @@ function initTable() {
}},
{targets: 6, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
var data = cellData.reachable;
if (data !== 0) {
innerHtml = "<span class='text-navy'>" + data + "</span>";
} else {
innerHtml = "<span>" + cellData + "</span>";
innerHtml = "<span>" + data + "</span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
$(td).html(innerHtml)
}},
{targets: 7, createdCell: function (td, cellData) {
var data = cellData.unreachable;
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
if (data !== 0) {
innerHtml = "<span class='text-danger'>" + data + "</span>";
} else {
innerHtml = "<span>" + cellData + "</span>";
innerHtml = "<span>" + data + "</span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + data + '">' + innerHtml + '</span>');
}},
{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,17 +114,17 @@ function initTable() {
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 10, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.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()
};
......
......@@ -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/<uuid:pk>/', views.AssetDetailView.as_view(), name='asset-detail'),
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
......
# ~*~ 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)
)
......@@ -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)
......
# 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)
......@@ -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'),
......
......@@ -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)
......
......@@ -4,3 +4,4 @@
from .backends import *
from .middleware import *
from .utils import *
from .decorator import *
......@@ -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
)
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))
......
# 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
......@@ -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)
......@@ -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()
def new_realm(self):
return KeycloakRealm(
self._openid_client = None
self._realm = None
self._openid_connect_client = None
@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(
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 openid_connect
return self._openid_connect_client
def new_openid_client(self):
@property
def openid_client(self):
"""
:rtype: keycloak.keycloak_openid.KeycloakOpenID
"""
return 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
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_url(self, name):
return self.openid_connect_client.get_url(name=name)
:param str username: authentication username
:param str password: authentication password
:return: OpenIDTokenProfile
"""
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
@ssl_verification
def get_userinfo(self, token):
user_info = self.openid_connect_client.userinfo(token=token)
return user_info
@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
......@@ -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',
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(
......
......@@ -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
......
......@@ -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
......@@ -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
......
# -*- 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
# -*- coding: utf-8 -*-
#
from werkzeug.local import Local
thread_local = Local()
def _find(attr):
return getattr(thread_local, attr, None)
# -*- coding: utf-8 -*-
#
from .models import *
from .serializers import *
from .api import *
from .views import *
# -*- 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
# -*- 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()
# 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
# -*- 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
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)
__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin']
class BulkSerializerMixin(object):
......@@ -108,13 +27,12 @@ class BulkSerializerMixin(object):
if all((isinstance(self.root, BulkListSerializer),
id_attr,
request_method in ('PUT', 'PATCH'))):
id_field = self.fields[id_attr]
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
......@@ -174,61 +92,3 @@ class BulkListSerializerMixin(object):
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
# -*- 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)
......@@ -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
......@@ -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)
......
# -*- 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)
# -*- 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)
......@@ -112,7 +112,6 @@ def to_dict(data):
@register.filter
def sort(data):
print(data)
return sorted(data)
......
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)
......@@ -130,16 +130,13 @@ def get_short_uuid_str():
def is_uuid(seq):
if isinstance(seq, str):
if UUID_PATTERN.match(seq):
if isinstance(seq, uuid.UUID):
return True
else:
return False
else:
for s in seq:
if not is_uuid(s):
return False
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)
......@@ -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):
......
......@@ -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,
}
......
# -*- coding: utf-8 -*-
#
VERSION = '1.5.0'
VERSION = '1.5.1'
......@@ -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
......
......@@ -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
......
......@@ -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/<str:lang>/', 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'),
]
# -*- 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():
......
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())})
......@@ -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 <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\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
#: 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:44 assets/templates/assets/asset_create.html:24
msgid "Protocols"
msgstr "协议组"
#: assets/serializers/asset.py:71
#: assets/serializers/asset.py:72
msgid "Hardware info"
msgstr "硬件信息"
#: assets/serializers/asset.py:72 assets/serializers/asset_user.py:29
#: assets/templates/assets/_asset_user_list.html:18
msgid "Connectivity"
msgstr "连接"
#: assets/serializers/asset.py:73 orgs/mixins.py:223
#: assets/serializers/asset.py:73 orgs/mixins/serializers.py:26
msgid "Org name"
msgstr "组织名称"
......@@ -1232,116 +1230,111 @@ 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
#: users/templates/users/user_password_update.html:46
#: users/templates/users/user_profile.html:68
#: users/templates/users/user_profile_update.html:43
#: users/templates/users/user_pubkey_update.html:43
msgid "Public key"
msgstr "ssh公钥"
#: assets/serializers/asset_user.py:43
#: assets/serializers/asset_user.py:32
msgid "Backend"
msgstr "后端"
#: assets/serializers/asset_user.py:65
#: assets/serializers/asset_user.py:57
msgid "private key invalid"
msgstr "密钥不合法"
#: assets/serializers/system_user.py:22
msgid "Unreachable assets"
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
#: users/templates/users/user_pubkey_update.html:43
msgid "Public key"
msgstr "ssh公钥"
#: assets/serializers/system_user.py:25
msgid "Reachable assets"
msgstr "可连接资产"
#: assets/serializers/node.py:33
msgid "The same level node name cannot be the same"
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 ""
"<div>Luna is a separately deployed program, you need to deploy Luna, coco, "
"configure nginx for url distribution,</div> </div>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: {} <br /> <br />{}"
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> <br> Tips: Using 5 digits linux "
"crontab expressions <min hour day month week> (<a href='https://tool.lu/"
......@@ -5397,10 +5389,6 @@ msgstr "定期执行"
msgid "Password rules"
msgstr "密码规则"
#: xpack/plugins/change_auth_plan/models.py:213
msgid "For security, do not change {} user's password"
msgstr "为了安全,禁止更改 {} 用户的密码"
#: xpack/plugins/change_auth_plan/models.py:217
msgid "Assets is empty, please add the asset"
msgstr "资产为空,请添加资产"
......@@ -5943,6 +5931,33 @@ msgstr "密码匣子"
msgid "vault create"
msgstr "创建"
#~ msgid "* For security, please do not change root user's password"
#~ msgstr "* 为了安全,请不要更改root用户的密码"
#~ msgid "Interface"
#~ msgstr "界面"
#~ msgid "already exists"
#~ msgstr "已经存在"
#~ msgid "Invalid file."
#~ msgstr "文件不合法"
#~ msgid "Refresh all node assets amount"
#~ msgstr "刷新所有节点资产数量"
#~ msgid "Rename failed, do not change the root node name"
#~ msgstr "重命名失败,不能更改root节点的名称"
#~ msgid "Failed to update SSH public key."
#~ msgstr "更新密钥失败"
#~ msgid "Unreachable assets"
#~ msgstr "不可达资产"
#~ msgid "Reachable assets"
#~ msgstr "可连接资产"
#~ msgid "User does not exist"
#~ msgstr "用户不存在"
......
......@@ -20,7 +20,7 @@ class CommandExecutionViewSet(viewsets.ModelViewSet):
)
def check_permissions(self, request):
if not settings.SECURITY_COMMAND_EXECUTION:
if not settings.SECURITY_COMMAND_EXECUTION and request.user.is_common_user:
return self.permission_denied(request, "Command execution disabled")
return super().check_permissions(request)
......
......@@ -2,7 +2,6 @@
#
from .ansible.inventory import BaseInventory
from assets.utils import get_assets_by_id_list, get_system_user_by_id
from common.utils import get_logger
......
......@@ -47,7 +47,7 @@ def run_command_execution(cid, **kwargs):
try:
execution.run()
except SoftTimeLimitExceeded:
print("HLLL")
logger.error("Run time out")
else:
logger.error("Not found the execution id: {}".format(cid))
......
......@@ -135,9 +135,7 @@ function getSelectedAssetsNode() {
var assetsNode = [];
nodes.forEach(function (node) {
if (node.meta.type === 'asset' && !node.isHidden) {
var protocols = $.map(node.meta.asset.protocols, function (v) {
return v.name
});
var protocols = node.meta.asset.protocols;
if (assetsNodeId.indexOf(node.id) === -1 && protocols.indexOf("ssh") > -1) {
assetsNodeId.push(node.id);
assetsNode.push(node)
......
......@@ -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
......
# -*- 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
# -*- coding: utf-8 -*-
#
from .models import *
from .serializers import *
from .forms import *
from .api import *
# -*- 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
# -*- 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()
# -*- coding: utf-8 -*-
#
from werkzeug.local import Local
import traceback
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 ..utils import (
set_current_org, get_current_org, current_org,
)
from .models import Organization
from ..models import Organization
logger = get_logger(__file__)
tl = Local()
__all__ = [
'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm',
'RootOrgViewMixin', 'OrgMembershipSerializerMixin',
'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin',
'BulkOrgResourceSerializerMixin', 'BulkOrgResourceModelSerializer',
'OrgManager', 'OrgModelMixin',
]
......@@ -35,38 +24,24 @@ 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:
_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():
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
#
# lines = traceback.format_stack()
# print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
# for line in lines[-10:-1]:
# print(line)
# print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
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)
queryset = queryset.filter(**kwargs)
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'
......@@ -76,35 +51,23 @@ class OrgManager(models.Manager):
def set_current_org(self, org):
if isinstance(org, str):
org = Organization.objects.get(name=org)
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)
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():
if current_org is not None 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
......@@ -133,6 +96,7 @@ class OrgModelMixin(models.Model):
"""
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:
......@@ -150,98 +114,3 @@ class OrgModelMixin(models.Model):
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
# -*- 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)
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
......
......@@ -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
# -*- 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
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(partial(_find, 'current_org'))
current_org = LocalProxy(get_current_org)
......@@ -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:
......
......@@ -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 *
......@@ -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):
......
......@@ -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', '')
......
# -*- 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
......@@ -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)
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
_object = {}
if su not in granted_system_users:
_object['actions'] = 0
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
# -*- 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)
# -*- 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"
......@@ -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')
# 前端渲染优化, 防止过多资产
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,9 +73,6 @@ 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"),
......
......@@ -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
......
......@@ -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)
]
# -*- 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')]),
),
]
......@@ -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)
]
# -*- 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'),
),
]
# -*- 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),
]
# -*- 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'),
),
]
# 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),
]
# 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(),
),
]
# 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'),
]
# 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'),
),
]
# 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'},
),
]
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():
......
......@@ -2,4 +2,5 @@
#
from .asset_permission import *
from .user_permission import *
from .remote_app_permission import *
......@@ -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'
# ]
# -*- 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)
......@@ -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)
......
......@@ -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);
})
</script>
{% endblock %}
\ No newline at end of file
......@@ -24,15 +24,7 @@
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
<div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager ">
<div id="assetTree" class="ztree">
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
{% include 'assets/_node_tree.html' %}
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
......@@ -86,7 +78,7 @@
<script>
var zTree, table, show = 0;
function onSelected(event, treeNode) {
function onNodeSelected(event, treeNode) {
setCookie('node_selected', treeNode.id);
var url = table.ajax.url();
if (treeNode.meta.type === 'node') {
......@@ -102,7 +94,7 @@ function onSelected(event, treeNode) {
}
function beforeAsync(treeId, treeNode) {
function beforeNodeAsync(treeId, treeNode) {
if (treeNode) {
return treeNode.meta.type === 'node'
}
......@@ -188,7 +180,7 @@ function initTable() {
$(td).html(update_btn + del_btn);
}}
],
ajax_url: '{% url "api-perms:asset-permission-list" %}',
ajax_url: '{% url "api-perms:asset-permission-list" %}?display=1',
columns: [
{data: "id"}, {data: "name"}, {data: "users"},
{data: "user_groups"}, {data: "assets"},
......@@ -204,28 +196,12 @@ function initTable() {
function initTree() {
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
async: {
enable: true,
url: "{% url 'api-assets:node-children-tree' %}?assets=1",
autoParam:["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: {
onSelected: onSelected,
beforeAsync: beforeAsync
}
};
zTree = $.fn.zTree.init($("#assetTree"), setting);
initNodeTree({
onSelected: onNodeSelected,
beforeAsync: beforeNodeAsync,
showMenu: false,
showAssets: true,
})
}
function toggle() {
......
......@@ -7,110 +7,64 @@ from .. import api
app_name = 'perms'
router = routers.DefaultRouter()
router.register('actions', api.ActionViewSet, 'action')
router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission')
asset_permission_urlpatterns = [
# 查询某个用户授权的资产和资产组
path('user/<uuid:pk>/assets/',
api.UserGrantedAssetsApi.as_view(), name='user-assets'),
path('user/assets/', api.UserGrantedAssetsApi.as_view(),
name='my-assets'),
path('user/<uuid:pk>/nodes/',
api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('user/nodes/', api.UserGrantedNodesApi.as_view(),
name='my-nodes'),
path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(),
name='my-node-children'),
path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/',
api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('user/nodes/<uuid:node_id>/assets/',
api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
path('user/<uuid:pk>/nodes-assets/',
api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(),
name='my-nodes-assets'),
path('user/<uuid:pk>/nodes-assets/tree/',
api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(),
name='my-nodes-assets-as-tree'),
path('user/<uuid:pk>/assets/', api.UserGrantedAssetsApi.as_view()),
path('users/<uuid:pk>/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'),
path('user/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'),
path('user/<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('user/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'),
path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), name='my-node-children'),
path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('user/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
path('user/<uuid:pk>/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), name='my-nodes-assets'),
path('user/<uuid:pk>/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-assets-as-tree'),
# 查询某个用户组授权的资产和资产组
path('user-group/<uuid:pk>/assets/',
api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
path('user-group/<uuid:pk>/nodes/',
api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
path('user-group/<uuid:pk>/nodes-assets/',
api.UserGroupGrantedNodesWithAssetsApi.as_view(),
name='user-group-nodes-assets'),
path('user-group/<uuid:pk>/nodes-assets/tree/',
api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(),
name='user-group-nodes-assets-as-tree'),
path('user-group/<uuid:pk>/nodes/<uuid:node_id>/assets/',
api.UserGroupGrantedNodeAssetsApi.as_view(),
name='user-group-node-assets'),
path('user-group/<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
path('user-group/<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
path('user-group/<uuid:pk>/nodes-assets/', api.UserGroupGrantedNodesWithAssetsApi.as_view(), name='user-group-nodes-assets'),
path('user-group/<uuid:pk>/nodes-assets/tree/', api.UserGroupGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-group-nodes-assets-as-tree'),
path('user-group/<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
# 用户和资产授权变更
path('asset-permissions/<uuid:pk>/user/remove/',
api.AssetPermissionRemoveUserApi.as_view(),
name='asset-permission-remove-user'),
path('asset-permissions/<uuid:pk>/user/add/',
api.AssetPermissionAddUserApi.as_view(),
name='asset-permission-add-user'),
path('asset-permissions/<uuid:pk>/asset/remove/',
api.AssetPermissionRemoveAssetApi.as_view(),
name='asset-permission-remove-asset'),
path('asset-permissions/<uuid:pk>/asset/add/',
api.AssetPermissionAddAssetApi.as_view(),
name='asset-permission-add-asset'),
path('asset-permissions/<uuid:pk>/user/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'),
path('asset-permissions/<uuid:pk>/user/add/', api.AssetPermissionAddUserApi.as_view(), name='asset-permission-add-user'),
path('asset-permissions/<uuid:pk>/asset/remove/', api.AssetPermissionRemoveAssetApi.as_view(), name='asset-permission-remove-asset'),
path('asset-permissions/<uuid:pk>/asset/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'),
# 验证用户是否有某个资产和系统用户的权限
path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(),
name='validate-user-asset-permission'),
path('asset-permission/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(),
name='get-user-asset-permission-actions'),
path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'),
path('asset-permission/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'),
]
remote_app_permission_urlpatterns = [
# 查询用户授权的RemoteApp
path('user/<uuid:pk>/remote-apps/',
api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
path('user/remote-apps/',
api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
path('user/<uuid:pk>/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
path('user/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
# 获取用户授权的RemoteApp树
path('user/<uuid:pk>/remote-apps/tree/',
api.UserGrantedRemoteAppsAsTreeApi.as_view(),
name='user-remote-apps-as-tree'),
path('user/remote-apps/tree/',
api.UserGrantedRemoteAppsAsTreeApi.as_view(),
name='my-remote-apps-as-tree'),
path('user/<uuid:pk>/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'),
path('user/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'),
# 查询用户组授权的RemoteApp
path('user-group/<uuid:pk>/remote-apps/',
api.UserGroupGrantedRemoteAppsApi.as_view(),
name='user-group-remote-apps'),
path('user-group/<uuid:pk>/remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'),
# 校验用户对RemoteApp的权限
path('remote-app-permission/user/validate/',
api.ValidateUserRemoteAppPermissionApi.as_view(),
name='validate-user-remote-app-permission'),
path('remote-app-permission/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'),
# 用户和RemoteApp变更
path('remote-app-permissions/<uuid:pk>/user/add/',
api.RemoteAppPermissionAddUserApi.as_view(),
name='remote-app-permission-add-user'),
path('remote-app-permissions/<uuid:pk>/user/remove/',
api.RemoteAppPermissionRemoveUserApi.as_view(),
name='remote-app-permission-remove-user'),
path('remote-app-permissions/<uuid:pk>/remote-app/remove/',
api.RemoteAppPermissionRemoveRemoteAppApi.as_view(),
name='remote-app-permission-remove-remote-app'),
path('remote-app-permissions/<uuid:pk>/remote-app/add/',
api.RemoteAppPermissionAddRemoteAppApi.as_view(),
name='remote-app-permission-add-remote-app'),
path('remote-app-permissions/<uuid:pk>/user/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'),
path('remote-app-permissions/<uuid:pk>/user/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'),
path('remote-app-permissions/<uuid:pk>/remote-app/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'),
path('remote-app-permissions/<uuid:pk>/remote-app/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'),
]
urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns
......
......@@ -4,6 +4,8 @@ import uuid
from collections import defaultdict
import json
from hashlib import md5
import time
import itertools
from django.utils import timezone
from django.db.models import Q
......@@ -16,7 +18,8 @@ from common.utils import get_logger
from common.tree import TreeNode
from .. import const
from ..models import AssetPermission, Action
from ..hands import Node
from ..hands import Node, Asset
from assets.utils import NodeUtil
logger = get_logger(__file__)
......@@ -24,22 +27,60 @@ logger = get_logger(__file__)
__all__ = [
'AssetPermissionUtil', 'is_obj_attr_has', 'sort_assets',
'parse_asset_to_tree_node', 'parse_node_to_tree_node',
'check_system_user_action',
]
class TreeNodeCounter(NodeUtil):
def __init__(self, nodes):
self.__nodes = nodes
super().__init__(with_assets_amount=True)
def get_queryset(self):
return self.__nodes
def timeit(func):
def wrapper(*args, **kwargs):
logger.debug("Start call: {}".format(func.__name__))
now = time.time()
result = func(*args, **kwargs)
using = time.time() - now
logger.debug("Call {} end, using: {:.2}s".format(func.__name__, using))
return result
return wrapper
class GenerateTree:
def __init__(self):
"""
nodes: {"node_instance": {
"asset_instance": set("system_user")
nodes = {
"<node1>": {
"system_users": {
"system_user": action,
"system_user2": action,
},
"assets": set([<asset_instance>]),
}
}
assets = {
"<asset_instance2>": {
"system_user": action,
"system_user2": action,
},
}
"""
self.__all_nodes = list(Node.objects.all())
self.nodes = defaultdict(dict)
self.direct_nodes = []
self._node_util = None
self.nodes = defaultdict(lambda: {"system_users": defaultdict(int), "assets": set(), "assets_amount": 0})
self.assets = defaultdict(lambda: defaultdict(int))
self._root_node = None
self._ungroup_node = None
self._nodes_with_assets = None
@property
def node_util(self):
if not self._node_util:
self._node_util = NodeUtil()
return self._node_util
@property
def root_node(self):
......@@ -48,10 +89,8 @@ class GenerateTree:
all_nodes = self.nodes.keys()
# 如果没有授权节点,就放到默认的根节点下
if not all_nodes:
root_node = Node.root()
self.add_node(root_node)
else:
root_node = max(all_nodes)
return None
root_node = min(all_nodes)
self._root_node = root_node
return root_node
......@@ -60,50 +99,98 @@ class GenerateTree:
if self._ungroup_node:
return self._ungroup_node
node_id = const.UNGROUPED_NODE_ID
node_key = self.root_node.get_next_child_key()
if self.root_node:
node_key = "{}:{}".format(self.root_node.key, self.root_node.child_mark)
else:
node_key = '0:0'
node_value = _("Default")
node = Node(id=node_id, key=node_key, value=node_value)
self.add_node(node)
self.add_node(node, {})
self._ungroup_node = node
return node
def add_asset(self, asset, system_users):
nodes = asset.nodes.all()
in_nodes = set(self.direct_nodes) & set(nodes)
for node in in_nodes:
self.nodes[node][asset].update(system_users)
if not in_nodes:
self.nodes[self.ungrouped_node][asset].update(system_users)
@property
def empty_node(self):
node_id = const.EMPTY_NODE_ID
value = _('Empty')
node = Node(id=node_id, value=value)
return node
def get_nodes(self):
for node in self.nodes:
assets = set(self.nodes.get(node).keys())
for n in self.nodes.keys():
if n.key.startswith(node.key + ':'):
assets.update(set(self.nodes[n].keys()))
node.assets_amount = len(assets)
return self.nodes
# 添加节点时,追溯到根节点
def add_node(self, node):
if node in self.nodes:
return
#@timeit
def add_assets_without_system_users(self, assets):
for asset in assets:
self.add_asset(asset, {})
#@timeit
def add_assets(self, assets):
for asset, system_users in assets.items():
self.add_asset(asset, system_users)
# #@timeit
def add_asset(self, asset, system_users=None):
nodes = asset.nodes.all()
nodes = self.node_util.get_nodes_by_queryset(nodes)
if not system_users:
system_users = defaultdict(int)
else:
self.nodes[node] = defaultdict(set)
if node.is_root():
system_users = {k: v for k, v in system_users.items()}
_system_users = self.assets[asset]
for system_user, action in _system_users.items():
system_users[system_user] |= action
# 获取父节点们
parents = self.node_util.get_nodes_parents(nodes, with_self=True)
for node in parents:
_system_users = self.nodes[node]["system_users"]
self.nodes[node]["assets_amount"] += 1
for system_user, action in _system_users.items():
system_users[system_user] |= action
# 过滤系统用户的协议
system_users = {s: v for s, v in system_users.items() if asset.has_protocol(s.protocol)}
self.assets[asset] = system_users
in_nodes = set(self.nodes.keys()) & set(nodes)
if not in_nodes:
self.nodes[self.ungrouped_node]["assets_amount"] += 1
self.nodes[self.ungrouped_node]["assets"].add(system_users)
return
for n in self.__all_nodes:
if n.key == node.parent_key:
self.add_node(n)
break
for node in in_nodes:
self.nodes[node]["assets"].add(asset)
def add_node(self, node, system_users=None):
if not system_users:
system_users = defaultdict(int)
self.nodes[node]["system_users"] = system_users
# 添加树节点
#@timeit
def add_nodes(self, nodes):
for node in nodes:
self.add_node(node)
self.add_nodes(node.get_all_children(with_self=False))
# 如果是直接授权的节点,则放到direct_nodes中
self.direct_nodes.append(node)
_nodes = nodes.keys()
family = self.node_util.get_family(_nodes, with_children=True)
for node in family:
self.add_node(node, nodes.get(node, {}))
def get_assets(self):
return dict(self.assets)
#@timeit
def get_nodes_with_assets(self):
if self._nodes_with_assets:
return self._nodes_with_assets
nodes = {}
for node, values in self.nodes.items():
node._assets_amount = values["assets_amount"]
nodes[node] = {asset: self.assets.get(asset, {}) for asset in values["assets"]}
# 如果返回空节点,页面构造授权资产树报错
if not nodes:
nodes[self.empty_node] = {}
self._nodes_with_assets = nodes
return dict(nodes)
def get_nodes(self):
return list(self.nodes.keys())
def get_user_permissions(user, include_group=True):
......@@ -112,11 +199,11 @@ def get_user_permissions(user, include_group=True):
arg = Q(users=user) | Q(user_groups__in=groups)
else:
arg = Q(users=user)
return AssetPermission.objects.all().valid().filter(arg)
return AssetPermission.get_queryset_with_prefetch().filter(arg)
def get_user_group_permissions(user_group):
return AssetPermission.objects.all().valid().filter(
return AssetPermission.get_queryset_with_prefetch().filter(
user_groups=user_group
)
......@@ -127,47 +214,27 @@ def get_asset_permissions(asset, include_node=True):
arg = Q(assets=asset) | Q(nodes__in=nodes)
else:
arg = Q(assets=asset)
return AssetPermission.objects.all().valid().filter(arg)
return AssetPermission.objects.valid().filter(arg)
def get_node_permissions(node):
return AssetPermission.objects.all().valid().filter(nodes=node)
return AssetPermission.objects.valid().filter(nodes=node)
def get_system_user_permissions(system_user):
return AssetPermission.objects.valid().all().filter(
return AssetPermission.objects.valid().filter(
system_users=system_user
)
class AssetPermissionUtil:
get_permissions_map = {
"User": get_user_permissions,
"UserGroup": get_user_group_permissions,
"Asset": get_asset_permissions,
"Node": get_node_permissions,
"SystemUser": get_system_user_permissions,
}
class AssetPermissionCacheMixin:
CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_'
CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_'
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh'))
def __init__(self, obj, cache_policy='0'):
self.object = obj
self.obj_id = str(obj.id)
self._permissions = None
self._permissions_id = None # 标记_permission的唯一值
self._assets = None
self._filter_id = 'None' # 当通过filter更改 permission是标记
self.cache_policy = cache_policy
self.tree = GenerateTree()
self.change_org_if_need()
@staticmethod
def change_org_if_need():
set_to_root_org()
cache_policy = '1'
obj_id = ''
_filter_id = None
@classmethod
def is_not_using_cache(cls, cache_policy):
......@@ -190,94 +257,7 @@ class AssetPermissionUtil:
def _is_refresh_cache(self):
return self.is_refresh_cache(self.cache_policy)
@property
def permissions(self):
if self._permissions:
return self._permissions
object_cls = self.object.__class__.__name__
func = self.get_permissions_map[object_cls]
permissions = func(self.object)
self._permissions = permissions
return permissions
def filter_permissions(self, **filters):
filters_json = json.dumps(filters, sort_keys=True)
self._permissions = self.permissions.filter(**filters)
self._filter_id = md5(filters_json.encode()).hexdigest()
@staticmethod
def _structured_system_user(system_users, actions):
"""
结构化系统用户
:param system_users:
:param actions:
:return: {system_user1: {'actions': set(), }, }
"""
_attr = {'actions': set(actions)}
_system_users = {system_user: _attr for system_user in system_users}
return _system_users
def get_nodes_direct(self):
"""
返回用户/组授权规则直接关联的节点
:return: {asset1: {system_user1: {'actions': set()},}}
"""
nodes = defaultdict(dict)
permissions = self.permissions.prefetch_related('nodes', 'system_users')
for perm in permissions:
actions = perm.actions.all()
self.tree.add_nodes(perm.nodes.all())
for node in perm.nodes.all():
system_users = perm.system_users.all()
system_users = self._structured_system_user(system_users, actions)
nodes[node].update(system_users)
return nodes
def get_assets_direct(self):
"""
返回用户授权规则直接关联的资产
:return: {asset1: {system_user1: {'actions': set()},}}
"""
assets = defaultdict(dict)
permissions = self.permissions.prefetch_related('assets', 'system_users')
for perm in permissions:
actions = perm.actions.all()
for asset in perm.assets.all().valid().prefetch_related('nodes'):
system_users = perm.system_users.filter(protocol__in=asset.protocols_name)
system_users = self._structured_system_user(system_users, actions)
assets[asset].update(system_users)
return assets
def get_assets_without_cache(self):
"""
:return: {asset1: set(system_user1,)}
"""
if self._assets:
return self._assets
assets = self.get_assets_direct()
nodes = self.get_nodes_direct()
for node, system_users in nodes.items():
_assets = node.get_all_assets().valid().prefetch_related('nodes')
for asset in _assets:
for system_user, attr_dict in system_users.items():
if not asset.has_protocol(system_user.protocol):
continue
if system_user in assets[asset]:
actions = assets[asset][system_user]['actions']
attr_dict['actions'].update(actions)
system_users.update({system_user: attr_dict})
assets[asset].update(system_users)
__assets = defaultdict(set)
for asset, system_users in assets.items():
for system_user, attr_dict in system_users.items():
setattr(system_user, 'actions', attr_dict['actions'])
__assets[asset] = set(system_users.keys())
self._assets = __assets
return self._assets
#@timeit
def get_cache_key(self, resource):
cache_key = self.CACHE_KEY_PREFIX + '{obj_id}_{filter_id}_{resource}'
return cache_key.format(
......@@ -285,93 +265,90 @@ class AssetPermissionUtil:
resource=resource
)
def node_key(self):
@property
def node_asset_key(self):
return self.get_cache_key('NODES_WITH_ASSETS')
@property
def node_key(self):
return self.get_cache_key('NODES')
@property
def asset_key(self):
return self.get_cache_key('ASSETS')
key = self.get_cache_key('ASSETS')
return key
@property
def system_key(self):
return self.get_cache_key('SYSTEM_USER')
def get_assets_from_cache(self):
cached = cache.get(self.asset_key)
def get_resource_from_cache(self, resource):
key_map = {
"assets": self.asset_key,
"nodes": self.node_key,
"nodes_with_assets": self.node_asset_key,
"system_users": self.system_key
}
key = key_map.get(resource)
if not key:
raise ValueError("Not a valid resource: {}".format(resource))
cached = cache.get(key)
if not cached:
self.update_cache()
cached = cache.get(self.asset_key)
cached = cache.get(key)
return cached
def get_assets(self):
if self._is_not_using_cache():
return self.get_assets_from_cache()
def get_resource(self, resource):
if self._is_using_cache():
return self.get_resource_from_cache(resource)
elif self._is_refresh_cache():
self.expire_cache()
return self.get_assets_from_cache()
data = self.get_resource_from_cache(resource)
return data
else:
self.expire_cache()
return self.get_assets_without_cache()
def get_nodes_with_assets_without_cache(self):
"""
返回节点并且包含资产
{"node": {"assets": set("system_user")}}
:return:
"""
assets = self.get_assets_without_cache()
for asset, system_users in assets.items():
self.tree.add_asset(asset, system_users)
return self.tree.get_nodes()
return self.get_resource_without_cache(resource)
def get_nodes_with_assets_from_cache(self):
cached = cache.get(self.node_key)
if not cached:
self.update_cache()
cached = cache.get(self.node_key)
return cached
def get_resource_without_cache(self, resource):
attr = 'get_{}_without_cache'.format(resource)
return getattr(self, attr)()
def get_nodes_with_assets(self):
if self._is_using_cache():
return self.get_nodes_with_assets_from_cache()
elif self._is_refresh_cache():
self.expire_cache()
return self.get_nodes_with_assets_from_cache()
else:
return self.get_nodes_with_assets_without_cache()
return self.get_resource("nodes_with_assets")
def get_system_user_without_cache(self):
system_users = set()
permissions = self.permissions.prefetch_related('system_users')
for perm in permissions:
system_users.update(perm.system_users.all())
return system_users
def get_assets(self):
return self.get_resource("assets")
def get_system_user_from_cache(self):
cached = cache.get(self.system_key)
if not cached:
self.update_cache()
cached = cache.get(self.system_key)
return cached
def get_nodes(self):
return self.get_resource("nodes")
def get_system_users(self):
if self._is_using_cache():
return self.get_system_user_from_cache()
elif self._is_refresh_cache():
self.expire_cache()
return self.get_system_user_from_cache()
else:
return self.get_system_user_without_cache()
return self.get_resource("system_users")
def get_meta_cache_key(self):
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_{filter_id}'
key = cache_key.format(
obj_id=str(self.object.id), filter_id=self._filter_id
obj_id=self.obj_id, filter_id=self._filter_id
)
return key
@property
def cache_meta(self):
key = self.get_meta_cache_key()
return cache.get(key) or {}
meta = cache.get(key) or {}
# print("Meta key: {}".format(key))
# print("Meta id: {}".format(meta["id"]))
return meta
def update_cache(self):
assets = self.get_resource_without_cache("assets")
nodes_with_assets = self.get_resource_without_cache("nodes_with_assets")
system_users = self.get_resource_without_cache("system_users")
nodes = self.get_resource_without_cache("nodes")
cache.set(self.asset_key, assets, self.CACHE_TIME)
cache.set(self.node_asset_key, nodes_with_assets, self.CACHE_TIME)
cache.set(self.system_key, system_users, self.CACHE_TIME)
cache.set(self.node_key, nodes, self.CACHE_TIME)
self.set_meta_to_cache()
def set_meta_to_cache(self):
key = self.get_meta_cache_key()
......@@ -380,22 +357,15 @@ class AssetPermissionUtil:
'datetime': timezone.now(),
'object': str(self.object)
}
# print("Set meta key: {}".format(key))
# print("set meta to cache: {}".format(meta["id"]))
cache.set(key, meta, self.CACHE_TIME)
def expire_cache_meta(self):
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_*'
key = cache_key.format(obj_id=str(self.object.id))
key = cache_key.format(obj_id=self.obj_id)
cache.delete_pattern(key)
def update_cache(self):
assets = self.get_assets_without_cache()
nodes = self.get_nodes_with_assets_without_cache()
system_users = self.get_system_user_without_cache()
cache.set(self.asset_key, assets, self.CACHE_TIME)
cache.set(self.node_key, nodes, self.CACHE_TIME)
cache.set(self.system_key, system_users, self.CACHE_TIME)
self.set_meta_to_cache()
def expire_cache(self):
"""
因为 获取用户的节点,资产,系统用户等都能会缓存,这里会清理所有与该对象有关的
......@@ -418,6 +388,142 @@ class AssetPermissionUtil:
cache.delete_pattern(key)
class AssetPermissionUtil(AssetPermissionCacheMixin):
get_permissions_map = {
"User": get_user_permissions,
"UserGroup": get_user_group_permissions,
"Asset": get_asset_permissions,
"Node": get_node_permissions,
"SystemUser": get_system_user_permissions,
}
assets_only = (
'id', 'hostname', 'ip', "platform", "domain_id",
'comment', 'is_active', 'os', 'org_id'
)
def __init__(self, obj, cache_policy='0'):
self.object = obj
self.obj_id = str(obj.id)
self._permissions = None
self._permissions_id = None # 标记_permission的唯一值
self._assets = None
self._filter_id = 'None' # 当通过filter更改 permission是标记
self.cache_policy = cache_policy
self.tree = GenerateTree()
self.change_org_if_need()
self.nodes = None
self._nodes = None
self._assets_direct = None
self._nodes_direct = None
@staticmethod
def change_org_if_need():
set_to_root_org()
@property
def permissions(self):
if self._permissions:
return self._permissions
object_cls = self.object.__class__.__name__
func = self.get_permissions_map[object_cls]
permissions = func(self.object)
self._permissions = permissions
return permissions
#@timeit
def filter_permissions(self, **filters):
filters_json = json.dumps(filters, sort_keys=True)
self._permissions = self.permissions.filter(**filters)
self._filter_id = md5(filters_json.encode()).hexdigest()
#@timeit
def get_nodes_direct(self):
"""
返回用户/组授权规则直接关联的节点
:return: {node1: {system_user1: {'actions': set()},}}
"""
if self._nodes_direct:
return self._nodes_direct
nodes = defaultdict(lambda: defaultdict(int))
for perm in self.permissions:
actions = [perm.actions]
system_users = perm.system_users.all()
_nodes = perm.nodes.all()
for node, system_user, action in itertools.product(_nodes, system_users, actions):
nodes[node][system_user] |= action
self.tree.add_nodes(nodes)
self._nodes_direct = nodes
return nodes
def get_nodes_without_cache(self):
self.get_assets_direct()
return self.tree.get_nodes()
#@timeit
def get_assets_direct(self):
"""
返回用户授权规则直接关联的资产
:return: {asset1: {system_user1: 1,}}
"""
if self._assets_direct:
return self._assets_direct
assets = defaultdict(lambda: defaultdict(int))
for perm in self.permissions:
actions = [perm.actions]
_assets = perm.assets.all().only(*self.assets_only)
system_users = perm.system_users.all()
iterable = itertools.product(_assets, system_users, actions)
for asset, system_user, action in iterable:
assets[asset][system_user] |= action
self.tree.add_assets(assets)
self._assets_direct = assets
return assets
#@timeit
def get_assets_without_cache(self):
"""
:return: {asset1: set(system_user1,)}
"""
if self._assets:
return self._assets
self.get_assets_direct()
nodes = self.get_nodes_direct()
pattern = set()
for node in nodes:
pattern.add(r'^{0}$|^{0}:'.format(node.key))
pattern = '|'.join(list(pattern))
if pattern:
assets = Asset.objects.filter(nodes__key__regex=pattern) \
.prefetch_related('nodes')\
.only(*self.assets_only)\
.distinct()
else:
assets = []
assets = list(assets)
self.tree.add_assets_without_system_users(assets)
assets = self.tree.get_assets()
self._assets = assets
return assets
#@timeit
def get_nodes_with_assets_without_cache(self):
"""
返回节点并且包含资产
{"node": {"asset": {"system_user": 1})}}
:return:
"""
self.get_assets_without_cache()
nodes_assets = self.tree.get_nodes_with_assets()
return nodes_assets
def get_system_users_without_cache(self):
system_users = set()
permissions = self.permissions.prefetch_related('system_users')
for perm in permissions:
system_users.update(perm.system_users.all())
return system_users
def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")):
if not attrs:
vals = [val for val in obj.__dict__.values() if isinstance(val, (str, int))]
......@@ -440,9 +546,7 @@ def sort_assets(assets, order_by='hostname', reverse=False):
def parse_node_to_tree_node(node):
from .. import serializers
name = '{} ({})'.format(node.value, node.assets_amount)
node_serializer = serializers.GrantedNodeSerializer(node)
data = {
'id': node.key,
'name': name,
......@@ -451,7 +555,11 @@ def parse_node_to_tree_node(node):
'isParent': True,
'open': node.is_root(),
'meta': {
'node': node_serializer.data,
'node': {
"id": node.id,
"key": node.key,
"value": node.value,
},
'type': 'node'
}
}
......@@ -460,23 +568,21 @@ def parse_node_to_tree_node(node):
def parse_asset_to_tree_node(node, asset, system_users):
system_users_protocol_matched = [s for s in system_users if asset.has_protocol(s.protocol)]
icon_skin = 'file'
if asset.platform.lower() == 'windows':
icon_skin = 'windows'
elif asset.platform.lower() == 'linux':
icon_skin = 'linux'
system_users = []
for system_user in system_users_protocol_matched:
system_users.append({
_system_users = []
for system_user, action in system_users.items():
_system_users.append({
'id': system_user.id,
'name': system_user.name,
'username': system_user.username,
'protocol': system_user.protocol,
'priority': system_user.priority,
'login_mode': system_user.login_mode,
'actions': [action.name for action in system_user.actions],
'comment': system_user.comment,
'actions': [Action.value_to_choices(action)],
})
data = {
'id': str(asset.id),
......@@ -487,14 +593,13 @@ def parse_asset_to_tree_node(node, asset, system_users):
'open': False,
'iconSkin': icon_skin,
'meta': {
'system_users': system_users,
'system_users': _system_users,
'type': 'asset',
'asset': {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,
'protocols': [{"name": p.name, "port": p.port}
for p in asset.protocols.all()],
'protocols': asset.protocols_as_list,
'platform': asset.platform,
'domain': None if not asset.domain else asset.domain.id,
'is_active': asset.is_active,
......@@ -504,16 +609,3 @@ def parse_asset_to_tree_node(node, asset, system_users):
}
tree_node = TreeNode(**data)
return tree_node
def check_system_user_action(system_user, action):
"""
:param system_user: SystemUser object (包含动态属性: actions)
:param action: Action object
:return: bool
"""
check_actions = [Action.get_action_all(), action]
granted_actions = getattr(system_user, 'actions', [])
actions = list(set(granted_actions).intersection(set(check_actions)))
return bool(actions)
......@@ -10,10 +10,9 @@ from django.conf import settings
from common.permissions import PermissionsMixin, IsOrgAdmin
from orgs.utils import current_org
from perms.hands import Node, Asset, SystemUser, User, UserGroup
from perms.models import AssetPermission, Action
from perms.hands import Node, Asset, SystemUser, UserGroup
from perms.models import AssetPermission
from perms.forms import AssetPermissionForm
from perms.const import PERMS_ACTION_NAME_ALL
__all__ = [
......@@ -58,14 +57,13 @@ class AssetPermissionCreateView(PermissionsMixin, CreateView):
assets_id = assets_id.split(",")
assets = Asset.objects.filter(id__in=assets_id)
form['assets'].initial = assets
form['actions'].initial = Action.objects.get(name=PERMS_ACTION_NAME_ALL)
return form
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('Create asset permission'),
'api_action': "create",
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -81,7 +79,8 @@ class AssetPermissionUpdateView(PermissionsMixin, UpdateView):
def get_context_data(self, **kwargs):
context = {
'app': _('Perms'),
'action': _('Update asset permission')
'action': _('Update asset permission'),
'api_action': "update",
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -156,7 +155,7 @@ class AssetPermissionAssetView(PermissionsMixin,
permission_classes = [IsOrgAdmin]
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset = AssetPermission.objects.all())
self.object = self.get_object(queryset=AssetPermission.objects.all())
return super().get(request, *args, **kwargs)
def get_queryset(self):
......
......@@ -242,22 +242,3 @@ class CommandStorageDeleteAPI(APIView):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class DjangoSettingsAPI(APIView):
def get(self, request):
if not settings.DEBUG:
return Response("Not in debug mode")
data = {}
for i in [settings, getattr(settings, '_wrapped')]:
if not i:
continue
for k, v in i.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data)
\ No newline at end of file
......@@ -5,7 +5,7 @@ class MailTestSerializer(serializers.Serializer):
EMAIL_HOST = serializers.CharField(max_length=1024, required=True)
EMAIL_PORT = serializers.IntegerField(default=25)
EMAIL_HOST_USER = serializers.CharField(max_length=1024)
EMAIL_HOST_PASSWORD = serializers.CharField()
EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True)
EMAIL_FROM = serializers.CharField(required=False, allow_blank=True)
EMAIL_USE_SSL = serializers.BooleanField(default=False)
EMAIL_USE_TLS = serializers.BooleanField(default=False)
......
......@@ -24,12 +24,13 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
@receiver(django_ready, dispatch_uid="my_unique_identifier")
def monkey_patch_settings(sender, **kwargs):
logger.debug("Monkey patch settings")
cache_key_prefix = '_SETTING_'
custom_need_cache_settings = [
'AUTHENTICATION_BACKENDS'
'AUTHENTICATION_BACKENDS', 'TERMINAL_HOST_KEY',
]
custom_no_cache_settings = [
'BASE_DIR', 'VERSION', 'AUTH_OPENID'
'BASE_DIR', 'VERSION', 'AUTH_OPENID',
]
django_settings = dir(global_settings)
uncached_settings = [i for i in django_settings if i.isupper()]
......
......@@ -15,5 +15,4 @@ urlpatterns = [
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),
path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'),
path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'),
]
......@@ -304,7 +304,7 @@ table.dataTable tbody td.selected td i.text-navy {
div.dataTables_wrapper div.dataTables_filter,
.dataTables_length {
float: right !important;
// float: right !important;
}
div.dataTables_wrapper div.dataTables_filter {
......@@ -466,3 +466,11 @@ div.dataTables_wrapper div.dataTables_filter {
span.select2-selection__placeholder {
line-height: 34px !important;
}
.p-l-5 {
padding-left: 5px;
}
.p-r-5 {
padding-right: 5px;
}
\ No newline at end of file
......@@ -277,7 +277,7 @@ function APIUpdateAttr(props) {
}).done(function(data, textStatue, jqXHR) {
if (flash_message) {
var msg = "";
if (user_fail_message) {
if (user_success_message) {
msg = user_success_message;
} else {
msg = default_success_message;
......@@ -472,9 +472,10 @@ jumpserver.initDataTable = function (options) {
};
var table = ele.DataTable({
pageLength: options.pageLength || 15,
dom: options.dom || '<"#uc.pull-left">flt<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"f><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
order: options.order || [],
// select: options.select || 'multi',
searchDelay: 800,
buttons: [],
columnDefs: columnDefs,
ajax: {
......@@ -500,8 +501,10 @@ jumpserver.initDataTable = function (options) {
$('[data-toggle="popover"]').popover({
html: true,
placement: 'bottom',
// trigger: 'hover',
trigger: 'click',
container: 'body'
}).on('click', function (e) {
$('[data-toggle="popover"]').not(this).popover('hide');
});
});
$('.ipt_check_all').on('click', function() {
......@@ -565,12 +568,14 @@ jumpserver.initServerSideDataTable = function (options) {
};
var table = ele.DataTable({
pageLength: options.pageLength || 15,
dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
// dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"f><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
order: options.order || [],
buttons: [],
columnDefs: columnDefs,
serverSide: true,
processing: true,
searchDelay: 800,
ajax: {
url: options.ajax_url ,
error: function(jqXHR, textStatus, errorThrown) {
......@@ -603,7 +608,10 @@ jumpserver.initServerSideDataTable = function (options) {
search_list.map(function (val, index) {
var kv = val.split(":");
if (kv.length === 2) {
search_attr[kv[0]] = kv[1]
var value = kv[1];
value = value.replace("+", " ");
console.log(value);
search_attr[kv[0]] = value
} else {
search_raw.push(kv)
}
......@@ -633,7 +641,7 @@ jumpserver.initServerSideDataTable = function (options) {
columns: options.columns || [],
select: options.select || select,
language: jumpserver.language,
lengthMenu: [[15, 25, 50, 9999], [15, 25, 50, 'All']]
lengthMenu: options.lengthMenu || [[15, 25, 50, 9999], [15, 25, 50, 'All']]
});
table.selected = [];
table.selected_rows = [];
......@@ -668,8 +676,14 @@ jumpserver.initServerSideDataTable = function (options) {
})
}
}).on('draw', function(){
$('#op').html(options.op_html || '');
$('#uc').html(options.uc_html || '');
$('[data-toggle="popover"]').popover({
html: true,
placement: 'bottom',
trigger: 'click',
container: 'body'
}).on('click', function (e) {
$('[data-toggle="popover"]').not(this).popover('hide');
});
var table_data = [];
$.each(table.rows().data(), function (id, row) {
if (row.id) {
......@@ -683,6 +697,11 @@ jumpserver.initServerSideDataTable = function (options) {
table.rows(index).select()
}
});
}).on("init", function () {
$('#op').html(options.op_html || '');
$('#uc').html(options.uc_html || '');
$('#fb').html(options.fb_html || '');
$('#fa').html(options.fa_html || '');
});
var table_id = table.settings()[0].sTableId;
$('#' + table_id + ' .ipt_check_all').on('click', function() {
......@@ -1062,3 +1081,79 @@ function htmlEscape ( d ) {
d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
d;
}
function objectAttrsIsList(obj, attrs) {
attrs.forEach(function (attr) {
if (!obj[attr]){
obj[attr] = []
}
else if (obj[attr] && !(obj[attr] instanceof Array)){
obj[attr] = [obj[attr]]
}
})
}
function objectAttrsIsDatetime(obj, attrs) {
attrs.forEach(function (attr) {
obj[attr] = new Date(obj[attr]).toISOString();
})
}
function objectAttrsIsBool(obj, attrs) {
attrs.forEach(function (attr) {
if (!obj[attr]) {
obj[attr] = false
} else if (['on', '1'].includes(obj[attr])) {
obj[attr] = true
}
})
}
function formatDateAsCN(d) {
var date = new Date(d);
return date.toISOString().replace("T", " ").replace(/\..*/, "");
}
function getUrlParams(url) {
url = url.split("?");
var params = "";
if (url.length === 2){
params = url[1];
}
return params
}
function getTimeUnits(u) {
var units = {
"d": "天",
"h": "时",
"m": "分",
"s": "秒",
};
if (navigator.language || "zh-CN") {
return units[u]
}
return u
}
function timeOffset(a, b) {
var start = new Date(a);
var end = new Date(b);
var offset = (end - start)/1000;
var days = offset / 3600 / 24;
var hours = offset / 3600;
var minutes = offset / 60;
var seconds = offset;
if (days > 1) {
return days.toFixed(1) + " " + getTimeUnits("d");
} else if (hours > 1) {
return hours.toFixed(1) + " " + getTimeUnits("h");
} else if (minutes > 1) {
return minutes.toFixed(1) + " " + getTimeUnits("m")
} else if (seconds > 1) {
return seconds.toFixed(1) + " " + getTimeUnits("s")
}
return ""
}
\ No newline at end of file
!function(a){a.fn.datepicker.dates["zh-CN"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["周日","周一","周二","周三","周四","周五","周六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",monthsTitle:"选择月份",clear:"清除",format:"yyyy-mm-dd",titleFormat:"yyyy年mm月",weekStart:1}}(jQuery);
\ No newline at end of file
......@@ -10,7 +10,7 @@
{% csrf_token %}
<div class="form-group">
<label class="control-label">{% trans "Download the imported template or use the exported CSV file format" %}</label>
<a href="{% block import_modal_download_template_url %}{% endblock %}?format=csv&template=import" style="display: block">{% trans 'Download the import template' %}</a>
<a href="{% block import_modal_download_template_url %}{% endblock %}?format=csv&template=import&limit=1" style="display: block">{% trans 'Download the import template' %}</a>
</div>
<div class="form-group">
......
......@@ -11,7 +11,7 @@
</div>
{% if ADMIN_ORGS and request.COOKIES.IN_ADMIN_PAGE != 'No' %}
{% if ADMIN_ORGS|length > 1 or not CURRENT_ORG.is_default %}
<div style="height: 55px;">
<div>
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" style="display: block; background-color: transparent; color: #8095a8; padding: 14px 20px 14px 25px">
<i class="fa fa-bookmark" style="width: 14px; "></i>
<span class="nav-label" style="padding-left: 7px">
......@@ -19,15 +19,13 @@
</span>
<span class="fa fa-sort-desc pull-right"></span>
</a>
<ul class="dropdown-menu" style="width: 219px;">
<ul class="dropdown-menu" style="min-width: 220px">
{% for org in ADMIN_ORGS %}
<li>
<a class="org-dropdown"
href="{% url 'orgs:org-switch' pk=org.id %}"
data-id="{{ org.id }}">
<a class="org-dropdown" href="{% url 'orgs:org-switch' pk=org.id %}" data-id="{{ org.id }}">
{{ org.name }}
{% if org.id == CURRENT_ORG.id %}
<span class="fa fa-circle pull-right" style="padding-top: 5px; color: #1ab394"></span>
<span class="fa fa-circle" style="padding-top: 5px; color: #1ab394"></span>
{% endif %}
</a>
</li>
......
......@@ -2,4 +2,5 @@
#
from .terminal import *
from .session import *
from .command import *
from .task import *
# -*- coding: utf-8 -*-
#
import time
from django.utils import timezone
from django.shortcuts import HttpResponse
from rest_framework.pagination import LimitOffsetPagination
from rest_framework import viewsets
from rest_framework import generics
from rest_framework.response import Response
from django.template import loader
from common.permissions import IsOrgAdminOrAppUser, IsAuditor
from common.utils import get_logger
from ..backends import (
get_command_storage, get_multi_command_storage,
SessionCommandSerializer,
)
logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'CommandExportApi']
class CommandQueryMixin:
command_store = get_command_storage()
pagination_class = LimitOffsetPagination
permission_classes = [IsOrgAdminOrAppUser | IsAuditor]
filter_fields = [
"asset", "system_user", "user", "session",
]
default_days_ago = 5
def get_queryset(self):
date_from, date_to = self.get_date_range()
q = self.request.query_params
multi_command_storage = get_multi_command_storage()
queryset = multi_command_storage.filter(
date_from=date_from, date_to=date_to, input=q.get("input"),
user=q.get("user"), asset=q.get("asset"),
system_user=q.get("system_user")
)
return queryset
def filter_queryset(self, queryset):
return queryset
def get_filter_fields(self):
fields = self.filter_fields
fields.extend(["date_from", "date_to"])
return fields
def get_date_range(self):
now = timezone.now()
days_ago = now - timezone.timedelta(days=self.default_days_ago)
default_start_st = days_ago.timestamp()
default_end_st = now.timestamp()
query_params = self.request.query_params
date_from_st = query_params.get("date_from") or default_start_st
date_to_st = query_params.get("date_to") or default_end_st
return float(date_from_st), float(date_to_st)
class CommandViewSet(CommandQueryMixin, viewsets.ModelViewSet):
"""接受app发送来的command log, 格式如下
{
"user": "admin",
"asset": "localhost",
"system_user": "web",
"session": "xxxxxx",
"input": "whoami",
"output": "d2hvbWFp", # base64.b64encode(s)
"timestamp": 1485238673.0
}
"""
command_store = get_command_storage()
serializer_class = SessionCommandSerializer
def create(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, many=True)
if serializer.is_valid():
ok = self.command_store.bulk_save(serializer.validated_data)
if ok:
return Response("ok", status=201)
else:
return Response("Save error", status=500)
else:
msg = "Command not valid: {}".format(serializer.errors)
logger.error(msg)
return Response({"msg": msg}, status=401)
class CommandExportApi(CommandQueryMixin, generics.ListAPIView):
def list(self, request, *args, **kwargs):
queryset = self.filter_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
# -*- coding: utf-8 -*-
#
import logging
import os
from django.shortcuts import get_object_or_404
......@@ -10,92 +9,62 @@ from django.conf import settings
from rest_framework.pagination import LimitOffsetPagination
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.generics import GenericAPIView
import jms_storage
from common.utils import is_uuid
from common.utils import is_uuid, get_logger
from common.permissions import IsOrgAdminOrAppUser, IsAuditor
from common.filters import DatetimeRangeFilter
from orgs.mixins import OrgBulkModelViewSet
from ..hands import SystemUser
from ..models import Terminal, Session
from ..models import Session
from .. import serializers
from ..backends import get_command_storage, get_multi_command_storage, \
SessionCommandSerializer
__all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet']
logger = logging.getLogger(__file__)
__all__ = ['SessionViewSet', 'SessionReplayViewSet',]
logger = get_logger(__name__)
class SessionViewSet(BulkModelViewSet):
class SessionViewSet(OrgBulkModelViewSet):
queryset = Session.objects.all()
serializer_class = serializers.SessionSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser | IsAuditor, )
def get_queryset(self):
queryset = super().get_queryset()
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
queryset = queryset.filter(terminal=terminal)
return queryset
filter_fields = [
"user", "asset", "system_user", "remote_addr",
"protocol", "terminal", "is_finished",
]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
]
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# 解决guacamole更新session时并发导致幽灵会话的问题
if self.request.method in ('PATCH',):
queryset = queryset.select_for_update()
return queryset
@property
def filter_backends(self):
backends = list(GenericAPIView.filter_backends)
backends.append(DatetimeRangeFilter)
return backends
def perform_create(self, serializer):
if hasattr(self.request.user, 'terminal'):
serializer.validated_data["terminal"] = self.request.user.terminal
sid = serializer.validated_data["system_user"]
# guacamole提交的是id
if is_uuid(sid):
_system_user = SystemUser.get_system_user_by_id_or_cached(sid)
if _system_user:
_system_user = get_object_or_404(SystemUser, id=sid)
serializer.validated_data["system_user"] = _system_user.name
return super().perform_create(serializer)
class CommandViewSet(viewsets.ViewSet):
"""接受app发送来的command log, 格式如下
{
"user": "admin",
"asset": "localhost",
"system_user": "web",
"session": "xxxxxx",
"input": "whoami",
"output": "d2hvbWFp", # base64.b64encode(s)
"timestamp": 1485238673.0
}
"""
command_store = get_command_storage()
serializer_class = SessionCommandSerializer
permission_classes = (IsOrgAdminOrAppUser | IsAuditor,)
def get_queryset(self):
self.command_store.filter(**dict(self.request.query_params))
def create(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, many=True)
if serializer.is_valid():
ok = self.command_store.bulk_save(serializer.validated_data)
if ok:
return Response("ok", status=201)
else:
return Response("Save error", status=500)
else:
msg = "Command not valid: {}".format(serializer.errors)
logger.error(msg)
return Response({"msg": msg}, status=401)
def list(self, request, *args, **kwargs):
multi_command_storage = get_multi_command_storage()
queryset = multi_command_storage.filter()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,)
permission_classes = (IsOrgAdminOrAppUser | IsAuditor,)
session = None
def create(self, request, *args, **kwargs):
......
......@@ -71,12 +71,16 @@ class CommandStore(CommandBase):
if not date_to and not session:
date_to = date_to_default
if date_from is not None:
filter_kwargs['timestamp__gte'] = int(date_from.timestamp())
if isinstance(date_from, datetime.datetime):
date_from = date_from.timestamp()
filter_kwargs['timestamp__gte'] = int(date_from)
if date_to is not None:
filter_kwargs['timestamp__lte'] = int(date_to.timestamp())
if isinstance(date_to, datetime.datetime):
date_to = date_to.timestamp()
filter_kwargs['timestamp__lte'] = int(date_to)
if user:
filter_kwargs["user"] = user
filter_kwargs["user__startswith"] = user
if asset:
filter_kwargs['asset'] = asset
if system_user:
......@@ -85,7 +89,6 @@ class CommandStore(CommandBase):
filter_kwargs['input__icontains'] = input
if session:
filter_kwargs['session'] = session
return filter_kwargs
def filter(self, date_from=None, date_to=None,
......
......@@ -9,8 +9,12 @@ class CommandStore(CommandBase):
self.storage_list = storage_list
def filter(self, **kwargs):
queryset = []
if len(self.storage_list) == 1:
storage = list(self.storage_list)[0]
queryset = storage.filter(**kwargs)
return queryset
queryset = []
for storage in self.storage_list:
queryset.extend(storage.filter(**kwargs))
return sorted(queryset, key=lambda command: command.timestamp, reverse=True)
......
......@@ -191,26 +191,22 @@ class Session(OrgModelMixin):
@property
def _date_start_first_has_replay_rdp_session(self):
if self._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
instance = self.__class__.objects.filter(
protocol='rdp', has_replay=True).order_by('date_start').first()
protocol='rdp', has_replay=True
).order_by('date_start').first()
if not instance:
return None
self._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = instance.date_start
return self._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION
date_start = timezone.now() - timezone.timedelta(days=365)
else:
date_start = instance.date_start
self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = date_start
return self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION
def can_replay(self):
if self.has_replay:
return True
# 判断对RDP Session添加上报has_replay状态机制之前的录像回放
if self._date_start_first_has_replay_rdp_session is None:
return True
if self.date_start < self._date_start_first_has_replay_rdp_session:
return True
return False
def save_to_storage(self, f):
......@@ -241,6 +237,10 @@ class Session(OrgModelMixin):
command_store = get_multi_command_storage()
return command_store.count(session=str(self.id))
@property
def login_from_display(self):
return self.get_login_from_display()
class Meta:
db_table = "terminal_session"
ordering = ["-date_start"]
......
......@@ -2,6 +2,7 @@
#
from rest_framework import serializers
from orgs.mixins import BulkOrgResourceModelSerializer
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from ..models import Terminal, Status, Session, Task
......@@ -24,13 +25,18 @@ class TerminalSerializer(serializers.ModelSerializer):
return Session.objects.filter(terminal=obj, is_finished=False).count()
class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class SessionSerializer(BulkOrgResourceModelSerializer):
command_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Session
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
fields = [
"id", "user", "asset", "system_user", "login_from",
"login_from_display", "remote_addr", "is_finished",
"has_replay", "can_replay", "protocol", "date_start", "date_end",
"terminal", "command_amount",
]
class StatusSerializer(serializers.ModelSerializer):
......
......@@ -3,97 +3,41 @@
{% load static %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/footable/footable.core.css" %}" rel="stylesheet">
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
.toggle {
cursor: pointer;
}
.detail-key {
width: 70px;
}
</style>
{% endblock %}
{% block content_left_head %}
{% block table_pagination %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline" style="padding-bottom: 8px">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<option value="">{% trans 'Asset' %}</option>
{% for a in asset_list %}
<option value="{{ a }}" {% if a == asset %} selected {% endif %}>{{ a }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<option value="">{% trans 'System user' %}</option>
{% for s in system_user_list %}
<option value="{{ s }}" {% if s == system_user %} selected {% endif %}>{{ s }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<input type="text" class="form-control input-sm" name="command" placeholder="{% trans 'Command' %}" value="{{ command }}">
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_container %}
<table class="footable table table-stripped table-bordered toggle-arrow-tiny" data-page="false" >
<table class="table table-striped table-bordered table-hover" id="command_table" data-page="false" >
<thead>
<tr>
<th data-toggle="true">ID</th>
<th></th>
<th>{% trans 'Command' %}</th>
<th>{% trans 'User' %}</th>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'System user'%}</th>
<th>{% trans 'Session' %}</th>
<th>{% trans 'Datetime' %}</th>
<th data-hide="all"></th>
</tr>
</thead>
<tbody>
{% for command in command_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ command.input }}</td>
<td>{{ command.user }}</td>
<td>{{ command.asset }}</td>
<td>{{ command.system_user }}</td>
<td><a href="{% url 'terminal:session-detail' pk=command.session %}">{% trans "Goto" %}</a></td>
<td>{{ command.timestamp|ts_to_date }}</td>
<td><pre style="border: none; background: none">{{ command.output }}</pre></td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="actions" class="">
<div id="actions" class="hide">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="export">{% trans 'Export command' %}</option>
......@@ -105,39 +49,166 @@
</div>
</div>
</div>
{% endblock %}
<div class="hide" id="daterange">
<div class="form-group p-l-5" id="date" style="padding-left: 5px">
<div class="input-daterange input-group p-l-5" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" id="date_from" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" id="date_to" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
</div>
<ul class="dropdown-menu search-help">
<li><a class="search-item" data-value="user">{% trans 'User' %}</a></li>
<li><a class="search-item" data-value="asset">{% trans 'Asset' %}</a></li>
<li><a class="search-item" data-value="system_user">{% trans 'System user' %}</a></li>
<li><a class="search-item" data-value="input">{% trans 'Command' %}</a></li>
</ul>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script src="{% static "js/plugins/footable/footable.all.min.js" %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}" charset="UTF-8"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.zh-CN.min.js' %}" ></script>
<script>
var table;
$(document).ready(function () {
$('.footable').footable();
$('.select2').select2({
dropdownAutoWidth : true,
width: 'auto'
});
$('#date .input-daterange').datepicker({
table = initTable().on("init", function () {
var dateFromRef = $("#date_from");
var dateToRef = $("#date_to");
var options = {
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
autoclose: true,
language: navigator.language || "en",
};
dateFromRef.datepicker(options).on("changeDate", function () {
var date = new Date($(this).val() + ' 0:0:0');
var url = table.ajax.url();
url = setUrlParam(url, "date_from", date.getTime()/1000);
table.ajax.url(url);
table.ajax.reload();
});
dateToRef.datepicker(options).on("changeDate", function () {
var date = new Date($(this).val() + ' 23:59:59');
var url = table.ajax.url();
url = setUrlParam(url, "date_to", date.getTime()/1000);
table.ajax.url(url);
table.ajax.reload();
});
});
})
.on('click', '#btn_bulk_update', function(){
var action = $('#slct_bulk_update').val();
var param_action = '&action=' + action;
var local_params = window.location.search;
if(!local_params){
param_action = '?action=' + action;
}
var params = local_params + param_action;
var pathname = window.location.pathname + 'export/';
var url = pathname + params;
// var action = $('#slct_bulk_update').val();
var params = getUrlParams(table.ajax.url());
var exportPath = "{% url 'api-terminal:command-export' %}";
var url = exportPath + "?" + params;
window.open(url);
});
}).on("click", '#command_table_filter input', function (e) {
e.preventDefault();
e.stopPropagation();
var offset1 = $('#command_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 = $("#command_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', '.toggle', function (e) {
e.preventDefault();
var detailRows = [];
var tr = $(this).closest('tr');
var row = table.row(tr);
var idx = $.inArray(tr.attr('id'), detailRows);
if (row.child.isShown()) {
tr.removeClass('details');
$(this).children('i:first-child').removeClass('fa-angle-down').addClass('fa-angle-right');
row.child.hide();
// Remove from the 'open' array
detailRows.splice(idx, 1);
} else {
tr.addClass('details');
$(this).children('i:first-child').removeClass('fa-angle-right').addClass('fa-angle-down');
row.child(format(row.data())).show();
// Add to the 'open' array
if (idx === -1) {
detailRows.push(tr.attr('id'));
}
}
})
function format(d) {
var output = $("<pre style='border: none; background: none'></pre>");
output.append(d.output);
return output
}
var commandListUrl = '{% url "api-terminal:command-list" %}';
var dateFrom = "{{ date_from.timestamp }}";
var dateTo = "{{ date_to.timestamp }}";
function initTable() {
commandListUrl = setUrlParam(commandListUrl, "date_from", dateFrom);
commandListUrl = setUrlParam(commandListUrl, "date_to", dateTo);
var options = {
ele: $('#command_table'),
columnDefs: [
{targets: 0, createdCell: function (td, cellData, rowData) {
$(td).addClass("toggle");
$(td).html("<i class='fa fa-angle-right'></i>");
}},
{targets: 5, createdCell: function (td, cellData) {
var data = '<a href="{% url "terminal:session-detail" pk=DEFAULT_PK %}">{% trans "Goto" %}</a>'
.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(data);
}},
{targets: 6, createdCell: function (td, cellData) {
var data = formatDateAsCN(cellData*1000);
$(td).html(data);
}},
],
toggle: true,
ajax_url: commandListUrl,
columns: [
{data: "id"}, {data: "input", orderable: false}, {data: "user", orderable: false},
{data: "asset"}, {data: "system_user"},
{data: "session"}, {data: "timestamp", width: "160px"},
],
select: {},
op_html: $('#actions').html(),
fb_html: $("#daterange").html(),
};
table = jumpserver.initServerSideDataTable(options);
return table
}
</script>
{% endblock %}
......
......@@ -7,65 +7,21 @@
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_pagination %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<option value="">{% trans 'Asset' %}</option>
{% for a in asset_list %}
<option value="{{ a }}" {% if a == asset %} selected {% endif %}>{{ a }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<option value="">{% trans 'System user' %}</option>
{% for su in system_user_list %}
<option value="{{ su }}" {% if su == system_user %} selected {% endif %}>{{ su }}</option>
{% endfor %}
</select>
</div>
{# <div class="input-group">#}
{# <input type="text" class="form-control input-sm" name="keyword" placeholder="Keyword" value="{{ keyword }}">#}
{# </div>#}
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
{% block table_container %}
<table class="table table-striped table-bordered table-hover" id="session_table" data-page="false" >
<thead>
<tr>
<th class="text-center"></th>
<th class="text-center">{% trans 'ID' %}</th>
<th class="text-center">{% trans 'User' %}</th>
......@@ -76,47 +32,16 @@
<th class="text-center">{% trans 'Login from' %}</th>
<th class="text-center">{% trans 'Command' %}</th>
<th class="text-center">{% trans 'Date start' %}</th>
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
<th class="text-center">{% trans 'Duration' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
{% endblock %}
{% block table_body %}
{% for session in session_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" class="cbx-term" value="{{ session.id }}"></td>
<td class="text-center">
<a href="{% url 'terminal:session-detail' pk=session.id %}">{{ forloop.counter }}</a>
</td>
<td class="text-center">{{ session.user }}</td>
<td class="text-center">{{ session.asset }}</td>
<td class="text-center">{{ session.system_user }}</td>
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
<td class="text-center">{{ session.protocol }}</td>
<td class="text-center">{{ session.get_login_from_display }}</td>
<td class="text-center">{{ session.command_amount }}</td>
<td class="text-center">{{ session.date_start }}</td>
{# <td class="text-center">{{ session.date_last_active }}</td>#}
<td class="text-center">{{ session.date_start|time_util_with_seconds:session.date_end }}</td>
<td>
{% if session.is_finished %}
<a {% if not session.can_replay %} disabled="" {% endif %} onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>
{% else %}
{% if session.protocol == 'ssh' and request.user.is_org_admin%}
<a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% else %}
<a class="btn btn-xs btn-danger btn-term" disabled value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
{% endblock %}
</thead>
<tbody>
</tbody>
</table>
{% block content_bottom_left %}
{% if request.user.is_org_admin %}
<div id="actions" {% if type != "online" %} style="display: none" {% endif %}>
<div id="actions" class="hide">
{% if type == "online" %}
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="terminate">{% trans 'Terminate selected' %}</option>
......@@ -128,14 +53,36 @@
</button>
</div>
</div>
</div>
{% endif %}
</div>
<div class="hide" id="daterange">
<div class="form-group p-l-5" id="date" style="padding-left: 5px">
<div class="input-daterange input-group p-l-5" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" id="date_from" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" id="date_to" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
</div>
<ul class="dropdown-menu search-help">
<li><a class="search-item" data-value="user">{% trans 'User' %}</a></li>
<li><a class="search-item" data-value="asset">{% trans 'Asset' %}</a></li>
<li><a class="search-item" data-value="system_user">{% trans 'System user' %}</a></li>
<li><a class="search-item" data-value="remote_addr">{% trans 'Remote addr' %}</a></li>
<li><a class="search-item" data-value="protocol">{% trans 'Protocol' %}</a></li>
</ul>
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
function terminateSession(data) {
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.zh-CN.min.js' %}" ></script>
<script>
function terminateSession(data) {
function success() {
window.setTimeout(function () {
window.location.reload()
......@@ -150,9 +97,78 @@
success: success,
success_message: success_message
});
}
var sessionListUrl = "{% url 'api-terminal:session-list' %}?is_finished={% if type == "online" %}0{% else %}1{% endif %}";
var dateFrom = "{{ date_from.timestamp }}";
var dateTo = "{{ date_to.timestamp }}";
function initTable() {
dateFrom = new Date(dateFrom * 1000).toISOString();
dateTo = new Date(dateTo * 1000).toISOString();
sessionListUrl = setUrlParam(sessionListUrl, "date_from", dateFrom);
sessionListUrl = setUrlParam(sessionListUrl, "date_to", dateTo);
var options = {
ele: $('#session_table'),
ordering: false,
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData, i) {
var index = i + 1;
var data = '<a href="{% url 'terminal:session-detail' pk=DEFAULT_PK %}">'
+ index + "</a>";
data = data.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(data);
}},
{targets: 9, createdCell: function (td, cellData) {
var data = formatDateAsCN(cellData);
$(td).html(data);
}},
{targets: 10, createdCell: function (td, cellData, rowData) {
var data = "";
if (cellData && rowData.date_start) {
data = timeOffset(rowData.date_start, cellData)
}
$(td).html(data);
}},
{targets: 11, createdCell: function (td, cellData, rowData) {
var btnGroup = "";
var replayBtn = '<a disabled data-session="sessionID" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>'
{#var replayBtn = '<a disabled onclick="window.open("url", "height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no)" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>'#}
replayBtn = replayBtn.replace("sessionID", rowData.id);
if (rowData.can_replay) {
replayBtn = replayBtn.replace("disabled", "")
}
var termBtn = '<a class="btn btn-xs btn-danger btn-term" disabled value="sessionID" terminal="terminalID" >{% trans "Terminate" %}</a>';
if (rowData.protocol === "ssh" && "{{ request.user.is_org_admin }}" === "True") {
termBtn = termBtn.replace("disabled", "")
.replace("sessionID", cellData)
.replace("terminalID", rowData.terminal)
}
if (rowData.is_finished) {
btnGroup += replayBtn
} else {
btnGroup += termBtn;
}
$(td).html(btnGroup);
}},
],
ajax_url: sessionListUrl,
columns: [
{data: "id"}, {data: "id"}, {data: "user", orderable: false},
{data: "asset", orderable: false}, {data: "system_user", orderable: false},
{data: "remote_addr"}, {data: "protocol"}, {data: "login_from_display"},
{data: "command_amount"}, {data: "date_start"},
{data: "date_end"}, {data: "id"},
],
op_html: $('#actions').html(),
fb_html: $("#daterange").html(),
};
table = jumpserver.initServerSideDataTable(options);
return table
}
function finishedSession(data) {
function finishedSession(data) {
var the_url = "{% url 'api-terminal:session-list' %}";
var success_message = '{% trans "Finish session success" %}';
var success = function() {
......@@ -165,47 +181,102 @@
success: success,
success_message: success_message
});
}
$(document).ready(function() {
jumpserver.initStaticTable('table');
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
}
var table;
$(document).ready(function() {
table = initTable("#session_table").on('init', function () {
var dateFromRef = $("#date_from");
var dateToRef = $("#date_to");
var options = {
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
autoclose: true,
language: navigator.language || "en",
};
dateFromRef.datepicker(options).on("changeDate", function () {
if (!$(this).val()) {
return
}
var value = $(this).val() + ' 0:0:0';
var date = new Date(value);
var url = table.ajax.url();
url = setUrlParam(url, "date_from", date.toISOString());
table.ajax.url(url);
table.ajax.reload();
});
}).on('click', '.btn-term', function () {
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_bulk_update', function () {
}).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 id_list = [];
$(".cbx-term:checked").each(function (index, data) {
id_list.push($(data).attr("value"))
});
if (id_list.length === 0) {
var idList = table.selected;
if (idList.length === 0) {
return false;
}
function doTerminate() {
terminateSession(id_list)
terminateSession(idList)
}
function doFinishSession() {
var data = [];
$.each(id_list, function (i, v) {
$.each(idList, function (i, v) {
data.push({
"pk": v,
"id": v,
"is_finished": true
})
});
......@@ -221,7 +292,7 @@
default:
break;
}
});
</script>
});
</script>
{% endblock %}
......@@ -26,6 +26,7 @@ urlpatterns = [
path('terminal/<uuid: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/<uuid:pk>/replay/',
# api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}),
......
......@@ -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')
]
......@@ -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():
......
# -*- 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
# -*- 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)
......
......@@ -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()
......
......@@ -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 isinstance(users, User):
users = [users]
if current_org and current_org.is_real():
user.orgs.add(current_org.id)
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()
......
......@@ -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')
......
# 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',
),
],
),
]
# 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'),
),
]
# 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',
),
]
......@@ -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")
......
......@@ -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())
......
......@@ -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: [],
......
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
<div class="" style="float: right">
<div class="pull-right" >
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
......
......@@ -217,33 +217,9 @@
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).on('click', '#btn_update_pk', function() {
var $this = $(this);
var pk = $('#txt_pk').val();
var the_url = '{% url "api-users:user-public-key-update" pk=user.id %}';
var body = {'_public_key': pk};
var success = function() {
$('#txt_pk').val('');
var msg = "{% trans 'Successfully updated the SSH public key.' %}";
swal("{% trans 'User SSH public key update' %}", msg, "success");
};
var fail = function() {
var msg = "{% trans 'Failed to update SSH public key.' %}";
swal({
title: "{% trans 'User SSH public key update' %}",
text: msg,
type: "error",
showCancelButton: false,
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: true
}, function () {
$('#txt_pk').focus();
}
);
};
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
}).on('click', '.btn-reset-pubkey', function () {
$(document).ready(function () {
})
.on('click', '.btn-reset-pubkey', function () {
var the_url = '{% url "users:user-pubkey-generate" %}';
window.open(the_url, "_blank")
})
......
......@@ -29,9 +29,7 @@ urlpatterns = [
# User view
path('user/', views.UserListView.as_view(), name='user-list'),
path('user/export/', views.UserExportView.as_view(), name='user-export'),
path('first-login/', views.UserFirstLoginView.as_view(), name='user-first-login'),
path('user/import/', views.UserBulkImportView.as_view(), name='user-import'),
path('user/create/', views.UserCreateView.as_view(), name='user-create'),
path('user/<uuid:pk>/update/', views.UserUpdateView.as_view(), name='user-update'),
path('user/update/', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
......
......@@ -46,9 +46,7 @@ from ..signals import post_user_create
__all__ = [
'UserListView', 'UserCreateView', 'UserDetailView',
'UserUpdateView',
'UserGrantedAssetView',
'UserExportView', 'UserBulkImportView', 'UserProfileView',
'UserUpdateView', 'UserGrantedAssetView', 'UserProfileView',
'UserProfileUpdateView', 'UserPasswordUpdateView',
'UserPublicKeyUpdateView', 'UserBulkUpdateView',
'UserPublicKeyGenerateView',
......@@ -135,19 +133,6 @@ class UserUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView):
kwargs.update(context)
return super().get_context_data(**kwargs)
def form_valid(self, form):
password = form.cleaned_data.get('password')
if not password:
return super().form_valid(form)
is_ok = check_password_rules(password)
if not is_ok:
form.add_error(
"password", _("* Your password does not meet the requirements")
)
return self.form_invalid(form)
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super(UserUpdateView, self).get_form_kwargs()
data = {'request': self.request}
......@@ -223,147 +208,6 @@ class UserDetailView(PermissionsMixin, DetailView):
return queryset
@method_decorator(csrf_exempt, name='dispatch')
class UserExportView(View):
def get(self, request):
fields = [
User._meta.get_field(name)
for name in [
'id', 'name', 'username', 'email', 'role',
'wechat', 'phone', 'is_active', 'comment',
]
]
spm = request.GET.get('spm', '')
users_id = cache.get(spm, [])
filename = 'users-{}.csv'.format(
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
response.write(codecs.BOM_UTF8)
users = User.objects.filter(id__in=users_id)
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
header = [field.verbose_name for field in fields]
header.append(_('User groups'))
writer.writerow(header)
for user in users:
groups = ','.join([group.name for group in user.groups.all()])
data = [getattr(user, field.name) for field in fields]
data.append(groups)
writer.writerow(data)
return response
def post(self, request):
try:
users_id = json.loads(request.body).get('users_id', [])
except ValueError:
return HttpResponse('Json object not valid', status=400)
spm = uuid.uuid4().hex
cache.set(spm, users_id, 300)
url = reverse('users:user-export') + '?spm=%s' % spm
return JsonResponse({'redirect': url})
class UserBulkImportView(PermissionsMixin, JSONResponseMixin, FormView):
form_class = forms.FileForm
permission_classes = [IsOrgAdmin]
def form_invalid(self, form):
try:
error = form.errors.values()[-1][-1]
except Exception as e:
error = _('Invalid file.')
data = {
'success': False,
'msg': error
}
return self.render_json_response(data)
# todo: need be patch, method to long
def form_valid(self, form):
f = form.cleaned_data['file']
det_result = chardet.detect(f.read())
f.seek(0) # reset file seek index
data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
csv_file = StringIO(data)
reader = csv.reader(csv_file)
csv_data = [row for row in reader]
header_ = csv_data[0]
fields = [
User._meta.get_field(name)
for name in [
'id', 'name', 'username', 'email', 'role',
'wechat', 'phone', 'is_active', 'comment',
]
]
mapping_reverse = {field.verbose_name: field.name for field in fields}
mapping_reverse[_('User groups')] = 'groups'
attr = [mapping_reverse.get(n, None) for n in header_]
if None in attr:
data = {'valid': False,
'msg': 'Must be same format as '
'template or export file'}
return self.render_json_response(data)
created, updated, failed = [], [], []
for row in csv_data[1:]:
if set(row) == {''}:
continue
user_dict = dict(zip(attr, row))
id_ = user_dict.pop('id')
for k, v in user_dict.items():
if k in ['is_active']:
if v.lower() == 'false':
v = False
else:
v = bool(v)
elif k == 'groups':
groups_name = v.split(',')
v = UserGroup.objects.filter(name__in=groups_name)
else:
continue
user_dict[k] = v
user = get_object_or_none(User, id=id_) if id_ and is_uuid(id_) else None
if not user:
try:
with transaction.atomic():
groups = user_dict.pop('groups')
user = User.objects.create(**user_dict)
user.groups.set(groups)
created.append(user_dict['username'])
post_user_create.send(self.__class__, user=user)
except Exception as e:
failed.append('%s: %s' % (user_dict['username'], str(e)))
else:
for k, v in user_dict.items():
if k == 'groups':
user.groups.set(v)
continue
if v:
setattr(user, k, v)
try:
user.save()
updated.append(user_dict['username'])
except Exception as e:
failed.append('%s: %s' % (user_dict['username'], str(e)))
data = {
'created': created,
'created_info': 'Created {}'.format(len(created)),
'updated': updated,
'updated_info': 'Updated {}'.format(len(updated)),
'failed': failed,
'failed_info': 'Failed {}'.format(len(failed)),
'valid': True,
'msg': 'Created: {}. Updated: {}, Error: {}'.format(
len(created), len(updated), len(failed))
}
return self.render_json_response(data)
class UserGrantedAssetView(PermissionsMixin, DetailView):
model = User
template_name = 'users/user_granted_asset.html'
......
......@@ -61,6 +61,8 @@ REDIS_PORT: 6379
# AUTH_OPENID_REALM_NAME: realm-name
# AUTH_OPENID_CLIENT_ID: client-id
# AUTH_OPENID_CLIENT_SECRET: client-secret
# AUTH_OPENID_IGNORE_SSL_VERIFICATION: True
# AUTH_OPENID_SHARE_SESSION: False
#
# Use Radius authorization
# 使用Radius来认证
......
#!/usr/bin/python
import os
import datetime
import shutil
import sys
import django
GUACAMOLE_REPLAYS_DIR = '/tmp/guacamole/record'
UPLOAD_TO = 'local'
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PROJECT_DIR = os.path.dirname(BASE_DIR)
LOCAL_REPLAY_DIR = os.path.join(PROJECT_DIR, 'data', 'media', 'replay')
APPS_DIR = os.path.join(PROJECT_DIR, "apps")
if os.path.exists(APPS_DIR):
sys.path.insert(0, APPS_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
django.setup()
from terminal.models import Session
def find_replays():
replays = []
for root, dirs, files in os.walk(GUACAMOLE_REPLAYS_DIR, topdown=True):
for name in files:
if name.startswith('20') and name.endswith('.gz'):
session_id = '-'.join(name.split('-')[3:]).replace(".gz", "")
file_path = os.path.join(root, name)
create_ts = os.stat(file_path).st_ctime
create_date = datetime.datetime.utcfromtimestamp(create_ts)
replays.append({
"id": session_id,
"path": file_path,
"date": create_date,
})
return replays
def upload_to_local(session):
source_path = session["path"]
session_id = session["id"]
target_filename = session_id + ".replay.gz"
date_created = session["date"].strftime("%Y-%m-%d")
target_dir = os.path.join(LOCAL_REPLAY_DIR, date_created )
target_path = os.path.join(target_dir, target_filename)
if not os.path.isdir(target_dir):
os.makedirs(target_dir)
print("Move {} => {}".format(source_path, target_path))
shutil.copy(source_path, target_path)
shutil.copystat(source_path, target_path)
os.unlink(source_path)
Session.objects.filter(id=session_id).update(is_finished=True)
if __name__ == '__main__':
for s in find_replays():
upload_to_local(s)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment