refactor: 整合系统用户和管理用户 (#6236)

* perf: 整合系统用户和管理用户

* stash

stash

perf: 优化系统用户和资产的表结构

* perf: 添加信号

* perf: 添加算法

* perf: 去掉 asset user backends

* perf: 整理系统用户api

* perfF: 暂存一下

* stash

* perf: 暂存一下

* perf: 暂存

* xxx

* perf: ...

* stash it

* xxx

* xxx

* xxx

* xxx

* xxx

* stash it

* 修改Protocols

* perf: 修改创建authbook信号

* perf: 添加auth info

* .stash

* perf: 基本完成

* perf: 修复完成

* perf: 修复更改的id

* perf: 修复迁移过去数量不对的问题

* perf: 修改systemuser

* fix: 修复批量编辑近期的问题

* fix: 修复authbook加载的问题

* xxx

Co-authored-by: ibuler <ibuler@qq.com>
pull/6401/head
fit2bot 2021-07-08 14:23:18 +08:00 committed by GitHub
parent a9f814a515
commit ec8dca90d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1524 additions and 2210 deletions

View File

@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
protocol_group = serializers.ListField( protocol_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
help_text=protocol_group_help_text.format( help_text=protocol_group_help_text.format(
', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET]) ', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet])
) )
) )

View File

@ -4,9 +4,9 @@ from .asset import *
from .label import * from .label import *
from .system_user import * from .system_user import *
from .system_user_relation import * from .system_user_relation import *
from .accounts import *
from .node import * from .node import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .asset_user import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *

View File

@ -0,0 +1,51 @@
from django.db.models import F
from django.conf import settings
from rest_framework.decorators import action
from rest_framework.response import Response
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook
from .. import serializers
__all__ = ['AccountViewSet', 'AccountSecretsViewSet']
class AccountViewSet(OrgBulkModelViewSet):
model = AuthBook
filterset_fields = ("username", "asset", "systemuser")
search_fields = filterset_fields
serializer_classes = {
'default': serializers.AccountSerializer,
'verify_account': serializers.AssetTaskSerializer
}
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = super().get_queryset()\
.annotate(ip=F('asset__ip'))\
.annotate(hostname=F('asset__hostname'))
return queryset
@action(methods=['post'], detail=True, url_path='verify')
def verify_account(self, request, *args, **kwargs):
account = super().get_object()
task = test_accounts_connectivity_manual.delay([account])
return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet):
"""
因为可能要导出所有账号所以单独建立了一个 viewset
"""
serializer_classes = {
'default': serializers.AccountSecretSerializer
}
permission_classes = (IsOrgAdmin, NeedMFAVerify)
http_method_names = ['get']
def get_permissions(self):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser]
return super().get_permissions()

View File

@ -1,109 +1,28 @@
from django.db import transaction
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import status
from rest_framework.response import Response
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from common.utils import get_logger from common.utils import get_logger
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset from ..models import SystemUser
from .. import serializers from .. import serializers
from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = ['AdminUserViewSet']
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
]
# 兼容一下老的 api
class AdminUserViewSet(OrgBulkModelViewSet): class AdminUserViewSet(OrgBulkModelViewSet):
""" """
Admin user api set, for add,delete,update,list,retrieve resource Admin user api set, for add,delete,update,list,retrieve resource
""" """
model = AdminUser model = SystemUser
filterset_fields = ("name", "username") filterset_fields = ("name", "username")
search_fields = filterset_fields search_fields = filterset_fields
serializer_class = serializers.AdminUserSerializer serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_classes = {
'default': serializers.AdminUserSerializer,
'retrieve': serializers.AdminUserDetailSerializer,
}
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset().filter(type=SystemUser.Type.admin)
queryset = queryset.annotate(assets_amount=Count('assets')) queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset return queryset
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
has_related_asset = instance.assets.exists()
if has_related_asset:
data = {'msg': _('Deleted failed, There are related assets')}
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return super().destroy(request, *args, **kwargs)
class AdminUserAuthApi(generics.UpdateAPIView):
model = AdminUser
serializer_class = serializers.AdminUserAuthSerializer
permission_classes = (IsOrgAdmin,)
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
model = AdminUser
serializer_class = serializers.ReplaceNodeAdminUserSerializer
permission_classes = (IsOrgAdmin,)
def update(self, request, *args, **kwargs):
admin_user = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
nodes = serializer.validated_data['nodes']
assets = []
for node in nodes:
assets.extend([asset.id for asset in node.get_all_assets()])
with transaction.atomic():
Asset.objects.filter(id__in=assets).update(admin_user=admin_user)
return Response({"msg": "ok"})
else:
return Response({'error': serializer.errors}, status=400)
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset admin user assets_connectivity
"""
model = AdminUser
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object()
task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()

View File

@ -33,8 +33,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
filterset_fields = { filterset_fields = {
'hostname': ['exact'], 'hostname': ['exact'],
'ip': ['exact'], 'ip': ['exact'],
'systemuser__id': ['exact'], 'system_users__id': ['exact'],
'admin_user__id': ['exact'],
'platform__base': ['exact'], 'platform__base': ['exact'],
'is_active': ['exact'], 'is_active': ['exact'],
'protocols': ['exact', 'icontains'] 'protocols': ['exact', 'icontains']
@ -43,7 +42,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = { serializer_classes = {
'default': serializers.AssetSerializer, 'default': serializers.AssetSerializer,
'display': serializers.AssetDisplaySerializer, 'single': serializers.AssetVerboseSerializer,
} }
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]

View File

@ -1,151 +0,0 @@
# -*- coding: utf-8 -*-
#
import coreapi
from django.conf import settings
from rest_framework.response import Response
from rest_framework import generics, filters
from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
from common.utils import get_object_or_none, get_logger
from common.mixins import CommonApiMixin
from ..backends import AssetUserManager
from ..models import Node
from .. import serializers
from ..tasks import (
test_asset_users_connectivity_manual
)
__all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
]
logger = get_logger(__name__)
class AssetUserFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
kwargs = {}
for field in view.filterset_fields:
value = request.GET.get(field)
if not value:
continue
if field == "node_id":
value = get_object_or_none(Node, pk=value)
kwargs["node"] = value
continue
elif field == "asset_id":
field = "asset"
kwargs[field] = value
if kwargs:
queryset = queryset.filter(**kwargs)
logger.debug("Filter {}".format(kwargs))
return queryset
class AssetUserSearchBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
value = request.GET.get('search')
if not value:
return queryset
queryset = queryset.search(value)
return queryset
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
def get_schema_fields(self, view):
return [
coreapi.Field(
name='latest', location='query', required=False,
type='string', example='1',
description='Only the latest version'
)
]
def filter_queryset(self, request, queryset, view):
latest = request.GET.get('latest') == '1'
if latest:
queryset = queryset.distinct()
return queryset
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
serializer_classes = {
'default': serializers.AssetUserWriteSerializer,
'display': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer,
}
permission_classes = [IsOrgAdminOrAppUser]
filterset_fields = [
"id", "ip", "hostname", "username",
"asset_id", "node_id",
"prefer", "prefer_id",
]
search_fields = ["ip", "hostname", "username"]
filter_backends = [
AssetUserFilterBackend, AssetUserSearchBackend,
AssetUserLatestFilterBackend,
]
def allow_bulk_destroy(self, qs, filtered):
return False
def get_object(self):
pk = self.kwargs.get("pk")
if pk is None:
return
queryset = self.get_queryset()
obj = queryset.get(id=pk)
return obj
def perform_destroy(self, instance):
manager = AssetUserManager()
manager.delete(instance)
def get_queryset(self):
manager = AssetUserManager()
queryset = manager.all()
return queryset
class AssetUserAuthInfoViewSet(AssetUserViewSet):
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
http_method_names = ['get', 'post']
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions()
class AssetUserTaskCreateAPI(generics.CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AssetUserTaskSerializer
filter_backends = AssetUserViewSet.filter_backends
filterset_fields = AssetUserViewSet.filterset_fields
def get_asset_users(self):
manager = AssetUserManager()
queryset = manager.all()
for cls in self.filter_backends:
queryset = cls().filter_queryset(self.request, queryset, self)
return list(queryset)
def perform_create(self, serializer):
asset_users = self.get_asset_users()
# action = serializer.validated_data["action"]
# only this
# if action == "test":
task = test_asset_users_connectivity_manual.delay(asset_users)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
return task
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@ -2,14 +2,12 @@
# #
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView from rest_framework.generics import CreateAPIView
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.utils import reverse from common.utils import reverse
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org
from tickets.models import Ticket
from tickets.api import GenericTicketStatusRetrieveCloseAPI from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..hands import IsOrgAdmin, IsAppUser from ..hands import IsOrgAdmin, IsAppUser
from ..models import CommandFilter, CommandFilterRule from ..models import CommandFilter, CommandFilterRule

View File

@ -32,7 +32,8 @@ class SystemUserViewSet(OrgBulkModelViewSet):
filterset_fields = { filterset_fields = {
'name': ['exact'], 'name': ['exact'],
'username': ['exact'], 'username': ['exact'],
'protocol': ['exact', 'in'] 'protocol': ['exact', 'in'],
'type': ['exact', 'in'],
} }
search_fields = filterset_fields search_fields = filterset_fields
serializer_class = serializers.SystemUserSerializer serializer_class = serializers.SystemUserSerializer

View File

@ -6,6 +6,7 @@ from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org from orgs.utils import current_org
from .. import models, serializers from .. import models, serializers
@ -15,6 +16,8 @@ __all__ = [
'SystemUserUserRelationViewSet', 'SystemUserUserRelationViewSet',
] ]
logger = get_logger(__name__)
class RelationMixin: class RelationMixin:
def get_queryset(self): def get_queryset(self):
@ -24,8 +27,8 @@ class RelationMixin:
queryset = queryset.filter(systemuser__org_id=org_id) queryset = queryset.filter(systemuser__org_id=org_id)
queryset = queryset.annotate(systemuser_display=Concat( queryset = queryset.annotate(systemuser_display=Concat(
F('systemuser__name'), Value('('), F('systemuser__username'), F('systemuser__name'), Value('('),
Value(')') F('systemuser__username'), Value(')')
)) ))
return queryset return queryset
@ -41,10 +44,11 @@ class RelationMixin:
system_users_objects_map[i.systemuser].append(_id) system_users_objects_map[i.systemuser].append(_id)
sender = self.get_sender() sender = self.get_sender()
for system_user, objects in system_users_objects_map.items(): for system_user, object_ids in system_users_objects_map.items():
logger.debug('System user relation changed, send m2m_changed signals')
m2m_changed.send( m2m_changed.send(
sender=sender, instance=system_user, action='post_add', sender=sender, instance=system_user, action='post_add',
reverse=False, model=model, pk_set=objects reverse=False, model=model, pk_set=set(object_ids)
) )
def get_sender(self): def get_sender(self):

View File

@ -1 +0,0 @@
from .manager import AssetUserManager

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
#
from abc import abstractmethod
from ..models import Asset
class BaseBackend:
@abstractmethod
def all(self):
pass
@abstractmethod
def filter(self, username=None, hostname=None, ip=None, assets=None,
node=None, prefer_id=None, **kwargs):
pass
@abstractmethod
def search(self, item):
pass
@abstractmethod
def get_queryset(self):
pass
@abstractmethod
def delete(self, union_id):
pass
@staticmethod
def qs_to_values(qs):
values = qs.values(
'hostname', 'ip', "asset_id",
'name', 'username', 'password', 'private_key', 'public_key',
'score', 'version',
"asset_username", "union_id",
'date_created', 'date_updated',
'org_id', 'backend', 'backend_display'
)
return values
@staticmethod
def make_assets_as_ids(assets):
if not assets:
return []
if isinstance(assets[0], Asset):
assets = [a.id for a in assets]
return assets

View File

@ -1,332 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from functools import reduce
from django.db.models import F, CharField, Value, IntegerField, Q, Count
from django.db.models.functions import Concat
from rest_framework.exceptions import PermissionDenied
from common.utils import get_object_or_none
from orgs.utils import current_org
from ..models import AuthBook, SystemUser, Asset, AdminUser
from .base import BaseBackend
class DBBackend(BaseBackend):
union_id_length = 2
def __init__(self, queryset=None):
if queryset is None:
queryset = self.all()
self.queryset = queryset
def _clone(self):
return self.__class__(self.queryset)
def all(self):
return AuthBook.objects.none()
def count(self):
return self.queryset.count()
def get_queryset(self):
return self.queryset
def delete(self, union_id):
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
return
return self._perform_delete_by_union_id(cleaned_union_id)
def _perform_delete_by_union_id(self, union_id_cleaned):
pass
def filter(self, assets=None, node=None, prefer=None, prefer_id=None,
union_id=None, id__in=None, **kwargs):
clone = self._clone()
clone._filter_union_id(union_id)
clone._filter_prefer(prefer, prefer_id)
clone._filter_node(node)
clone._filter_assets(assets)
clone._filter_other(kwargs)
clone._filter_id_in(id__in)
return clone
def _filter_union_id(self, union_id):
if not union_id:
return
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
self.queryset = self.queryset.none()
return
return self._perform_filter_union_id(union_id, cleaned_union_id)
def _check_union_id(self, union_id, cleaned_union_id):
return union_id and len(cleaned_union_id) == self.union_id_length
def _perform_filter_union_id(self, union_id, union_id_cleaned):
self.queryset = self.queryset.filter(union_id=union_id)
def _filter_assets(self, assets):
asset_ids = self.make_assets_as_ids(assets)
if asset_ids:
self.queryset = self.queryset.filter(asset_id__in=asset_ids)
def _filter_node(self, node):
pass
def _filter_id_in(self, ids):
if ids and isinstance(ids, list):
self.queryset = self.queryset.filter(union_id__in=ids)
@staticmethod
def clean_kwargs(kwargs):
return {k: v for k, v in kwargs.items() if v}
def _filter_other(self, kwargs):
kwargs = self.clean_kwargs(kwargs)
if kwargs:
self.queryset = self.queryset.filter(**kwargs)
def _filter_prefer(self, prefer, prefer_id):
pass
def search(self, item):
qs = []
for i in ['hostname', 'ip', 'username']:
kwargs = {i + '__startswith': item}
qs.append(Q(**kwargs))
q = reduce(lambda x, y: x | y, qs)
clone = self._clone()
clone.queryset = clone.queryset.filter(q).distinct()
return clone
class SystemUserBackend(DBBackend):
model = SystemUser.assets.through
backend = 'system_user'
backend_display = _('System user')
prefer = backend
base_score = 0
union_id_length = 2
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.prefer:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(systemuser__id=prefer_id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser__id=system_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
if all((system_user, asset)):
system_user.assets.remove(asset)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def get_annotate(self):
kwargs = dict(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
name=F("systemuser__name"),
username=F("systemuser__username"),
password=F("systemuser__password"),
private_key=F("systemuser__private_key"),
public_key=F("systemuser__public_key"),
score=F("systemuser__priority") + self.base_score,
version=Value(0, IntegerField()),
date_created=F("systemuser__date_created"),
date_updated=F("systemuser__date_updated"),
asset_username=Concat(F("asset__id"), Value("_"),
F("systemuser__username"),
output_field=CharField()),
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
output_field=CharField()),
org_id=F("asset__org_id"),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
return kwargs
def get_filter(self):
return dict(
systemuser__username_same_with_user=False,
)
def all(self):
kwargs = self.get_annotate()
filters = self.get_filter()
qs = self.model.objects.all().annotate(**kwargs)
if not current_org.is_root():
filters['org_id'] = current_org.org_id()
qs = qs.filter(**filters)
qs = self.qs_to_values(qs)
return qs
class DynamicSystemUserBackend(SystemUserBackend):
backend = 'system_user_dynamic'
backend_display = _('System user(Dynamic)')
prefer = 'system_user'
union_id_length = 3
def get_annotate(self):
kwargs = super().get_annotate()
kwargs.update(dict(
name=Concat(
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
output_field=CharField()
),
username=F("systemuser__users__username"),
asset_username=Concat(
F("asset__id"), Value("_"),
F("systemuser__users__username"),
output_field=CharField()
),
union_id=Concat(
F("systemuser_id"), Value("_"), F("asset_id"),
Value("_"), F("systemuser__users__id"),
output_field=CharField()
),
users_count=Count('systemuser__users'),
))
return kwargs
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser_id=system_user_id,
union_id=union_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
if not system_user:
return
system_user.users.remove(user_id)
if system_user.users.count() == 0:
system_user.assets.remove(asset_id)
def get_filter(self):
return dict(
users_count__gt=0,
systemuser__username_same_with_user=True
)
class AdminUserBackend(DBBackend):
model = Asset
backend = 'admin_user'
backend_display = _('Admin user')
prefer = backend
base_score = 200
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.backend:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(admin_user__id=prefer_id)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(nodes__id=node.id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
admin_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=asset_id, admin_user_id=admin_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
raise PermissionDenied(_("Could not remove asset admin user"))
def all(self):
qs = self.model.objects.all().annotate(
asset_id=F("id"),
name=F("admin_user__name"),
username=F("admin_user__username"),
password=F("admin_user__password"),
private_key=F("admin_user__private_key"),
public_key=F("admin_user__public_key"),
score=Value(self.base_score, IntegerField()),
version=Value(0, IntegerField()),
date_updated=F("admin_user__date_updated"),
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs
class AuthbookBackend(DBBackend):
model = AuthBook
backend = 'db'
backend_display = _('Database')
prefer = backend
base_score = 400
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def _filter_prefer(self, prefer, prefer_id):
if not prefer or not prefer_id:
return
if prefer.lower() == "admin_user":
model = AdminUser
elif prefer.lower() == "system_user":
model = SystemUser
else:
self.queryset = self.queryset.none()
return
obj = get_object_or_none(model, pk=prefer_id)
if obj is None:
self.queryset = self.queryset.none()
return
username = obj.get_username()
if isinstance(username, str):
self.queryset = self.queryset.filter(username=username)
# dynamic system user return more username
else:
self.queryset = self.queryset.filter(username__in=username)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=authbook_id, asset_id=asset_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
authbook = get_object_or_none(AuthBook, pk=authbook_id)
if authbook.is_latest:
raise PermissionDenied(_("Latest version could not be delete"))
AuthBook.objects.filter(id=authbook_id).delete()
def all(self):
qs = self.model.objects.all().annotate(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
score=F('version') + self.base_score,
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs

View File

@ -1,162 +0,0 @@
# -*- coding: utf-8 -*-
#
from itertools import chain, groupby
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from orgs.utils import current_org
from common.utils import get_logger, lazyproperty
from common.struct import QuerySetChain
from ..models import AssetUser, AuthBook
from .db import (
AuthbookBackend, SystemUserBackend, AdminUserBackend,
DynamicSystemUserBackend
)
logger = get_logger(__name__)
class NotSupportError(Exception):
pass
class AssetUserQueryset:
ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned
def __init__(self, backends=()):
self.backends = backends
self._distinct_queryset = None
def backends_queryset(self):
return [b.get_queryset() for b in self.backends]
@lazyproperty
def backends_counts(self):
return [b.count() for b in self.backends]
def filter(self, hostname=None, ip=None, username=None,
assets=None, asset=None, node=None,
id=None, prefer_id=None, prefer=None, id__in=None):
if not assets and asset:
assets = [asset]
kwargs = dict(
hostname=hostname, ip=ip, username=username,
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
id__in=id__in, union_id=id,
)
logger.debug("Filter: {}".format(kwargs))
backends = []
for backend in self.backends:
clone = backend.filter(**kwargs)
backends.append(clone)
return self._clone(backends)
def _clone(self, backends=None):
if backends is None:
backends = self.backends
return self.__class__(backends)
def search(self, item):
backends = []
for backend in self.backends:
new = backend.search(item)
backends.append(new)
return self._clone(backends)
def distinct(self):
logger.debug("Distinct asset user queryset")
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
queryset_sorted = sorted(
queryset_chain,
key=lambda item: (item["asset_username"], item["score"]),
reverse=True,
)
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
final = [next(result[1]) for result in results]
self._distinct_queryset = final
return self
def get(self, latest=False, **kwargs):
queryset = self.filter(**kwargs)
if latest:
queryset = queryset.distinct()
queryset = list(queryset)
count = len(queryset)
if count == 1:
data = queryset[0]
return data
elif count > 1:
msg = 'Should return 1 record, but get {}'.format(count)
raise MultipleObjectsReturned(msg)
else:
msg = 'No record found(org is {})'.format(current_org.name)
raise ObjectDoesNotExist(msg)
def get_latest(self, **kwargs):
return self.get(latest=True, **kwargs)
@staticmethod
def to_asset_user(data):
obj = AssetUser()
for k, v in data.items():
setattr(obj, k, v)
return obj
@property
def queryset(self):
if self._distinct_queryset is not None:
return self._distinct_queryset
return QuerySetChain(self.backends_queryset())
def count(self):
if self._distinct_queryset is not None:
return len(self._distinct_queryset)
else:
return sum(self.backends_counts)
def __getitem__(self, ndx):
return self.queryset.__getitem__(ndx)
def __iter__(self):
self._data = iter(self.queryset)
return self
def __next__(self):
return self.to_asset_user(next(self._data))
class AssetUserManager:
support_backends = (
('db', AuthbookBackend),
('system_user', SystemUserBackend),
('admin_user', AdminUserBackend),
('system_user_dynamic', DynamicSystemUserBackend),
)
def __init__(self):
self.backends = [backend() for name, backend in self.support_backends]
self._queryset = AssetUserQueryset(self.backends)
def all(self):
return self._queryset
def delete(self, obj):
name_backends_map = dict(self.support_backends)
backend_name = obj.backend
backend_cls = name_backends_map.get(backend_name)
union_id = obj.union_id
if backend_cls:
backend_cls().delete(union_id)
else:
raise ObjectDoesNotExist("Not backend found")
@staticmethod
def create(**kwargs):
# 使用create方法创建AuthBook对象解决并发创建问题添加锁机制
authbook = AuthBook.create(**kwargs)
return authbook
def __getattr__(self, item):
return getattr(self._queryset, item)

View File

@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
#
# from django.conf import settings
# from .vault import VaultBackend

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
#

View File

@ -0,0 +1,66 @@
# Generated by Django 3.1.6 on 2021-06-04 16:46
from django.db import migrations, models, transaction
def migrate_admin_user_to_system_user(apps, schema_editor):
admin_user_model = apps.get_model("assets", "AdminUser")
system_user_model = apps.get_model("assets", "SystemUser")
db_alias = schema_editor.connection.alias
admin_users = admin_user_model.objects.using(db_alias).all()
print()
for admin_user in admin_users:
kwargs = {}
for attr in [
'id', 'org_id', 'username', 'password', 'private_key', 'public_key',
'comment', 'date_created', 'date_updated', 'created_by',
]:
value = getattr(admin_user, attr)
kwargs[attr] = value
name = admin_user.name
exist = system_user_model.objects.using(db_alias).filter(
name=admin_user.name, org_id=admin_user.org_id
).exists()
if exist:
name = admin_user.name + '_' + str(admin_user.id)[:5]
kwargs.update({
'name': name,
'type': 'admin',
'protocol': 'ssh',
'auto_push': False,
})
with transaction.atomic():
s = system_user_model(**kwargs)
s.save()
print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name))
assets = admin_user.assets.all()
s.assets.set(assets)
class Migration(migrations.Migration):
dependencies = [
('assets', '0070_auto_20210426_1515'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='type',
field=models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type'),
),
migrations.AlterField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic managed'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.RunPython(migrate_admin_user_to_system_user)
]

View File

@ -0,0 +1,85 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10
import common.fields.model
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid
from django.utils import timezone
from django.db import migrations, transaction
def migrate_old_authbook_to_history(apps, schema_editor):
authbook_model = apps.get_model("assets", "AuthBook")
history_model = apps.get_model("assets", "HistoricalAuthBook")
db_alias = schema_editor.connection.alias
print()
while True:
authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:20]
if not authbooks:
break
historys = []
authbook_ids = []
# Todo: 或许能优化成更新那样
for authbook in authbooks:
authbook_ids.append(authbook.id)
history = history_model()
for attr in [
'id', 'username', 'password', 'private_key', 'public_key', 'version',
'comment', 'created_by', 'asset', 'date_created', 'date_updated'
]:
setattr(history, attr, getattr(authbook, attr))
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate old auth book to history table: {} items".format(len(authbook_ids)))
history_model.objects.bulk_create(historys, ignore_conflicts=True)
authbook_model.objects.filter(id__in=authbook_ids).delete()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0071_systemuser_type'),
]
operations = [
migrations.CreateModel(
name='HistoricalAuthBook',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('asset', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.asset', verbose_name='Asset')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical AuthBook',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.RunPython(migrate_old_authbook_to_history)
]

View File

@ -0,0 +1,105 @@
# Generated by Django 3.1.6 on 2021-06-06 03:42
from django.utils import timezone
from django.db import migrations, models, transaction
import django.db.models.deletion
def migrate_system_assets_to_authbook(apps, schema_editor):
system_user_model = apps.get_model("assets", "SystemUser")
system_user_asset_model = system_user_model.assets.through
authbook_model = apps.get_model('assets', 'AuthBook')
history_model = apps.get_model("assets", "HistoricalAuthBook")
print()
system_users = system_user_model.objects.all()
for s in system_users:
while True:
systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:20]
if not systemuser_asset_relations:
break
authbooks = []
relations_ids = []
historys = []
for i in systemuser_asset_relations:
authbook = authbook_model(asset=i.asset, systemuser=i.systemuser, org_id=s.org_id)
authbooks.append(authbook)
relations_ids.append(i.id)
history = history_model(
asset=i.asset, systemuser=i.systemuser,
date_created=timezone.now(), date_updated=timezone.now(),
)
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate system user assets relations: {} items".format(len(relations_ids)))
authbook_model.objects.bulk_create(authbooks, ignore_conflicts=True)
history_model.objects.bulk_create(historys)
system_user_asset_model.objects.filter(id__in=relations_ids).delete()
def migrate_authbook_secret_to_system_user(apps, schema_editor):
authbook_model = apps.get_model('assets', 'AuthBook')
history_model = apps.get_model('assets', 'HistoricalAuthBook')
print()
authbooks_without_systemuser = authbook_model.objects.filter(systemuser__isnull=True)
for authbook in authbooks_without_systemuser:
matched = authbook_model.objects.filter(
asset=authbook.asset, systemuser__username=authbook.username
)
if not matched:
continue
historys = []
for i in matched:
history = history_model(
asset=i.asset, systemuser=i.systemuser,
date_created=timezone.now(), date_updated=timezone.now(),
version=authbook.version
)
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate secret to system user assets account: {} items".format(len(historys)))
matched.update(password=authbook.password, private_key=authbook.private_key,
public_key=authbook.public_key, version=authbook.version)
history_model.objects.bulk_create(historys)
class Migration(migrations.Migration):
dependencies = [
('assets', '0072_historicalauthbook'),
]
operations = [
migrations.AddField(
model_name='authbook',
name='systemuser',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user'),
),
migrations.AddField(
model_name='historicalauthbook',
name='systemuser',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user'),
),
migrations.AlterUniqueTogether(
name='authbook',
unique_together={('username', 'asset', 'systemuser')},
),
migrations.RunPython(migrate_system_assets_to_authbook),
migrations.RunPython(migrate_authbook_secret_to_system_user),
migrations.RemoveField(
model_name='authbook',
name='is_latest',
),
migrations.RemoveField(
model_name='historicalauthbook',
name='is_latest',
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.1.6 on 2021-06-06 03:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0073_auto_20210606_1142'),
]
operations = [
migrations.RemoveField(
model_name='systemuser',
name='assets',
),
migrations.RenameField(
model_name='asset',
old_name='admin_user',
new_name='_admin_user',
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, related_name='system_users', through='assets.AuthBook', to='assets.Asset', verbose_name='Assets'),
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 3.1 on 2021-07-05 09:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0074_remove_systemuser_assets'),
]
operations = [
migrations.AddField(
model_name='asset',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='asset',
name='date_verified',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='authbook',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='authbook',
name='date_verified',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='historicalauthbook',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='historicalauthbook',
name='date_verified',
field=models.DateTimeField(null=True),
),
migrations.AlterField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='gateway',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@ -2,7 +2,6 @@ from .base import *
from .asset import * from .asset import *
from .label import Label from .label import Label
from .user import * from .user import *
from .asset_user import *
from .cluster import * from .cluster import *
from .group import * from .group import *
from .domain import * from .domain import *

View File

@ -4,18 +4,20 @@
import uuid import uuid
import logging import logging
import random
from functools import reduce from functools import reduce
from collections import OrderedDict from collections import OrderedDict
from django.utils import timezone
from django.db import models from django.db import models
from common.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField from common.fields.model import JsonDictTextField
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import ConnectivityMixin
from .utils import Connectivity from .base import AbsConnectivity
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet'] __all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,16 +59,12 @@ class AssetQuerySet(models.QuerySet):
class ProtocolsMixin: class ProtocolsMixin:
protocols = '' protocols = ''
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp' class Protocol(TextChoices):
PROTOCOL_TELNET = 'telnet' ssh = 'ssh', 'SSH'
PROTOCOL_VNC = 'vnc' rdp = 'rdp', 'RDP'
PROTOCOL_CHOICES = ( telnet = 'telnet', 'Telnet'
(PROTOCOL_SSH, 'ssh'), vnc = 'vnc', 'VNC'
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'),
)
@property @property
def protocols_as_list(self): def protocols_as_list(self):
@ -167,7 +165,7 @@ class Platform(models.Model):
# ordering = ('name',) # ordering = ('name',)
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
# Important # Important
PLATFORM_CHOICES = ( PLATFORM_CHOICES = (
('Linux', 'Linux'), ('Linux', 'Linux'),
@ -182,8 +180,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
protocol = models.CharField(max_length=128, default=ProtocolsMixin.PROTOCOL_SSH, protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh,
choices=ProtocolsMixin.PROTOCOL_CHOICES, choices=ProtocolsMixin.Protocol.choices,
verbose_name=_('Protocol')) verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
@ -193,7 +191,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth # Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets') _admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets')
# Some information # Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
@ -223,11 +221,26 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager.from_queryset(AssetQuerySet)() objects = AssetManager.from_queryset(AssetQuerySet)()
_connectivity = None
def __str__(self): def __str__(self):
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
@property
def admin_user(self):
return self.system_users.filter(type='admin').first()
@admin_user.setter
def admin_user(self, system_user):
if not system_user:
return
if system_user.type != 'admin':
raise ValidationError('System user should be type admin')
system_user.assets.add(self)
def remove_admin_user(self):
from ..models import AuthBook
AuthBook.objects.filter(asset=self, systemuser__type='admin').delete()
@property @property
def is_valid(self): def is_valid(self):
warning = '' warning = ''
@ -276,23 +289,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
else: else:
return '' return ''
@property
def connectivity(self):
if self._connectivity:
return self._connectivity
if not self.admin_user_username:
return Connectivity.unknown()
connectivity = ConnectivityMixin.get_asset_username_connectivity(
self, self.admin_user_username
)
return connectivity
@connectivity.setter
def connectivity(self, value):
if not self.admin_user:
return
self.admin_user.set_asset_connectivity(self, value)
def get_auth_info(self): def get_auth_info(self):
if not self.admin_user: if not self.admin_user:
return {} return {}

View File

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
#
from .authbook import AuthBook
class AssetUser(AuthBook):
hostname = ""
ip = ""
backend = ""
backend_display = ""
union_id = ""
asset_username = ""
class Meta:
proxy = True

View File

@ -1,92 +1,94 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db import models, transaction from django.db import models
from django.db.models import Max
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied
from orgs.mixins.models import OrgManager from simple_history.models import HistoricalRecords
from .base import BaseUser
from .base import BaseUser, AbsConnectivity
__all__ = ['AuthBook'] __all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet): class AuthBook(BaseUser, AbsConnectivity):
def delete(self):
if self.count() > 1:
raise PermissionDenied(_("Bulk delete deny"))
return super().delete()
class AuthBookManager(OrgManager):
pass
class AuthBook(BaseUser):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version')) version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords()
objects = AuthBookManager.from_queryset(AuthBookQuerySet)() auth_attrs = ['username', 'password', 'private_key', 'public_key']
backend = "db"
# 用于system user和admin_user的动态设置
_connectivity = None
CONN_CACHE_KEY = "ASSET_USER_CONN_{}"
class Meta: class Meta:
verbose_name = _('AuthBook') verbose_name = _('AuthBook')
unique_together = [('username', 'asset', 'systemuser')]
def get_related_assets(self): def __init__(self, *args, **kwargs):
return [self.asset] super().__init__(*args, **kwargs)
self.auth_snapshot = {}
self.load_auth()
def generate_id_with_asset(self, asset): def get_or_systemuser_attr(self, attr):
return self.id val = getattr(self, attr, None)
if val:
return val
if self.systemuser:
return getattr(self.systemuser, attr, '')
return ''
@classmethod def load_auth(self):
def get_max_version(cls, username, asset): for attr in self.auth_attrs:
version_max = cls.objects.filter(username=username, asset=asset) \ value = self.get_or_systemuser_attr(attr)
.aggregate(Max('version')) self.auth_snapshot[attr] = [getattr(self, attr), value]
version_max = version_max['version__max'] or 0 setattr(self, attr, value)
return version_max
@classmethod def unload_auth(self):
def create(cls, **kwargs): if not self.systemuser:
""" return
使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时)
并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同) for attr, values in self.auth_snapshot.items():
同时设置自己的 is_latest=True, version=max_version + 1 origin_value, loaded_value = values
""" current_value = getattr(self, attr, '')
username = kwargs['username'] if current_value == loaded_value:
asset = kwargs.get('asset') or kwargs.get('asset_id') setattr(self, attr, origin_value)
with transaction.atomic():
# 使用select_for_update限制并发创建相同的username、asset条目 def save(self, *args, **kwargs):
instances = cls.objects.select_for_update().filter(username=username, asset=asset) self.unload_auth()
instances.filter(is_latest=True).update(is_latest=False) instance = super().save(*args, **kwargs)
max_version = cls.get_max_version(username, asset) self.load_auth()
kwargs.update({ return instance
'version': max_version + 1,
'is_latest': True
})
obj = cls.objects.create(**kwargs)
return obj
@property @property
def connectivity(self): def username_display(self):
return self.get_asset_connectivity(self.asset) return self.get_or_systemuser_attr('username') or '*'
@property @property
def keyword(self): def smart_name(self):
return '{}_#_{}'.format(self.username, str(self.asset.id)) username = self.username_display
@property if self.asset:
def hostname(self): asset = str(self.asset)
return self.asset.hostname else:
asset = '*'
return '{}@{}'.format(username, asset)
@property def sync_to_system_user_account(self):
def ip(self): if self.systemuser:
return self.asset.ip return
matched = AuthBook.objects.filter(
asset=self.asset, systemuser__username=self.username
)
if not matched:
return
for i in matched:
i.password = self.password
i.private_key = self.private_key
i.public_key = self.public_key
i.comment = 'Update triggered by account {}'.format(self.id)
i.save(update_fields=['password', 'private_key', 'public_key'])
def __str__(self): def __str__(self):
return '{}@{}'.format(self.username, self.asset) return self.smart_name

View File

@ -8,8 +8,10 @@ from hashlib import md5
import sshpubkeys import sshpubkeys
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django.db.models import QuerySet
from common.utils import random_string, signer from common.utils import random_string, signer
from common.utils import ( from common.utils import (
@ -19,85 +21,39 @@ from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric from common.validators import alphanumeric
from common import fields from common import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .utils import Connectivity
logger = get_logger(__file__) logger = get_logger(__file__)
class ConnectivityMixin: class Connectivity(models.TextChoices):
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY" unknown = 'unknown', _('Unknown')
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT" ok = 'ok', _('Ok')
ASSET_USER_CACHE_TIME = 3600 * 24 failed = 'failed', _('Failed')
id = ''
username = ''
@property
def part_id(self):
i = '-'.join(str(self.id).split('-')[:3])
return i
def set_connectivity(self, summary): class AbsConnectivity(models.Model):
unreachable = summary.get('dark', {}).keys() connectivity = models.CharField(
reachable = summary.get('contacted', {}).keys() choices=Connectivity.choices, default=Connectivity.unknown,
max_length=16, verbose_name=_('Connectivity')
)
date_verified = models.DateTimeField(null=True)
assets = self.get_related_assets() def set_connectivity(self, val):
if not isinstance(assets, list): self.connectivity = val
assets = assets.only('id', 'hostname', 'admin_user__id') self.date_verified = timezone.now()
for asset in assets: self.save(update_fields=['connectivity', 'date_verified'])
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)
@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
@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
@classmethod @classmethod
def get_asset_username_connectivity(cls, asset, username): def bulk_set_connectivity(cls, queryset_or_id, connectivity):
key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id) if not isinstance(queryset_or_id, QuerySet):
return Connectivity.get(key) queryset = cls.objects.filter(id__in=queryset_or_id)
else:
queryset = queryset_or_id
queryset.update(connectivity=connectivity, date_verified=timezone.now())
def get_asset_connectivity(self, asset): class Meta:
key = self.get_asset_connectivity_key(asset) abstract = True
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)
class AuthMixin: class AuthMixin:
@ -105,7 +61,6 @@ class AuthMixin:
password = '' password = ''
public_key = '' public_key = ''
username = '' username = ''
_prefer = 'system_user'
@property @property
def ssh_key_fingerprint(self): def ssh_key_fingerprint(self):
@ -173,38 +128,6 @@ class AuthMixin:
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
def has_special_auth(self, asset=None, username=None):
from .authbook import AuthBook
if username is None:
username = self.username
queryset = AuthBook.objects.filter(username=username)
if asset:
queryset = queryset.filter(asset=asset)
return queryset.exists()
def get_asset_user(self, asset, username=None):
from ..backends import AssetUserManager
if username is None:
username = self.username
try:
manager = AssetUserManager()
other = manager.get_latest(
username=username, asset=asset,
prefer_id=self.id, prefer=self._prefer,
)
return other
except Exception as e:
logger.error(e, exc_info=True)
return None
def load_asset_special_auth(self, asset=None, username=None):
if not asset:
return self
instance = self.get_asset_user(asset, username=username)
if instance:
self._merge_auth(instance)
def _merge_auth(self, other): def _merge_auth(self, other):
if other.password: if other.password:
self.password = other.password self.password = other.password
@ -244,7 +167,7 @@ class AuthMixin:
) )
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): class BaseUser(OrgModelMixin, AuthMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
@ -259,8 +182,6 @@ class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 600 ASSET_USER_CACHE_TIME = 600
_prefer = "system_user"
def get_related_assets(self): def get_related_assets(self):
assets = self.assets.filter(org_id=self.org_id) assets = self.assets.filter(org_id=self.org_id)
return assets return assets

View File

@ -7,6 +7,7 @@ import re
import paramiko import paramiko
from django.db import models from django.db import models
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils.strings import no_special_chars from common.utils.strings import no_special_chars
@ -43,15 +44,12 @@ class Domain(OrgModelMixin):
class Gateway(BaseUser): class Gateway(BaseUser):
PROTOCOL_SSH = 'ssh' class Protocol(TextChoices):
PROTOCOL_RDP = 'rdp' ssh = 'ssh', 'SSH'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
)
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol")) protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol"))
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))

View File

@ -10,15 +10,278 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache from django.core.cache import cache
from common.utils import signer, get_object_or_none from common.utils import signer, get_object_or_none
from common.exceptions import JMSException from common.db.models import TextChoices
from .base import BaseUser from .base import BaseUser
from .asset import Asset from .asset import Asset
from .authbook import AuthBook
__all__ = ['AdminUser', 'SystemUser'] __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProtocolMixin:
protocol: str
class Protocol(TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL'
k8s = 'k8s', 'K8S'
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
ASSET_CATEGORY_PROTOCOLS = [
Protocol.ssh, Protocol.rdp, Protocol.telnet, Protocol.vnc
]
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
Protocol.rdp
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s
]
APPLICATION_CATEGORY_PROTOCOLS = [
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
*APPLICATION_CATEGORY_DB_PROTOCOLS,
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
]
@property
def is_protocol_support_push(self):
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
@classmethod
def get_protocol_by_application_type(cls, app_type):
from applications.const import ApplicationTypeChoices
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in ApplicationTypeChoices.remote_app_types():
protocol = cls.Protocol.rdp
else:
protocol = None
return protocol
@property
def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
class AuthMixin:
username_same_with_user: bool
protocol: str
ASSET_CATEGORY_PROTOCOLS: list
login_mode: str
LOGIN_MANUAL: str
id: str
username: str
password: str
private_key: str
public_key: str
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
if not auth:
raise ValueError('Auth not set')
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Set system user temp auth: {key}')
cache.set(key, auth, ttl)
def get_temp_auth(self, asset_or_app_id, user_id):
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Get system user temp auth: {key}')
password = cache.get(key)
return password
def load_tmp_auth_if_has(self, asset_or_app_id, user):
if not asset_or_app_id or not user:
return
if self.login_mode != self.LOGIN_MANUAL:
return
auth = self.get_temp_auth(asset_or_app_id, user)
if not auth:
return
username = auth.get('username')
password = auth.get('password')
if username:
self.username = username
if password:
self.password = password
def load_app_more_auth(self, app_id=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
if not user_id:
return
user = get_object_or_none(User, pk=user_id)
if not user:
return
self.load_tmp_auth_if_has(app_id, user)
def load_asset_special_auth(self, asset, username=''):
"""
"""
authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self))
if len(authbooks) == 0:
return None
elif len(authbooks) == 1:
authbook = authbooks[0]
else:
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
authbook = authbooks[0]
self.password = authbook.password
self.private_key = authbook.private_key
self.public_key = authbook.public_key
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
asset = None
if asset_id:
asset = get_object_or_none(Asset, pk=asset_id)
# 没有资产就没有必要继续了
if not asset:
logger.debug('Asset not found, pass')
return
user = None
if user_id:
user = get_object_or_none(User, pk=user_id)
_username = self.username
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
self.username = _username
# 加载某个资产的特殊配置认证信息
self.load_asset_special_auth(asset, _username)
self.load_tmp_auth_if_has(asset_id, user)
class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(LOGIN_AUTO, _('Automatic managed')),
(LOGIN_MANUAL, _('Manually input'))
)
class Type(TextChoices):
common = 'common', _('Common user')
admin = 'admin', _('Admin user')
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets"),
through='assets.AuthBook', through_fields=['systemuser', 'asset'],
related_name='system_users'
)
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type'))
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
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)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
def __str__(self):
username = self.username
if self.username_same_with_user:
username = 'dynamic'
return '{0.name}({1})'.format(self, username)
@property
def nodes_amount(self):
return self.nodes.all().count()
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.is_protocol_support_push:
return True
else:
return False
@property
def is_admin_user(self):
return self.type == self.Type.admin
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.Protocol.rdp, self.Protocol.vnc]
@property
def is_need_test_asset_connective(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).distinct()
return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ActionChoices.allow:
return True, None
elif action == rule.ActionChoices.deny:
return False, matched_cmd
return True, None
def get_all_assets(self):
from assets.models import Node
nodes_keys = self.nodes.all().values_list('key', flat=True)
asset_ids = set(self.assets.all().values_list('id', flat=True))
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
assets = Asset.objects.filter(id__in=asset_ids)
return assets
def save(self, *args, **kwargs):
if self.username_same_with_user:
self.username = '*'
return super().save(*args, **kwargs)
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")
# Todo: 准备废弃
class AdminUser(BaseUser): class AdminUser(BaseUser):
""" """
A privileged user that ansible can use it to push system user and so on A privileged user that ansible can use it to push system user and so on
@ -65,243 +328,3 @@ class AdminUser(BaseUser):
ordering = ['name'] ordering = ['name']
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Admin user") verbose_name = _("Admin user")
class SystemUser(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql'
PROTOCOL_ORACLE = 'oracle'
PROTOCOL_MARIADB = 'mariadb'
PROTOCOL_POSTGRESQL = 'postgresql'
PROTOCOL_K8S = 'k8s'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'),
(PROTOCOL_ORACLE, 'oracle'),
(PROTOCOL_MARIADB, 'mariadb'),
(PROTOCOL_POSTGRESQL, 'postgresql'),
(PROTOCOL_K8S, 'k8s'),
)
SUPPORT_PUSH_PROTOCOLS = [PROTOCOL_SSH, PROTOCOL_RDP]
ASSET_CATEGORY_PROTOCOLS = [
PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC
]
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
PROTOCOL_RDP
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
PROTOCOL_K8S
]
APPLICATION_CATEGORY_PROTOCOLS = [
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
*APPLICATION_CATEGORY_DB_PROTOCOLS,
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
]
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login'))
)
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
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)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
_prefer = 'system_user'
def __str__(self):
username = self.username
if self.username_same_with_user:
username = 'dynamic'
return '{0.name}({1})'.format(self, username)
def get_username(self):
if self.username_same_with_user:
return list(self.users.values_list('username', flat=True))
else:
return self.username
@property
def nodes_amount(self):
return self.nodes.all().count()
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.is_protocol_support_push:
return True
else:
return False
@property
def is_protocol_support_push(self):
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC]
@property
def is_need_test_asset_connective(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def has_special_auth(self, asset=None, username=None):
if username is None and self.username_same_with_user:
raise TypeError('System user is dynamic, username should be pass')
return super().has_special_auth(asset=asset, username=username)
@property
def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def _merge_auth(self, other):
super()._merge_auth(other)
if self.username_same_with_user:
self.username = other.username
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
if not auth:
raise ValueError('Auth not set')
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Set system user temp auth: {key}')
cache.set(key, auth, ttl)
def get_temp_auth(self, asset_or_app_id, user_id):
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Get system user temp auth: {key}')
password = cache.get(key)
return password
def load_tmp_auth_if_has(self, asset_or_app_id, user):
if not asset_or_app_id or not user:
return
if self.login_mode != self.LOGIN_MANUAL:
pass
auth = self.get_temp_auth(asset_or_app_id, user)
if not auth:
return
username = auth.get('username')
password = auth.get('password')
if username:
self.username = username
if password:
self.password = password
def load_app_more_auth(self, app_id=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
if not user_id:
return
user = get_object_or_none(User, pk=user_id)
if not user:
return
self.load_tmp_auth_if_has(app_id, user)
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
asset = None
if asset_id:
asset = get_object_or_none(Asset, pk=asset_id)
# 没有资产就没有必要继续了
if not asset:
logger.debug('Asset not found, pass')
return
user = None
if user_id:
user = get_object_or_none(User, pk=user_id)
_username = self.username
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
# 加载某个资产的特殊配置认证信息
try:
self.load_asset_special_auth(asset, _username)
except Exception as e:
logger.error('Load special auth Error: ', e)
pass
self.load_tmp_auth_if_has(asset_id, user)
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).distinct()
return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ActionChoices.allow:
return True, None
elif action == rule.ActionChoices.deny:
return False, matched_cmd
return True, None
def get_all_assets(self):
from assets.models import Node
nodes_keys = self.nodes.all().values_list('key', flat=True)
asset_ids = set(self.assets.all().values_list('id', flat=True))
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
assets = Asset.objects.filter(id__in=asset_ids)
return assets
@classmethod
def get_protocol_by_application_type(cls, app_type):
from applications.const import ApplicationTypeChoices
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in ApplicationTypeChoices.remote_app_types():
protocol = cls.PROTOCOL_RDP
else:
protocol = None
return protocol
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")

View File

@ -11,7 +11,7 @@ from common.utils import validate_ssh_private_key
__all__ = [ __all__ = [
'init_model', 'generate_fake', 'private_key_validator', 'Connectivity', 'init_model', 'generate_fake', 'private_key_validator',
] ]
@ -35,74 +35,3 @@ def private_key_validator(value):
_('%(value)s is not an even number'), _('%(value)s is not an even number'),
params={'value': value}, params={'value': value},
) )
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=None):
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()

View File

@ -8,6 +8,6 @@ from .system_user import *
from .node import * from .node import *
from .domain import * from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .asset_user import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account import *

View File

@ -0,0 +1,42 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from assets.models import AuthBook
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
ip = serializers.ReadOnlyField(label=_("IP"))
hostname = serializers.ReadOnlyField(label=_("Hostname"))
class Meta:
model = AuthBook
fields_mini = ['id', 'username', 'ip', 'hostname', 'version']
fields_write_only = ['password', 'private_key', "public_key"]
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset', 'systemuser']
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('systemuser', 'asset')
return queryset
class AccountSecretSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
'public_key': {'write_only': False},
}

View File

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..models import Node, AdminUser from ..models import SystemUser
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializer, AuthSerializerMixin from .base import AuthSerializerMixin
class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
@ -15,7 +14,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
""" """
class Meta: class Meta:
model = AdminUser model = SystemUser
fields_mini = ['id', 'name', 'username'] fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'private_key', 'public_key'] fields_write_only = ['password', 'private_key', 'public_key']
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
@ -34,39 +33,8 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'assets_amount': {'label': _('Asset')}, 'assets_amount': {'label': _('Asset')},
} }
def create(self, validated_data):
class AdminUserDetailSerializer(AdminUserSerializer): data = {k: v for k, v in validated_data.items()}
class Meta(AdminUserSerializer.Meta): data['protocol'] = 'ssh'
fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint'] data['type'] = SystemUser.Type.admin
return super().create(data)
class AdminUserAuthSerializer(AuthSerializer):
class Meta:
model = AdminUser
fields = ['password', 'private_key']
class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
"""
管理用户更新关联到的集群
"""
nodes = serializers.PrimaryKeyRelatedField(
many=True, queryset=Node.objects
)
class Meta:
model = AdminUser
fields = ['id', 'nodes']
class TaskIDSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)
class AssetUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)

View File

@ -1,24 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.db.models import F
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node, Platform from ..models import Asset, Node, Platform, SystemUser
from .base import ConnectivitySerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'AssetVerboseSerializer',
'AssetDisplaySerializer',
'ProtocolsField', 'PlatformSerializer', 'ProtocolsField', 'PlatformSerializer',
'AssetDetailSerializer', 'AssetTaskSerializer', 'AssetTaskSerializer',
] ]
class ProtocolField(serializers.RegexField): class ProtocolField(serializers.RegexField):
protocols = '|'.join(dict(Asset.PROTOCOL_CHOICES).keys()) protocols = '|'.join(dict(Asset.Protocol.choices).keys())
default_error_messages = { default_error_messages = {
'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535')) 'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535'))
} }
@ -65,9 +62,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
platform = serializers.SlugRelatedField( platform = serializers.SlugRelatedField(
slug_field='name', queryset=Platform.objects.all(), label=_("Platform") slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
) )
admin_user = serializers.PrimaryKeyRelatedField(
queryset=SystemUser.objects, label=_('Admin user'), write_only=True
)
protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22']) protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name')) domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
""" """
@ -81,25 +80,18 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
'created_by', 'date_created', 'hardware_info', 'hardware_info', 'connectivity', 'date_verified'
] ]
fields_fk = [ fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform' 'domain', 'domain_display', 'platform', 'admin_user'
] ]
fk_only_fields = {
'platform': ['name']
}
fields_m2m = [ fields_m2m = [
'nodes', 'nodes_display', 'labels', 'nodes', 'nodes_display', 'labels',
] ]
annotates_fields = {
# 'admin_user_display': 'admin_user__name'
}
fields_as = list(annotates_fields.keys())
fields = fields_small + fields_fk + fields_m2m + fields_as
read_only_fields = [ read_only_fields = [
'created_by', 'date_created', 'created_by', 'date_created',
] + fields_as ]
fields = fields_small + fields_fk + fields_m2m + read_only_fields
extra_kwargs = { extra_kwargs = {
'protocol': {'write_only': True}, 'protocol': {'write_only': True},
@ -108,10 +100,19 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'org_name': {'label': _('Org name')} 'org_name': {'label': _('Org name')}
} }
def get_fields(self):
fields = super().get_fields()
admin_user_field = fields.get('admin_user')
# 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user
if admin_user_field:
admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin)
return fields
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('admin_user', 'domain', 'platform') queryset = queryset.prefetch_related('domain', 'platform')
queryset = queryset.prefetch_related('nodes', 'labels') queryset = queryset.prefetch_related('nodes', 'labels')
return queryset return queryset
@ -146,25 +147,26 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
def create(self, validated_data): def create(self, validated_data):
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
nodes_display = validated_data.pop('nodes_display', '') nodes_display = validated_data.pop('nodes_display', '')
admin_user = validated_data.pop('admin_user', '')
instance = super().create(validated_data) instance = super().create(validated_data)
self.perform_nodes_display_create(instance, nodes_display) self.perform_nodes_display_create(instance, nodes_display)
instance.admin_user = admin_user
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
nodes_display = validated_data.pop('nodes_display', '') nodes_display = validated_data.pop('nodes_display', '')
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
admin_user = validated_data.pop('admin_user', '')
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
self.perform_nodes_display_create(instance, nodes_display) self.perform_nodes_display_create(instance, nodes_display)
instance.admin_user = admin_user
return instance return instance
class AssetDisplaySerializer(AssetSerializer): class AssetVerboseSerializer(AssetSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) admin_user = serializers.PrimaryKeyRelatedField(
queryset=SystemUser.objects, label=_('Admin user')
class Meta(AssetSerializer.Meta): )
fields = AssetSerializer.Meta.fields + [
'connectivity',
]
class PlatformSerializer(serializers.ModelSerializer): class PlatformSerializer(serializers.ModelSerializer):
@ -186,16 +188,11 @@ class PlatformSerializer(serializers.ModelSerializer):
] ]
class AssetDetailSerializer(AssetSerializer):
platform = PlatformSerializer(read_only=True)
class AssetSimpleSerializer(serializers.ModelSerializer): class AssetSimpleSerializer(serializers.ModelSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'hostname', 'ip', 'connectivity', 'port'] fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
class AssetTaskSerializer(serializers.Serializer): class AssetTaskSerializer(serializers.Serializer):

View File

@ -1,98 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset
from ..backends import AssetUserManager
from .base import ConnectivitySerializer, AuthSerializerMixin
__all__ = [
'AssetUserWriteSerializer', 'AssetUserReadSerializer',
'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
]
class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = AuthBook
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'username']
fields_write_only = ['password', 'private_key', "public_key"]
fields_small = fields_mini + fields_write_only + ['comment']
fields_fk = ['asset']
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
def create(self, validated_data):
if not validated_data.get("name") and validated_data.get("username"):
validated_data["name"] = validated_data["username"]
instance = AssetUserManager.create(**validated_data)
return instance
class AssetUserReadSerializer(AssetUserWriteSerializer):
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
asset = serializers.CharField(source='asset_id', label=_('Asset'))
backend = serializers.CharField(read_only=True, label=_("Backend"))
backend_display = serializers.CharField(read_only=True, label=_("Source"))
class Meta(AssetUserWriteSerializer.Meta):
read_only_fields = (
'date_created', 'date_updated',
'created_by', 'version',
)
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'private_key', "public_key"]
fields_small = fields_mini + fields_write_only + [
'backend', 'backend_display', 'version',
'date_created', "date_updated",
'comment'
]
fields_fk = ['asset', 'hostname', 'ip']
fields = fields_small + fields_fk
extra_kwargs = {
'name': {'required': False},
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True,
required=False, label=_('Password')
)
public_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True,
required=False, label=_('Public key')
)
private_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True,
required=False, label=_('Private key')
)
class AssetUserPushSerializer(serializers.Serializer):
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
username = serializers.CharField(max_length=1024)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass

View File

@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, validate_ssh_private_key from common.utils import ssh_pubkey_gen, validate_ssh_private_key
from ..models import AssetUser
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
@ -29,11 +28,6 @@ class AuthSerializer(serializers.ModelSerializer):
return self.instance return self.instance
class ConnectivitySerializer(serializers.Serializer):
status = serializers.IntegerField()
datetime = serializers.DateTimeField()
class AuthSerializerMixin: class AuthSerializerMixin:
def validate_password(self, password): def validate_password(self, password):
return password return password
@ -64,15 +58,3 @@ class AuthSerializerMixin:
def update(self, instance, validated_data): def update(self, instance, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().update(instance, validated_data) return super().update(instance, validated_data)
class AuthInfoSerializer(serializers.ModelSerializer):
private_key = serializers.ReadOnlyField(source='get_private_key')
class Meta:
model = AssetUser
fields = [
'username', 'password',
'private_key', 'public_key',
'date_updated',
]

View File

@ -3,7 +3,6 @@
import re import re
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule from ..models import CommandFilter, CommandFilterRule
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
@ -15,7 +14,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = CommandFilter model = CommandFilter
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + [
'org_id', 'org_name', 'org_id', 'org_name',
@ -48,7 +46,6 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
] ]
fields_fk = ['filter'] fields_fk = ['filter']
fields = '__all__' fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -3,7 +3,6 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.validators import NoSpecialChars from common.validators import NoSpecialChars
from ..models import Domain, Gateway from ..models import Domain, Gateway
@ -29,7 +28,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
extra_kwargs = { extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')}, 'assets': {'required': False, 'label': _('Assets')},
} }
list_serializer_class = AdaptedBulkListSerializer
@staticmethod @staticmethod
def get_asset_count(obj): def get_asset_count(obj):
@ -47,7 +45,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = Gateway model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_write_only = [ fields_write_only = [
'password', 'private_key', 'public_key', 'password', 'private_key', 'public_key',
@ -66,16 +63,6 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'public_key': {"write_only": True}, 'public_key': {"write_only": True},
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_limit_to_ssh()
def protocol_limit_to_ssh(self):
protocol_field = self.fields['protocol']
choices = protocol_field.choices
choices.pop('rdp')
protocol_field._choices = choices
class GatewayWithAuthSerializer(GatewaySerializer): class GatewayWithAuthSerializer(GatewaySerializer):
class Meta(GatewaySerializer.Meta): class Meta(GatewaySerializer.Meta):

View File

@ -4,7 +4,6 @@
from rest_framework import serializers from rest_framework import serializers
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from common.drf.serializers import AdaptedBulkListSerializer
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from ..models import FavoriteAsset from ..models import FavoriteAsset
@ -18,6 +17,5 @@ class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
) )
class Meta: class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = FavoriteAsset model = FavoriteAsset
fields = ['user', 'asset'] fields = ['user', 'asset']

View File

@ -3,7 +3,6 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Label from ..models import Label
@ -30,7 +29,6 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
extra_kwargs = { extra_kwargs = {
'assets': {'required': False} 'assets': {'required': False}
} }
list_serializer_class = AdaptedBulkListSerializer
@staticmethod @staticmethod
def get_asset_count(obj): def get_asset_count(obj):

View File

@ -2,7 +2,6 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count
from common.drf.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@ -23,15 +22,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
系统用户 系统用户
""" """
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True) auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
type_display = serializers.ReadOnlyField(source='get_type_display')
class Meta: class Meta:
model = SystemUser model = SystemUser
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name', 'username'] fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key'] fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
'protocol', 'login_mode', 'login_mode_display', 'priority', 'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display',
'sudo', 'shell', 'sftp_root', 'token', 'priority', 'sudo', 'shell', 'sftp_root', 'token',
'home', 'system_groups', 'ad_domain', 'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key', 'username_same_with_user', 'auto_push', 'auto_generate_key',
'date_created', 'date_updated', 'date_created', 'date_updated',
@ -55,9 +54,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
login_mode = self.initial_data.get("login_mode") login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol") protocol = self.initial_data.get("protocol")
if login_mode == SystemUser.LOGIN_MANUAL or \ if login_mode == SystemUser.LOGIN_MANUAL:
protocol in [SystemUser.PROTOCOL_TELNET, value = False
SystemUser.PROTOCOL_VNC]: elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
value = False value = False
return value return value
@ -71,7 +70,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
value = False value = False
elif login_mode == SystemUser.LOGIN_MANUAL: elif login_mode == SystemUser.LOGIN_MANUAL:
value = False value = False
elif protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]: elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
value = False value = False
return value return value
@ -80,7 +79,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return username_same_with_user return username_same_with_user
protocol = self.initial_data.get("protocol", "ssh") protocol = self.initial_data.get("protocol", "ssh")
queryset = SystemUser.objects.filter( queryset = SystemUser.objects.filter(
protocol=protocol, username_same_with_user=True protocol=protocol,
username_same_with_user=True
) )
if self.instance: if self.instance:
queryset = queryset.exclude(id=self.instance.id) queryset = queryset.exclude(id=self.instance.id)
@ -96,12 +96,14 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
login_mode = self.initial_data.get("login_mode") login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol") protocol = self.initial_data.get("protocol")
username_same_with_user = self.initial_data.get("username_same_with_user") username_same_with_user = self.initial_data.get("username_same_with_user")
if username_same_with_user:
return ''
if login_mode == SystemUser.LOGIN_AUTO and \ if login_mode == SystemUser.LOGIN_AUTO and \
protocol != SystemUser.PROTOCOL_VNC: protocol != SystemUser.Protocol.vnc:
msg = _('* Automatic login mode must fill in the username.') msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
if username_same_with_user:
username = '*'
return username return username
def validate_home(self, home): def validate_home(self, home):
@ -118,28 +120,41 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return value return value
@staticmethod
def validate_admin_user(attrs):
tp = attrs.get('type')
if tp != SystemUser.Type.admin:
return attrs
attrs['protocol'] = SystemUser.Protocol.ssh
attrs['login_mode'] = SystemUser.LOGIN_AUTO
attrs['username_same_with_user'] = False
attrs['auto_push'] = False
return attrs
def validate_password(self, password): def validate_password(self, password):
super().validate_password(password) super().validate_password(password)
auto_gen_key = self.initial_data.get("auto_generate_key", False) auto_gen_key = self.initial_data.get("auto_generate_key", False)
private_key = self.initial_data.get("private_key") private_key = self.initial_data.get("private_key")
login_mode = self.initial_data.get("login_mode") login_mode = self.initial_data.get("login_mode")
if not self.instance and not auto_gen_key and not password and \ if not self.instance and not auto_gen_key and not password and \
not private_key and login_mode == SystemUser.LOGIN_AUTO: not private_key and login_mode == SystemUser.LOGIN_AUTO:
raise serializers.ValidationError(_("Password or private key required")) raise serializers.ValidationError(_("Password or private key required"))
return password return password
def validate(self, attrs): def validate_gen_key(self, attrs):
username = attrs.get("username", "manual") username = attrs.get("username", "manual")
auto_gen_key = attrs.pop("auto_generate_key", False) auto_gen_key = attrs.pop("auto_generate_key", False)
protocol = attrs.get("protocol") protocol = attrs.get("protocol")
if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]: if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
return attrs return attrs
if auto_gen_key: # 自动生成
if auto_gen_key and not self.instance:
password = SystemUser.gen_password() password = SystemUser.gen_password()
attrs["password"] = password attrs["password"] = password
if protocol == SystemUser.PROTOCOL_SSH: if protocol == SystemUser.Protocol.ssh:
private_key, public_key = SystemUser.gen_key(username) private_key, public_key = SystemUser.gen_key(username)
attrs["private_key"] = private_key attrs["private_key"] = private_key
attrs["public_key"] = public_key attrs["public_key"] = public_key
@ -147,11 +162,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
elif attrs.get("private_key", None): elif attrs.get("private_key", None):
private_key = attrs["private_key"] private_key = attrs["private_key"]
password = attrs.get("password") password = attrs.get("password")
public_key = ssh_pubkey_gen(private_key, password=password, public_key = ssh_pubkey_gen(private_key, password=password, username=username)
username=username)
attrs["public_key"] = public_key attrs["public_key"] = public_key
return attrs return attrs
def validate(self, attrs):
attrs = self.validate_admin_user(attrs)
attrs = self.validate_gen_key(attrs)
return attrs
class SystemUserListSerializer(SystemUserSerializer): class SystemUserListSerializer(SystemUserSerializer):
@ -222,24 +241,26 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer):
fields.extend(['systemuser', "systemuser_display"]) fields.extend(['systemuser', "systemuser_display"])
return fields return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField() asset_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = SystemUser.assets.through model = SystemUser.assets.through
fields = [ fields = [
'id', "asset", "asset_display", "id", "asset", "asset_display",
'systemuser', 'systemuser_display'
] ]
use_model_bulk_create = True
model_bulk_create_kwargs = {
'ignore_conflicts': True
}
class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
node_display = serializers.SerializerMethodField() node_display = serializers.SerializerMethodField()
class Meta(RelationMixin.Meta): class Meta:
model = SystemUser.nodes.through model = SystemUser.nodes.through
fields = [ fields = [
'id', 'node', "node_display", 'id', 'node', "node_display",
@ -252,7 +273,7 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField() user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = SystemUser.users.through model = SystemUser.users.through
fields = [ fields = [
'id', "user", "user_display", 'id', "user", "user_display",

View File

@ -1,3 +1,5 @@
from .common import * from .asset import *
from .system_user import *
from .authbook import *
from .node_assets_amount import * from .node_assets_amount import *
from .node_assets_mapping import * from .node_assets_mapping import *

View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_delete, post_delete, pre_save
)
from django.dispatch import receiver
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node
from assets.tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets,
)
logger = get_logger(__file__)
def update_asset_hardware_info_on_created(asset):
logger.debug("Update asset `{}` hardware info".format(asset))
update_assets_hardware_info_util.delay([asset])
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
@receiver(pre_save, sender=Node)
def on_node_pre_save(sender, instance: Node, **kwargs):
instance.parent_key = instance.compute_parent_key()
@receiver(post_save, sender=Asset)
@on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
"""
当资产创建时更新硬件信息更新可连接性
确保资产必须属于一个节点
"""
if created:
logger.info("Asset create signal recv: {}".format(instance))
# 获取资产硬件信息
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 确保资产存在一个节点
has_node = instance.nodes.all().exists()
if not has_node:
instance.nodes.add(Node.org_root())
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
"""
本操作共访问 4 次数据库
当资产的节点发生变化时或者 当节点的资产关系发生变化时
节点下新增的资产添加到节点关联的系统用户中
"""
if action != POST_ADD:
return
logger.debug("Assets node add signal recv: {}".format(action))
if reverse:
nodes = [instance.key]
asset_ids = pk_set
else:
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
asset_ids = [instance.id]
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
nodes_ancestors_keys = set()
for node in nodes:
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
system_user_ids = SystemUser.objects.filter(
nodes__key__in=nodes_ancestors_keys
).distinct().values_list('id', flat=True)
# 查询所有已存在的关系
m2m_model = SystemUser.assets.through
exist = set(m2m_model.objects.filter(
system_user_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('system_user_id', 'asset_id'))
# TODO 优化
to_create = []
for system_user_id in system_user_ids:
asset_ids_to_push = []
for asset_id in asset_ids:
if (system_user_id, asset_id) in exist:
continue
asset_ids_to_push.append(asset_id)
to_create.append(m2m_model(
system_user_id=system_user_id,
asset_id=asset_id
))
if asset_ids_to_push:
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create)
RELATED_NODE_IDS = '_related_node_ids'
@receiver(pre_delete, sender=Asset)
def on_asset_delete(instance: Asset, using, **kwargs):
node_ids = set(Node.objects.filter(
assets=instance
).distinct().values_list('id', flat=True))
setattr(instance, RELATED_NODE_IDS, node_ids)
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
)
@receiver(post_delete, sender=Asset)
def on_asset_post_delete(instance: Asset, using, **kwargs):
node_ids = getattr(instance, RELATED_NODE_IDS, None)
if node_ids:
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
)

View File

@ -0,0 +1,46 @@
from django.dispatch import receiver
from django.apps import apps
from simple_history.signals import pre_create_historical_record
from django.db.models.signals import post_save, pre_save
from common.utils import get_logger
from orgs.utils import tmp_to_root_org
from ..models import AuthBook, SystemUser
AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook')
logger = get_logger(__name__)
@receiver(pre_create_historical_record, sender=AuthBookHistory)
def pre_create_historical_record_callback(sender, instance=None, history_instance=None, **kwargs):
attrs_to_copy = ['username', 'password', 'private_key']
for attr in attrs_to_copy:
if getattr(history_instance, attr):
continue
if not history_instance.systemuser:
continue
system_user_attr_value = getattr(history_instance.systemuser, attr)
if system_user_attr_value:
setattr(history_instance, attr, system_user_attr_value)
@receiver(post_save, sender=AuthBook)
def on_authbook_post_create(sender, instance, **kwargs):
# 去掉这个资产的管理用户
if instance.systemuser and instance.systemuser.is_admin_user:
with tmp_to_root_org():
deleted_count, other = AuthBook.objects.filter(
asset=instance.asset,
systemuser__type=SystemUser.Type.admin
).exclude(id=instance.id).delete()
logger.debug('Remove asset old admin user: {}'.format(deleted_count))
if not instance.systemuser:
instance.sync_to_system_user_account()
@receiver(pre_save, sender=AuthBook)
def on_authbook_pre_create(sender, instance, **kwargs):
# 升级版本号
instance.version = instance.history.all().count() + 1

View File

@ -1,223 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_delete, post_delete, pre_save
)
from django.dispatch import receiver
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node
from users.models import User
from assets.tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users
)
logger = get_logger(__file__)
def update_asset_hardware_info_on_created(asset):
logger.debug("Update asset `{}` hardware info".format(asset))
update_assets_hardware_info_util.delay([asset])
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
@receiver(pre_save, sender=Node)
def on_node_pre_save(sender, instance: Node, **kwargs):
instance.parent_key = instance.compute_parent_key()
@receiver(post_save, sender=Asset)
@on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
"""
当资产创建时更新硬件信息更新可连接性
确保资产必须属于一个节点
"""
if created:
logger.info("Asset create signal recv: {}".format(instance))
# 获取资产硬件信息
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 确保资产存在一个节点
has_node = instance.nodes.all().exists()
if not has_node:
instance.nodes.add(Node.org_root())
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
当系统用户更新时可能更新了秘钥用户名等这时要自动推送系统用户到资产上,
其实应该当 用户名密码秘钥 sudo等更新时再推送这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产
关联到上面
"""
if instance and not created:
logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
@receiver(m2m_changed, sender=SystemUser.assets.through)
@on_transaction_commit
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
"""
当系统用户和资产关系发生变化时应该重新推送系统用户到新添加的资产中
"""
if action != POST_ADD:
return
logger.debug("System user assets change signal recv: {}".format(instance))
if model == Asset:
system_user_ids = [instance.id]
asset_ids = pk_set
else:
system_user_ids = pk_set
asset_ids = [instance.id]
for system_user_id in system_user_ids:
push_system_user_to_assets.delay(system_user_id, asset_ids)
@receiver(m2m_changed, sender=SystemUser.users.through)
@on_transaction_commit
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
"""
当系统用户和用户关系发生变化时应该重新推送系统用户资产中
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
for username in usernames:
push_system_user_to_assets_manual.delay(instance, username)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
@on_transaction_commit
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
当系统用户和节点关系发生变化时应该将节点下资产关联到新的系统用户上
"""
if action != POST_ADD:
return
logger.info("System user nodes update signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Node:
nodes_keys = queryset.values_list('key', flat=True)
system_users = [instance]
else:
nodes_keys = [instance.key]
system_users = queryset
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
"""
当系统用户和用户组关系发生变化时应该将组下用户关联到新的系统用户上
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
logger.info("System user groups update signal recv: {}".format(instance))
users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*users)
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
"""
本操作共访问 4 次数据库
当资产的节点发生变化时或者 当节点的资产关系发生变化时
节点下新增的资产添加到节点关联的系统用户中
"""
if action != POST_ADD:
return
logger.debug("Assets node add signal recv: {}".format(action))
if reverse:
nodes = [instance.key]
asset_ids = pk_set
else:
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
asset_ids = [instance.id]
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
nodes_ancestors_keys = set()
for node in nodes:
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
system_user_ids = SystemUser.objects.filter(
nodes__key__in=nodes_ancestors_keys
).distinct().values_list('id', flat=True)
# 查询所有已存在的关系
m2m_model = SystemUser.assets.through
exist = set(m2m_model.objects.filter(
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('systemuser_id', 'asset_id'))
# TODO 优化
to_create = []
for system_user_id in system_user_ids:
asset_ids_to_push = []
for asset_id in asset_ids:
if (system_user_id, asset_id) in exist:
continue
asset_ids_to_push.append(asset_id)
to_create.append(m2m_model(
systemuser_id=system_user_id,
asset_id=asset_id
))
if asset_ids_to_push:
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create)
RELATED_NODE_IDS = '_related_node_ids'
@receiver(pre_delete, sender=Asset)
def on_asset_delete(instance: Asset, using, **kwargs):
node_ids = set(Node.objects.filter(
assets=instance
).distinct().values_list('id', flat=True))
setattr(instance, RELATED_NODE_IDS, node_ids)
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
)
@receiver(post_delete, sender=Asset)
def on_asset_post_delete(instance: Asset, using, **kwargs):
node_ids = getattr(instance, RELATED_NODE_IDS, None)
if node_ids:
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
)

View File

@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_save, pre_delete, post_delete
)
from django.dispatch import receiver
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node, AuthBook
from users.models import User
from orgs.utils import get_current_org, tmp_to_root_org
from assets.tasks import (
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users
)
logger = get_logger(__file__)
@receiver(m2m_changed, sender=SystemUser.assets.through)
@on_transaction_commit
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
"""
当系统用户和资产关系发生变化时应该重新推送系统用户到新添加的资产中
"""
logger.debug("System user assets change signal recv: {}".format(instance))
if not instance:
logger.debug('No system user found')
return
if model == Asset:
system_user_ids = [instance.id]
asset_ids = pk_set
else:
system_user_ids = pk_set
asset_ids = [instance.id]
# 通过 through 创建的没有 org_id
current_org_id = get_current_org().id
with tmp_to_root_org():
authbooks = AuthBook.objects.filter(
asset_id__in=asset_ids,
systemuser_id__in=system_user_ids
)
authbooks.update(org_id=current_org_id)
save_action_mapper = {
'pre_add': pre_save,
'post_add': post_save,
'pre_remove': pre_delete,
'post_remove': post_delete
}
for ab in authbooks:
ab.org_id = current_org_id
post_action = save_action_mapper[action]
logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id))
post_action.send(sender=AuthBook, instance=ab, created=True)
if action == 'post_add':
for system_user_id in system_user_ids:
push_system_user_to_assets.delay(system_user_id, asset_ids)
@receiver(m2m_changed, sender=SystemUser.users.through)
@on_transaction_commit
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
"""
当系统用户和用户关系发生变化时应该重新推送系统用户资产中
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
for username in usernames:
push_system_user_to_assets_manual.delay(instance, username)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
@on_transaction_commit
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
当系统用户和节点关系发生变化时应该将节点下资产关联到新的系统用户上
"""
if action != POST_ADD:
return
logger.info("System user nodes update signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Node:
nodes_keys = queryset.values_list('key', flat=True)
system_users = [instance]
else:
nodes_keys = [instance.key]
system_users = queryset
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
"""
当系统用户和用户组关系发生变化时应该将组下用户关联到新的系统用户上
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
logger.info("System user groups update signal recv: {}".format(instance))
users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*users)
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
当系统用户更新时可能更新了秘钥用户名等这时要自动推送系统用户到资产上,
其实应该当 用户名密码秘钥 sudo等更新时再推送这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产
关联到上面
"""
if instance and not created:
logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])

View File

@ -2,9 +2,8 @@
# #
from .utils import * from .utils import *
from .common import * from .common import *
from .admin_user_connectivity import *
from .asset_connectivity import * from .asset_connectivity import *
from .asset_user_connectivity import * from .account_connectivity import *
from .gather_asset_users import * from .gather_asset_users import *
from .gather_asset_hardware_info import * from .gather_asset_hardware_info import *
from .push_system_user import * from .push_system_user import *

View File

@ -3,9 +3,9 @@
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import get_logger, get_object_or_none from common.utils import get_logger
from orgs.utils import org_aware_func from orgs.utils import org_aware_func
from ..models import Asset from ..models import Connectivity
from . import const from . import const
from .utils import check_asset_can_run_ansible from .utils import check_asset_can_run_ansible
@ -14,13 +14,13 @@ logger = get_logger(__file__)
__all__ = [ __all__ = [
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual', 'test_account_connectivity_util', 'test_accounts_connectivity_manual',
'get_test_asset_user_connectivity_tasks', 'test_user_connectivity', 'get_test_account_connectivity_tasks', 'test_user_connectivity',
'run_adhoc', 'run_adhoc',
] ]
def get_test_asset_user_connectivity_tasks(asset): def get_test_account_connectivity_tasks(asset):
if asset.is_unixlike(): if asset.is_unixlike():
tasks = const.PING_UNIXLIKE_TASKS tasks = const.PING_UNIXLIKE_TASKS
elif asset.is_windows(): elif asset.is_windows():
@ -57,7 +57,7 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
""" """
from ops.inventory import JMSCustomInventory from ops.inventory import JMSCustomInventory
tasks = get_test_asset_user_connectivity_tasks(asset) tasks = get_test_account_connectivity_tasks(asset)
if not tasks: if not tasks:
logger.debug("No tasks ") logger.debug("No tasks ")
return {}, {} return {}, {}
@ -71,62 +71,37 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
return raw, summary return raw, summary
@org_aware_func("asset_user") @org_aware_func("account")
def test_asset_user_connectivity_util(asset_user, task_name): def test_account_connectivity_util(account, task_name):
""" """
:param asset_user: <AuthBook>对象 :param account: <AuthBook>对象
:param task_name: :param task_name:
:return: :return:
""" """
if not check_asset_can_run_ansible(asset_user.asset): if not check_asset_can_run_ansible(account.asset):
return return
try: try:
raw, summary = test_user_connectivity( raw, summary = test_user_connectivity(
task_name=task_name, asset=asset_user.asset, task_name=task_name, asset=account.asset,
username=asset_user.username, password=asset_user.password, username=account.username, password=account.password,
private_key=asset_user.private_key_file private_key=account.private_key_file
) )
except Exception as e: except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e)) logger.warn("Failed run adhoc {}, {}".format(task_name, e))
return return
asset_user.set_connectivity(summary)
if summary.get('success'):
account.set_connectivity(Connectivity.ok)
else:
account.set_connectivity(Connectivity.failed)
@shared_task(queue="ansible") @shared_task(queue="ansible")
def test_asset_users_connectivity_manual(asset_users): def test_accounts_connectivity_manual(accounts):
""" """
:param asset_users: <AuthBook>对象 :param accounts: <AuthBook>对象
""" """
for asset_user in asset_users: for account in accounts:
task_name = _("Test asset user connectivity: {}").format(asset_user) task_name = _("Test account connectivity: {}").format(account)
test_asset_user_connectivity_util(asset_user, task_name) test_account_connectivity_util(account, task_name)
@shared_task(queue="ansible")
def push_asset_user_util(asset_user):
"""
:param asset_user: <Asset user>对象
"""
from .push_system_user import push_system_user_util
if not asset_user.backend.startswith('system_user'):
logger.error("Asset user is not from system user")
return
union_id = asset_user.union_id
union_id_list = union_id.split('_')
if len(union_id_list) < 2:
logger.error("Asset user union id length less than 2")
return
system_user_id = union_id_list[0]
asset_id = union_id_list[1]
asset = get_object_or_none(Asset, pk=asset_id)
system_user = None
if not asset:
return
hosts = check_asset_can_run_ansible([asset])
if asset.is_unixlike:
pass

View File

@ -1,69 +0,0 @@
# ~*~ coding: utf-8 ~*~
from celery import shared_task
from django.utils.translation import ugettext as _
from django.core.cache import cache
from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger
from ops.celery.decorator import register_as_period_task
from ..models import AdminUser
from .utils import clean_ansible_task_hosts
from .asset_connectivity import test_asset_connectivity_util
from . import const
logger = get_logger(__file__)
__all__ = [
'test_admin_user_connectivity_util', 'test_admin_user_connectivity_manual',
'test_admin_user_connectivity_period'
]
@org_aware_func("admin_user")
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
:param admin_user:
:param task_name:
:return:
"""
assets = admin_user.get_related_assets()
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
summary = test_asset_connectivity_util(hosts, task_name)
return summary
@shared_task(queue="ansible")
@register_as_period_task(interval=3600)
def test_admin_user_connectivity_period():
"""
A period task that update the ansible task period
"""
if not const.PERIOD_TASK_ENABLED:
logger.debug('Period task off, skip')
return
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
prev_execute_time = cache.get(key)
if prev_execute_time:
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return
cache.set(key, 1, 60*40)
with tmp_to_root_org():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(
admin_user.name
)
test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40)
@shared_task(queue="ansible")
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
return True

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func from orgs.utils import org_aware_func
from ..models.utils import Connectivity from ..models import Asset, Connectivity, AuthBook
from . import const from . import const
from .utils import clean_ansible_task_hosts, group_asset_by_platform from .utils import clean_ansible_task_hosts, group_asset_by_platform
@ -18,6 +18,28 @@ __all__ = [
] ]
def set_assets_accounts_connectivity(assets, results_summary):
asset_ids_ok = set()
asset_ids_failed = set()
asset_hostnames_ok = results_summary.get('contacted', {}).keys()
for asset in assets:
if asset.hostname in asset_hostnames_ok:
asset_ids_ok.add(asset.id)
else:
asset_ids_failed.add(asset.id)
Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok)
Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed)
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin')
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin')
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok)
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
@shared_task(queue="ansible") @shared_task(queue="ansible")
@org_aware_func("assets") @org_aware_func("assets")
def test_asset_connectivity_util(assets, task_name=None): def test_asset_connectivity_util(assets, task_name=None):
@ -60,14 +82,7 @@ def test_asset_connectivity_util(assets, task_name=None):
results_summary['contacted'].update(contacted) results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark) results_summary['dark'].update(dark)
continue continue
set_assets_accounts_connectivity(assets, results_summary)
for asset in assets:
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 = Connectivity.unknown()
return results_summary return results_summary

View File

@ -11,16 +11,16 @@ app_name = 'assets'
router = BulkRouter() router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset') router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
router.register(r'labels', api.LabelViewSet, 'label') router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'nodes', api.NodeViewSet, 'node')
router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'domains', api.DomainViewSet, 'domain')
router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'gateways', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-users', api.AssetUserViewSet, 'asset-user')
router.register(r'asset-user-auth-infos', api.AssetUserAuthInfoViewSet, 'asset-user-auth-info')
router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user')
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
@ -37,13 +37,6 @@ urlpatterns = [
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'),
path('admin-users/<uuid:pk>/nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-users/<uuid:pk>/auth/', api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-users/<uuid:pk>/connective/', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-users/<uuid:pk>/assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-users/<uuid:pk>/assets/<uuid:asset_id>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), path('system-users/<uuid:pk>/assets/<uuid:asset_id>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),

View File

@ -5,7 +5,6 @@ from rest_framework import serializers
from django.db.models import F from django.db.models import F
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from common.drf.serializers import AdaptedBulkListSerializer
from terminal.models import Session from terminal.models import Session
from ops.models import CommandExecution from ops.models import CommandExecution
from . import models from . import models
@ -108,7 +107,6 @@ class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.M
commandexecution_display = serializers.ReadOnlyField() commandexecution_display = serializers.ReadOnlyField()
class Meta: class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = CommandExecution.hosts.through model = CommandExecution.hosts.through
fields = [ fields = [
'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display' 'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display'

View File

@ -15,7 +15,7 @@ from rest_framework import serializers
from authentication.signals import post_auth_failed, post_auth_success from authentication.signals import post_auth_failed, post_auth_success
from common.utils import get_logger, random_string from common.utils import get_logger, random_string
from common.drf.api import SerializerMixin2 from common.drf.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
@ -29,7 +29,7 @@ logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet'] __all__ = ['UserConnectionTokenViewSet']
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericViewSet): class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewSet):
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
serializer_classes = { serializer_classes = {
'default': ConnectionTokenSerializer, 'default': ConnectionTokenSerializer,

View File

@ -2,12 +2,13 @@
# #
import time import time
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from common.permissions import IsValidUser from common.permissions import IsValidUser, NeedMFAVerify
from ..serializers import OtpVerifySerializer from ..serializers import OtpVerifySerializer
from .. import serializers from .. import serializers
from .. import errors from .. import errors
@ -48,6 +49,9 @@ class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer serializer_class = OtpVerifySerializer
def get(self, request, *args, **kwargs):
return Response({'code': 'valid', 'msg': 'verified'})
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -58,3 +62,8 @@ class UserOtpVerifyApi(CreateAPIView):
return Response({"ok": "1"}) return Response({"ok": "1"})
else: else:
return Response({"error": _("Code is invalid")}, status=400) return Response({"error": _("Code is invalid")}, status=400)
def get_permissions(self):
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [NeedMFAVerify]
return super().get_permissions()

View File

@ -2,12 +2,12 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from ..mixins.api import ( from ..mixins.api import (
SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, SerializerMixin, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
RelationMixin, AllowBulkDestoryMixin, RenderToJsonMixin, RelationMixin, AllowBulkDestroyMixin, RenderToJsonMixin,
) )
class CommonMixin(SerializerMixin2, class CommonMixin(SerializerMixin,
QuerySetMixin, QuerySetMixin,
ExtraFilterFieldsMixin, ExtraFilterFieldsMixin,
PaginatedResponseMixin, PaginatedResponseMixin,
@ -26,13 +26,13 @@ class JMSModelViewSet(CommonMixin,
class JMSBulkModelViewSet(CommonMixin, class JMSBulkModelViewSet(CommonMixin,
AllowBulkDestoryMixin, AllowBulkDestroyMixin,
BulkModelViewSet): BulkModelViewSet):
pass pass
class JMSBulkRelationModelViewSet(CommonMixin, class JMSBulkRelationModelViewSet(CommonMixin,
RelationMixin, RelationMixin,
AllowBulkDestoryMixin, AllowBulkDestroyMixin,
BulkModelViewSet): BulkModelViewSet):
pass pass

View File

@ -23,7 +23,7 @@ from ..utils import lazyproperty
__all__ = [ __all__ = [
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin', 'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin', 'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
] ]
@ -62,21 +62,27 @@ class RenderToJsonMixin:
class SerializerMixin: class SerializerMixin:
""" 根据用户请求动作的不同,获取不同的 `serializer_class `""" """ 根据用户请求动作的不同,获取不同的 `serializer_class `"""
action: str
request: Request
serializer_classes = None serializer_classes = None
single_actions = ['put', 'retrieve', 'patch']
def get_serializer_class_by_view_action(self): def get_serializer_class_by_view_action(self):
if not hasattr(self, 'serializer_classes'): if not hasattr(self, 'serializer_classes'):
return None return None
if not isinstance(self.serializer_classes, dict): if not isinstance(self.serializer_classes, dict):
return None return None
action = self.request.query_params.get('action')
serializer_class = None view_action = self.request.query_params.get('action') or self.action or 'list'
if action: serializer_class = self.serializer_classes.get(view_action)
# metadata方法 使用 action 参数获取
serializer_class = self.serializer_classes.get(action)
if serializer_class is None: if serializer_class is None:
serializer_class = self.serializer_classes.get(self.action) view_method = self.request.method.lower()
serializer_class = self.serializer_classes.get(view_method)
if serializer_class is None and view_action in self.single_actions:
serializer_class = self.serializer_classes.get('single')
if serializer_class is None: if serializer_class is None:
serializer_class = self.serializer_classes.get('display') serializer_class = self.serializer_classes.get('display')
if serializer_class is None: if serializer_class is None:
@ -301,36 +307,18 @@ class RelationMixin:
self.send_m2m_changed_signal(instance, 'post_remove') self.send_m2m_changed_signal(instance, 'post_remove')
class SerializerMixin2:
serializer_classes = {}
def get_serializer_class(self):
if self.serializer_classes:
serializer_class = self.serializer_classes.get(
self.action, self.serializer_classes.get('default')
)
if isinstance(serializer_class, dict):
serializer_class = serializer_class.get(
self.request.method.lower, serializer_class.get('default')
)
assert serializer_class, '`serializer_classes` config error'
return serializer_class
return super().get_serializer_class()
class QuerySetMixin: class QuerySetMixin:
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
queryset = serializer_class.setup_eager_loading(queryset) queryset = serializer_class.setup_eager_loading(queryset)
return queryset return queryset
class AllowBulkDestoryMixin: class AllowBulkDestroyMixin:
def allow_bulk_destroy(self, qs, filtered): def allow_bulk_destroy(self, qs, filtered):
""" """
我们规定批量删除的情况必须用 `id` 指定要删除的数据 我们规定批量删除的情况必须用 `id` 指定要删除的数据

View File

@ -67,6 +67,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.forms', 'django.forms',
'simple_history',
] ]
@ -86,6 +87,7 @@ MIDDLEWARE = [
'orgs.middleware.OrgMiddleware', 'orgs.middleware.OrgMiddleware',
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware', 'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware', 'authentication.backends.cas.middleware.CASMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
] ]

Binary file not shown.

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-06-25 17:12+0800\n" "POT-Creation-Date: 2021-06-29 17:02+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -19,7 +19,7 @@ msgstr ""
#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47
#: applications/models/application.py:11 assets/models/asset.py:142 #: applications/models/application.py:11 assets/models/asset.py:142
#: assets/models/base.py:249 assets/models/cluster.py:18 #: assets/models/base.py:216 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:21 assets/models/domain.py:21 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21
#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24
#: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29
@ -40,7 +40,7 @@ msgid "Priority"
msgstr "优先级" msgstr "优先级"
#: acls/models/base.py:28 assets/models/cmd_filter.py:54 #: acls/models/base.py:28 assets/models/cmd_filter.py:54
#: assets/models/user.py:123 #: assets/models/user.py:246
msgid "1-100, the lower the value will be match first" msgid "1-100, the lower the value will be match first"
msgstr "优先级可选范围为 1-100 (数值越小越优先)" msgstr "优先级可选范围为 1-100 (数值越小越优先)"
@ -54,16 +54,16 @@ msgstr "激活中"
# msgstr "创建日期" # msgstr "创建日期"
#: acls/models/base.py:32 applications/models/application.py:24 #: acls/models/base.py:32 applications/models/application.py:24
#: assets/models/asset.py:147 assets/models/asset.py:223 #: assets/models/asset.py:147 assets/models/asset.py:223
#: assets/models/base.py:254 assets/models/cluster.py:29 #: assets/models/base.py:221 assets/models/cluster.py:29
#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64
#: assets/models/domain.py:22 assets/models/domain.py:56 #: assets/models/domain.py:22 assets/models/domain.py:56
#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37
#: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34
#: terminal/models/storage.py:26 terminal/models/terminal.py:114 #: terminal/models/storage.py:29 terminal/models/storage.py:96
#: tickets/models/ticket.py:73 users/models/group.py:16 #: terminal/models/terminal.py:114 tickets/models/ticket.py:73
#: users/models/user.py:583 xpack/plugins/change_auth_plan/models.py:77 #: users/models/group.py:16 users/models/user.py:583
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:98 #: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35
#: xpack/plugins/gathered_user/models.py:26 #: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26
msgid "Comment" msgid "Comment"
msgstr "备注" msgstr "备注"
@ -119,9 +119,8 @@ msgstr "系统用户"
#: acls/models/login_asset_acl.py:22 #: acls/models/login_asset_acl.py:22
#: applications/serializers/attrs/application_category/remote_app.py:33 #: applications/serializers/attrs/application_category/remote_app.py:33
#: assets/models/asset.py:355 assets/models/authbook.py:27 #: assets/models/asset.py:372 assets/models/authbook.py:17
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 #: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:33
#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:91
#: assets/serializers/system_user.py:202 audits/models.py:38 #: assets/serializers/system_user.py:202 audits/models.py:38
#: perms/models/asset_permission.py:99 templates/index.html:82 #: perms/models/asset_permission.py:99 templates/index.html:82
#: terminal/backends/command/models.py:19 #: terminal/backends/command/models.py:19
@ -158,7 +157,7 @@ msgstr ""
#: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31
#: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: applications/serializers/attrs/application_type/mysql_workbench.py:18
#: assets/models/asset.py:183 assets/models/domain.py:52 #: assets/models/asset.py:183 assets/models/domain.py:52
#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:113 #: settings/serializers/settings.py:113
#: users/templates/users/_granted_assets.html:26 #: users/templates/users/_granted_assets.html:26
#: users/templates/users/user_asset_permission.html:156 #: users/templates/users/user_asset_permission.html:156
msgid "IP" msgid "IP"
@ -178,13 +177,13 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
#: applications/serializers/attrs/application_type/custom.py:21 #: applications/serializers/attrs/application_type/custom.py:21
#: applications/serializers/attrs/application_type/mysql_workbench.py:30 #: applications/serializers/attrs/application_type/mysql_workbench.py:30
#: applications/serializers/attrs/application_type/vmware_client.py:26 #: applications/serializers/attrs/application_type/vmware_client.py:26
#: assets/models/base.py:250 assets/models/gathered_user.py:15 #: assets/models/base.py:217 assets/models/gathered_user.py:15
#: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17
#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:548 #: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:548
#: users/templates/users/_select_user_modal.html:14 #: users/templates/users/_select_user_modal.html:14
#: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:47
#: xpack/plugins/change_auth_plan/models.py:278 #: xpack/plugins/change_auth_plan/models.py:278
#: xpack/plugins/cloud/serializers.py:65 #: xpack/plugins/cloud/serializers.py:51
msgid "Username" msgid "Username"
msgstr "用户名" msgstr "用户名"
@ -198,8 +197,7 @@ msgstr ""
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
#: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184
#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 #: assets/serializers/gathered_user.py:23 settings/serializers/settings.py:112
#: settings/serializers/settings.py:112
#: users/templates/users/_granted_assets.html:25 #: users/templates/users/_granted_assets.html:25
#: users/templates/users/user_asset_permission.html:157 #: users/templates/users/user_asset_permission.html:157
msgid "Hostname" msgid "Hostname"
@ -212,7 +210,7 @@ msgid ""
msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}"
#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187
#: assets/models/domain.py:54 assets/models/user.py:124 #: assets/models/domain.py:54 assets/models/user.py:247
#: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 #: terminal/serializers/session.py:32 terminal/serializers/storage.py:69
msgid "Protocol" msgid "Protocol"
msgstr "协议" msgstr "协议"
@ -254,7 +252,7 @@ msgid "Category"
msgstr "类别" msgstr "类别"
#: applications/models/application.py:16 assets/models/cmd_filter.py:53 #: applications/models/application.py:16 assets/models/cmd_filter.py:53
#: perms/models/application_permission.py:23 #: assets/models/user.py:245 perms/models/application_permission.py:23
#: perms/serializers/application/permission.py:17 #: perms/serializers/application/permission.py:17
#: perms/serializers/application/user_permission.py:34 #: perms/serializers/application/user_permission.py:34
#: terminal/models/storage.py:47 terminal/models/storage.py:108 #: terminal/models/storage.py:47 terminal/models/storage.py:108
@ -296,7 +294,7 @@ msgstr "应用类型"
#: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177 #: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177
#: assets/serializers/system_user.py:203 #: assets/serializers/system_user.py:203
msgid "Login mode display" msgid "Login mode display"
msgstr "登录模式(显示名称)" msgstr "认证方式(显示名称)"
#: applications/serializers/attrs/application_category/cloud.py:9 #: applications/serializers/attrs/application_category/cloud.py:9
#: assets/models/cluster.py:40 #: assets/models/cluster.py:40
@ -304,7 +302,7 @@ msgid "Cluster"
msgstr "集群" msgstr "集群"
#: applications/serializers/attrs/application_category/db.py:11 #: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:63 #: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:49
msgid "Host" msgid "Host"
msgstr "主机" msgstr "主机"
@ -314,7 +312,7 @@ msgstr "主机"
#: applications/serializers/attrs/application_type/oracle.py:11 #: applications/serializers/attrs/application_type/oracle.py:11
#: applications/serializers/attrs/application_type/pgsql.py:11 #: applications/serializers/attrs/application_type/pgsql.py:11
#: assets/models/asset.py:188 assets/models/domain.py:53 #: assets/models/asset.py:188 assets/models/domain.py:53
#: xpack/plugins/cloud/serializers.py:64 #: xpack/plugins/cloud/serializers.py:50
msgid "Port" msgid "Port"
msgstr "端口" msgstr "端口"
@ -334,8 +332,8 @@ msgstr "目标URL"
#: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/custom.py:25
#: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/mysql_workbench.py:34
#: applications/serializers/attrs/application_type/vmware_client.py:30 #: applications/serializers/attrs/application_type/vmware_client.py:30
#: assets/models/base.py:251 assets/serializers/asset_user.py:78 #: assets/models/base.py:218 audits/signals_handler.py:58
#: audits/signals_handler.py:58 authentication/forms.py:22 #: authentication/forms.py:22
#: authentication/templates/authentication/login.html:164 #: authentication/templates/authentication/login.html:164
#: settings/serializers/settings.py:94 users/forms/profile.py:21 #: settings/serializers/settings.py:94 users/forms/profile.py:21
#: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_otp_check_password.html:13
@ -344,7 +342,7 @@ msgstr "目标URL"
#: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:68
#: xpack/plugins/change_auth_plan/models.py:190 #: xpack/plugins/change_auth_plan/models.py:190
#: xpack/plugins/change_auth_plan/models.py:285 #: xpack/plugins/change_auth_plan/models.py:285
#: xpack/plugins/cloud/serializers.py:67 #: xpack/plugins/cloud/serializers.py:53
msgid "Password" msgid "Password"
msgstr "密码" msgstr "密码"
@ -356,10 +354,6 @@ msgstr "运行参数"
msgid "Target url" msgid "Target url"
msgstr "目标URL" msgstr "目标URL"
#: assets/api/admin_user.py:50
msgid "Deleted failed, There are related assets"
msgstr "删除失败,存在关联资产"
#: assets/api/domain.py:50 #: assets/api/domain.py:50
msgid "Number required" msgid "Number required"
msgstr "需要为数字" msgstr "需要为数字"
@ -376,38 +370,6 @@ msgstr "不能删除根节点 ({})"
msgid "Deletion failed and the node contains assets" msgid "Deletion failed and the node contains assets"
msgstr "删除失败,节点包含资产" msgstr "删除失败,节点包含资产"
#: assets/backends/db.py:110 assets/models/user.py:307 audits/models.py:39
#: perms/models/application_permission.py:31
#: perms/models/asset_permission.py:101 templates/_nav.html:45
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42
#: users/templates/users/_granted_assets.html:27
#: users/templates/users/user_asset_permission.html:42
#: users/templates/users/user_asset_permission.html:76
#: users/templates/users/user_asset_permission.html:159
#: users/templates/users/user_database_app_permission.html:40
#: users/templates/users/user_database_app_permission.html:67
msgid "System user"
msgstr "系统用户"
#: assets/backends/db.py:181
msgid "System user(Dynamic)"
msgstr "系统用户(动态)"
#: assets/backends/db.py:233 assets/models/asset.py:196
#: assets/models/cluster.py:19 assets/models/user.py:67 templates/_nav.html:44
#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:181
msgid "Admin user"
msgstr "管理用户"
#: assets/backends/db.py:254
msgid "Could not remove asset admin user"
msgstr "不能移除资产的管理用户账号"
#: assets/backends/db.py:318
msgid "Latest version could not be delete"
msgstr "最新版本的不能被删除"
#: assets/models/asset.py:143 #: assets/models/asset.py:143
msgid "Base" msgid "Base"
msgstr "基础" msgstr "基础"
@ -416,7 +378,7 @@ msgstr "基础"
msgid "Charset" msgid "Charset"
msgstr "编码" msgstr "编码"
#: assets/models/asset.py:145 assets/serializers/asset.py:171 #: assets/models/asset.py:145 assets/serializers/asset.py:180
#: tickets/models/ticket.py:40 #: tickets/models/ticket.py:40
msgid "Meta" msgid "Meta"
msgstr "元数据" msgstr "元数据"
@ -426,16 +388,16 @@ msgid "Internal"
msgstr "内部的" msgstr "内部的"
#: assets/models/asset.py:166 assets/models/asset.py:190 #: assets/models/asset.py:166 assets/models/asset.py:190
#: assets/serializers/asset.py:66 perms/serializers/asset/user_permission.py:43 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:43
msgid "Platform" msgid "Platform"
msgstr "系统平台" msgstr "系统平台"
#: assets/models/asset.py:189 assets/serializers/asset.py:68 #: assets/models/asset.py:189 assets/serializers/asset.py:70
#: perms/serializers/asset/user_permission.py:41 #: perms/serializers/asset/user_permission.py:41
msgid "Protocols" msgid "Protocols"
msgstr "协议组" msgstr "协议组"
#: assets/models/asset.py:192 assets/models/user.py:119 #: assets/models/asset.py:192 assets/models/user.py:237
#: perms/models/asset_permission.py:100 #: perms/models/asset_permission.py:100
#: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/change_auth_plan/models.py:56
#: xpack/plugins/gathered_user/models.py:24 #: xpack/plugins/gathered_user/models.py:24
@ -448,6 +410,13 @@ msgstr "节点"
msgid "Is active" msgid "Is active"
msgstr "激活" msgstr "激活"
#: assets/models/asset.py:196 assets/models/cluster.py:19
#: assets/models/user.py:234 assets/models/user.py:370
#: assets/serializers/asset.py:68 templates/_nav.html:44
#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:146
msgid "Admin user"
msgstr "管理用户"
#: assets/models/asset.py:199 #: assets/models/asset.py:199
msgid "Public IP" msgid "Public IP"
msgstr "公网IP" msgstr "公网IP"
@ -516,7 +485,7 @@ msgstr "主机名原始"
msgid "Labels" msgid "Labels"
msgstr "标签管理" msgstr "标签管理"
#: assets/models/asset.py:221 assets/models/base.py:257 #: assets/models/asset.py:221 assets/models/base.py:224
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
#: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: assets/models/cmd_filter.py:67 assets/models/group.py:21
#: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24
@ -528,7 +497,7 @@ msgstr "创建者"
# msgid "Created by" # msgid "Created by"
# msgstr "创建者" # msgstr "创建者"
#: assets/models/asset.py:222 assets/models/base.py:255 #: assets/models/asset.py:222 assets/models/base.py:222
#: assets/models/cluster.py:26 assets/models/domain.py:24 #: assets/models/cluster.py:26 assets/models/domain.py:24
#: assets/models/gathered_user.py:19 assets/models/group.py:22 #: assets/models/gathered_user.py:19 assets/models/group.py:22
#: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50
@ -538,35 +507,45 @@ msgstr "创建者"
msgid "Date created" msgid "Date created"
msgstr "创建日期" msgstr "创建日期"
#: assets/models/authbook.py:18 #: assets/models/authbook.py:18 assets/models/user.py:321 audits/models.py:39
msgid "Bulk delete deny" #: perms/models/application_permission.py:31
msgstr "拒绝批量删除" #: perms/models/asset_permission.py:101 templates/_nav.html:45
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42
#: users/templates/users/_granted_assets.html:27
#: users/templates/users/user_asset_permission.html:42
#: users/templates/users/user_asset_permission.html:76
#: users/templates/users/user_asset_permission.html:159
#: users/templates/users/user_database_app_permission.html:40
#: users/templates/users/user_database_app_permission.html:67
msgid "System user"
msgstr "系统用户"
#: assets/models/authbook.py:28 #: assets/models/authbook.py:20
msgid "Latest version"
msgstr "最新版本"
#: assets/models/authbook.py:29
msgid "Version" msgid "Version"
msgstr "版本" msgstr "版本"
#: assets/models/authbook.py:21
msgid "Latest version"
msgstr "最新版本"
#: assets/models/authbook.py:38 #: assets/models/authbook.py:38
msgid "AuthBook" msgid "AuthBook"
msgstr "" msgstr "账号"
#: assets/models/base.py:252 xpack/plugins/change_auth_plan/models.py:72 #: assets/models/base.py:219 xpack/plugins/change_auth_plan/models.py:72
#: xpack/plugins/change_auth_plan/models.py:197 #: xpack/plugins/change_auth_plan/models.py:197
#: xpack/plugins/change_auth_plan/models.py:292 #: xpack/plugins/change_auth_plan/models.py:292
msgid "SSH private key" msgid "SSH private key"
msgstr "SSH密钥" msgstr "SSH密钥"
#: assets/models/base.py:253 xpack/plugins/change_auth_plan/models.py:75 #: assets/models/base.py:220 xpack/plugins/change_auth_plan/models.py:75
#: xpack/plugins/change_auth_plan/models.py:193 #: xpack/plugins/change_auth_plan/models.py:193
#: xpack/plugins/change_auth_plan/models.py:288 #: xpack/plugins/change_auth_plan/models.py:288
msgid "SSH public key" msgid "SSH public key"
msgstr "SSH公钥" msgstr "SSH公钥"
#: assets/models/base.py:256 assets/models/gathered_user.py:20 #: assets/models/base.py:223 assets/models/gathered_user.py:20
#: common/db/models.py:73 common/mixins/models.py:51 ops/models/adhoc.py:39 #: common/db/models.py:73 common/mixins/models.py:51 ops/models/adhoc.py:39
#: orgs/models.py:421 #: orgs/models.py:421
msgid "Date updated" msgid "Date updated"
@ -614,7 +593,7 @@ msgstr "系统"
msgid "Default Cluster" msgid "Default Cluster"
msgstr "默认Cluster" msgstr "默认Cluster"
#: assets/models/cmd_filter.py:33 assets/models/user.py:129 #: assets/models/cmd_filter.py:33 assets/models/user.py:252
msgid "Command filter" msgid "Command filter"
msgstr "命令过滤器" msgstr "命令过滤器"
@ -715,65 +694,65 @@ msgstr "ssh私钥"
#: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:41
#: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:73
#: users/templates/users/user_asset_permission.html:158 #: users/templates/users/user_asset_permission.html:158
#: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:182 #: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:147
msgid "Node" msgid "Node"
msgstr "节点" msgstr "节点"
#: assets/models/user.py:115 #: assets/models/user.py:227
msgid "Automatic login" msgid "Automatic managed"
msgstr "自动登录" msgstr "托管密码"
#: assets/models/user.py:116 #: assets/models/user.py:228
msgid "Manually login" msgid "Manually input"
msgstr "手动登录" msgstr "手动输入"
#: assets/models/user.py:118 #: assets/models/user.py:236
msgid "Username same with user" msgid "Username same with user"
msgstr "用户名与用户相同" msgstr "用户名与用户相同"
#: assets/models/user.py:120 assets/serializers/domain.py:30 #: assets/models/user.py:239 assets/serializers/domain.py:30
#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52 #: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52
msgid "Assets" msgid "Assets"
msgstr "资产" msgstr "资产"
#: assets/models/user.py:121 templates/_nav.html:17 #: assets/models/user.py:243 templates/_nav.html:17
#: users/views/profile/password.py:43 users/views/profile/pubkey.py:37 #: users/views/profile/password.py:43 users/views/profile/pubkey.py:37
msgid "Users" msgid "Users"
msgstr "用户管理" msgstr "用户管理"
#: assets/models/user.py:122 #: assets/models/user.py:244
msgid "User groups" msgid "User groups"
msgstr "用户组" msgstr "用户组"
#: assets/models/user.py:125 #: assets/models/user.py:248
msgid "Auto push" msgid "Auto push"
msgstr "自动推送" msgstr "自动推送"
#: assets/models/user.py:126 #: assets/models/user.py:249
msgid "Sudo" msgid "Sudo"
msgstr "Sudo" msgstr "Sudo"
#: assets/models/user.py:127 #: assets/models/user.py:250
msgid "Shell" msgid "Shell"
msgstr "Shell" msgstr "Shell"
#: assets/models/user.py:128 #: assets/models/user.py:251
msgid "Login mode" msgid "Login mode"
msgstr "登录模式" msgstr "认证方式"
#: assets/models/user.py:130 #: assets/models/user.py:253
msgid "SFTP Root" msgid "SFTP Root"
msgstr "SFTP根路径" msgstr "SFTP根路径"
#: assets/models/user.py:131 authentication/models.py:95 #: assets/models/user.py:254 authentication/models.py:95
msgid "Token" msgid "Token"
msgstr "" msgstr ""
#: assets/models/user.py:132 #: assets/models/user.py:255
msgid "Home" msgid "Home"
msgstr "家目录" msgstr "家目录"
#: assets/models/user.py:133 #: assets/models/user.py:256
msgid "System groups" msgid "System groups"
msgstr "用户组" msgstr "用户组"
@ -794,23 +773,19 @@ msgstr "可连接"
msgid "Unknown" msgid "Unknown"
msgstr "未知" msgstr "未知"
#: assets/serializers/asset.py:23 #: assets/serializers/asset.py:22
msgid "Protocol format should {}/{}" msgid "Protocol format should {}/{}"
msgstr "协议格式 {}/{}" msgstr "协议格式 {}/{}"
#: assets/serializers/asset.py:40 #: assets/serializers/asset.py:39
msgid "Protocol duplicate: {}" msgid "Protocol duplicate: {}"
msgstr "协议重复: {}" msgstr "协议重复: {}"
#: assets/serializers/asset.py:69 #: assets/serializers/asset.py:71
msgid "Domain name" msgid "Domain name"
msgstr "网域名称" msgstr "网域名称"
#: assets/serializers/asset.py:70 #: assets/serializers/asset.py:72 perms/serializers/asset/permission.py:49
msgid "Admin user name"
msgstr "管理用户名称"
#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:49
msgid "Nodes name" msgid "Nodes name"
msgstr "节点名称" msgstr "节点名称"
@ -822,33 +797,10 @@ msgstr "硬件信息"
msgid "Org name" msgid "Org name"
msgstr "组织名称" msgstr "组织名称"
#: assets/serializers/asset.py:162 assets/serializers/asset.py:194 #: assets/serializers/asset.py:171 assets/serializers/asset.py:203
msgid "Connectivity" msgid "Connectivity"
msgstr "连接" msgstr "连接"
#: assets/serializers/asset_user.py:45
#: authentication/templates/authentication/_access_key_modal.html:30
#: users/serializers/group.py:37
msgid "ID"
msgstr "ID"
#: assets/serializers/asset_user.py:49
msgid "Backend"
msgstr "后端"
#: assets/serializers/asset_user.py:50 users/models/user.py:596
msgid "Source"
msgstr "来源"
#: assets/serializers/asset_user.py:82 users/forms/profile.py:160
#: users/models/user.py:580 users/templates/users/user_password_update.html:48
msgid "Public key"
msgstr "SSH公钥"
#: assets/serializers/asset_user.py:86 users/models/user.py:577
msgid "Private key"
msgstr "ssh私钥"
#: assets/serializers/base.py:47 #: assets/serializers/base.py:47
msgid "private key invalid" msgid "private key invalid"
msgstr "密钥不合法" msgstr "密钥不合法"
@ -1124,7 +1076,7 @@ msgstr "启用"
msgid "-" msgid "-"
msgstr "" msgstr ""
#: audits/models.py:97 xpack/plugins/cloud/const.py:27 #: audits/models.py:97 xpack/plugins/cloud/const.py:25
msgid "Failed" msgid "Failed"
msgstr "失败" msgstr "失败"
@ -1455,6 +1407,11 @@ msgstr "使用api key签名请求头每个请求的头部是不一样的"
msgid "docs" msgid "docs"
msgstr "文档" msgstr "文档"
#: authentication/templates/authentication/_access_key_modal.html:30
#: users/serializers/group.py:37
msgid "ID"
msgstr "ID"
#: authentication/templates/authentication/_access_key_modal.html:31 #: authentication/templates/authentication/_access_key_modal.html:31
msgid "Secret" msgid "Secret"
msgstr "秘钥" msgstr "秘钥"
@ -3927,6 +3884,11 @@ msgstr "不能和原来的密钥相同"
msgid "Not a valid ssh public key" msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法" msgstr "SSH密钥不合法"
#: users/forms/profile.py:160 users/models/user.py:580
#: users/templates/users/user_password_update.html:48
msgid "Public key"
msgstr "SSH公钥"
#: users/models/user.py:174 #: users/models/user.py:174
msgid "System administrator" msgid "System administrator"
msgstr "系统管理员" msgstr "系统管理员"
@ -3951,6 +3913,14 @@ msgstr "头像"
msgid "Wechat" msgid "Wechat"
msgstr "微信" msgstr "微信"
#: users/models/user.py:577
msgid "Private key"
msgstr "ssh私钥"
#: users/models/user.py:596
msgid "Source"
msgstr "来源"
#: users/models/user.py:600 #: users/models/user.py:600
msgid "Date password last updated" msgid "Date password last updated"
msgstr "最后更新密码日期" msgstr "最后更新密码日期"
@ -4061,7 +4031,7 @@ msgid "Security token validation"
msgstr "安全令牌验证" msgstr "安全令牌验证"
#: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78 #: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78
#: xpack/plugins/cloud/serializers.py:180 #: xpack/plugins/cloud/serializers.py:145
msgid "Account" msgid "Account"
msgstr "账户" msgstr "账户"
@ -4762,39 +4732,31 @@ msgstr ""
msgid "Nutanix" msgid "Nutanix"
msgstr "" msgstr ""
#: xpack/plugins/cloud/const.py:17 #: xpack/plugins/cloud/const.py:20
msgid "Huawei Private Cloud"
msgstr "华为私有云"
#: xpack/plugins/cloud/const.py:18
msgid "Qingyun Private Cloud"
msgstr ""
#: xpack/plugins/cloud/const.py:22
msgid "Instance name" msgid "Instance name"
msgstr "实例名称" msgstr "实例名称"
#: xpack/plugins/cloud/const.py:23 #: xpack/plugins/cloud/const.py:21
msgid "Instance name and Partial IP" msgid "Instance name and Partial IP"
msgstr "实例名称和部分IP" msgstr "实例名称和部分IP"
#: xpack/plugins/cloud/const.py:28 #: xpack/plugins/cloud/const.py:26
msgid "Succeed" msgid "Succeed"
msgstr "成功" msgstr "成功"
#: xpack/plugins/cloud/const.py:32 #: xpack/plugins/cloud/const.py:30
msgid "Unsync" msgid "Unsync"
msgstr "未同步" msgstr "未同步"
#: xpack/plugins/cloud/const.py:33 #: xpack/plugins/cloud/const.py:31
msgid "New Sync" msgid "New Sync"
msgstr "新同步" msgstr "新同步"
#: xpack/plugins/cloud/const.py:34 #: xpack/plugins/cloud/const.py:32
msgid "Synced" msgid "Synced"
msgstr "已同步" msgstr "已同步"
#: xpack/plugins/cloud/const.py:35 #: xpack/plugins/cloud/const.py:33
msgid "Released" msgid "Released"
msgstr "已释放" msgstr "已释放"
@ -4810,7 +4772,7 @@ msgstr "云服务商"
msgid "Cloud account" msgid "Cloud account"
msgstr "云账号" msgstr "云账号"
#: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:161 #: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:126
msgid "Regions" msgid "Regions"
msgstr "地域" msgstr "地域"
@ -4818,7 +4780,7 @@ msgstr "地域"
msgid "Hostname strategy" msgid "Hostname strategy"
msgstr "主机名策略" msgstr "主机名策略"
#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:184 #: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:149
msgid "Always update" msgid "Always update"
msgstr "总是更新" msgstr "总是更新"
@ -5010,28 +4972,20 @@ msgstr ""
msgid "Subscription ID" msgid "Subscription ID"
msgstr "" msgstr ""
#: xpack/plugins/cloud/serializers.py:49 #: xpack/plugins/cloud/serializers.py:124
msgid "This field is required"
msgstr "这个字段是必填项"
#: xpack/plugins/cloud/serializers.py:83 xpack/plugins/cloud/serializers.py:87
msgid "API Endpoint"
msgstr "API 端点"
#: xpack/plugins/cloud/serializers.py:159
msgid "History count" msgid "History count"
msgstr "执行次数" msgstr "执行次数"
#: xpack/plugins/cloud/serializers.py:160 #: xpack/plugins/cloud/serializers.py:125
msgid "Instance count" msgid "Instance count"
msgstr "实例个数" msgstr "实例个数"
#: xpack/plugins/cloud/serializers.py:183 #: xpack/plugins/cloud/serializers.py:148
#: xpack/plugins/gathered_user/serializers.py:20 #: xpack/plugins/gathered_user/serializers.py:20
msgid "Periodic display" msgid "Periodic display"
msgstr "定时执行" msgstr "定时执行"
#: xpack/plugins/cloud/utils.py:65 #: xpack/plugins/cloud/utils.py:64
msgid "Account unavailable" msgid "Account unavailable"
msgstr "账户无效" msgstr "账户无效"
@ -5118,3 +5072,36 @@ msgstr "旗舰版"
#: xpack/plugins/license/models.py:77 #: xpack/plugins/license/models.py:77
msgid "Community edition" msgid "Community edition"
msgstr "社区版" msgstr "社区版"
#~ msgid "Deleted failed, There are related assets"
#~ msgstr "删除失败,存在关联资产"
#~ msgid "System user(Dynamic)"
#~ msgstr "系统用户(动态)"
#~ msgid "Could not remove asset admin user"
#~ msgstr "不能移除资产的管理用户账号"
#~ msgid "Latest version could not be delete"
#~ msgstr "最新版本的不能被删除"
#~ msgid "Bulk delete deny"
#~ msgstr "拒绝批量删除"
#~ msgid "Admin user name"
#~ msgstr "管理用户名称"
#~ msgid "Backend"
#~ msgstr "后端"
#~ msgid "Huawei Private Cloud"
#~ msgstr "华为私有云"
#~ msgid "This field is required"
#~ msgstr "这个字段是必填项"
#~ msgid "API Endpoint"
#~ msgstr "API 端点"
#~ msgid "Terminal command alert"
#~ msgstr "终端命令告警"

View File

@ -31,7 +31,11 @@ class JMSBaseInventory(BaseInventory):
if run_as_admin: if run_as_admin:
info.update(asset.get_auth_info()) info.update(asset.get_auth_info())
if asset.is_unixlike(): if asset.is_unixlike():
info["become"] = asset.admin_user.become_info info["become"] = {
"method": 'sudo',
"user": 'root',
"pass": ''
}
if asset.is_windows(): if asset.is_windows():
info["vars"].update({ info["vars"].update({
"ansible_connection": "ssh", "ansible_connection": "ssh",
@ -103,8 +107,6 @@ class JMSInventory(JMSBaseInventory):
super().__init__(host_list=host_list) super().__init__(host_list=host_list)
def get_run_user_info(self, host): def get_run_user_info(self, host):
from assets.backends import AssetUserManager
if not self.run_as and not self.system_user: if not self.run_as and not self.system_user:
return {} return {}
@ -112,17 +114,12 @@ class JMSInventory(JMSBaseInventory):
asset = self.assets.filter(id=asset_id).first() asset = self.assets.filter(id=asset_id).first()
if not asset: if not asset:
logger.error('Host not found: ', asset_id) logger.error('Host not found: ', asset_id)
return {}
if self.system_user: if self.system_user:
self.system_user.load_asset_special_auth(asset=asset, username=self.run_as) self.system_user.load_asset_special_auth(asset=asset, username=self.run_as)
return self.system_user._to_secret_json() return self.system_user._to_secret_json()
else:
try:
manager = AssetUserManager()
run_user = manager.get_latest(username=self.run_as, asset=asset, prefer='system_user')
return run_user._to_secret_json()
except Exception as e:
logger.error(e, exc_info=True)
return {} return {}

View File

@ -286,18 +286,12 @@ class AdHocExecution(OrgModelMixin):
raw = '' raw = ''
try: try:
date_start_s = timezone.now().now().strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Start task: {}").format(date_start_s, self.task.name))
raw, summary = self.start_runner() raw, summary = self.start_runner()
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
raw = {"dark": {"all": str(e)}, "contacted": []} raw = {"dark": {"all": str(e)}, "contacted": []}
finally: finally:
self.clean_up(summary, time_start) self.clean_up(summary, time_start)
date_end = timezone.now().now()
date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Task finish").format(date_end_s))
print('.\n\n.')
return raw, summary return raw, summary
def clean_up(self, summary, time_start): def clean_up(self, summary, time_start):

View File

@ -9,10 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty, settings from common.utils import lazyproperty, settings
from common.const import choices from common.const import choices
from common.db.models import ChoiceSet from common.db.models import TextChoices
class ROLE(ChoiceSet): class ROLE(TextChoices):
ADMIN = choices.ADMIN, _('Organization administrator') ADMIN = choices.ADMIN, _('Organization administrator')
AUDITOR = choices.AUDITOR, _("Organization auditor") AUDITOR = choices.AUDITOR, _("Organization auditor")
USER = choices.USER, _('User') USER = choices.USER, _('User')

View File

@ -4,7 +4,6 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from users.models.user import User from users.models.user import User
from common.drf.serializers import AdaptedBulkListSerializer
from common.drf.serializers import BulkModelSerializer from common.drf.serializers import BulkModelSerializer
from common.db.models import concated_display as display from common.db.models import concated_display as display
from .models import Organization, OrganizationMember, ROLE from .models import Organization, OrganizationMember, ROLE
@ -35,7 +34,6 @@ class OrgSerializer(ModelSerializer):
class Meta: class Meta:
model = Organization model = Organization
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + [
'resource_statistics', 'resource_statistics',

View File

@ -36,8 +36,8 @@ class OrgsMappingForMemoryPubSub(LazyObject):
orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub() orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub()
def expire_orgs_mapping_for_memory(): def expire_orgs_mapping_for_memory(org_id):
orgs_mapping_for_memory_pub_sub.publish('expire_orgs_mapping') orgs_mapping_for_memory_pub_sub.publish(str(org_id))
@receiver(django_ready) @receiver(django_ready)
@ -54,7 +54,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
if message['data'] == b'error': if message['data'] == b'error':
raise ValueError raise ValueError
Organization.expire_orgs_mapping() Organization.expire_orgs_mapping()
logger.debug('Expire orgs mapping') logger.debug('Expire orgs mapping: ' + str(message['data']))
except Exception as e: except Exception as e:
logger.exception(f'subscribe_orgs_mapping_expire: {e}') logger.exception(f'subscribe_orgs_mapping_expire: {e}')
Organization.expire_orgs_mapping() Organization.expire_orgs_mapping()
@ -65,10 +65,9 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
@receiver(post_save, sender=Organization) @receiver(post_save, sender=Organization)
def on_org_create_or_update(sender, instance=None, created=False, **kwargs): def on_org_create_or_update(sender, instance, created=False, **kwargs):
# 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到 # 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到
expire_orgs_mapping_for_memory() expire_orgs_mapping_for_memory(instance.id)
if instance:
old_org = get_current_org() old_org = get_current_org()
set_current_org(instance) set_current_org(instance)
node_root = Node.org_root() node_root = Node.org_root()
@ -78,9 +77,9 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
set_current_org(old_org) set_current_org(old_org)
@receiver(post_delete, sender=Organization) @receiver(pre_delete, sender=Organization)
def on_org_delete(sender, **kwargs): def on_org_delete(sender, instance, **kwargs):
expire_orgs_mapping_for_memory() expire_orgs_mapping_for_memory(instance.id)
@receiver(pre_delete, sender=Organization) @receiver(pre_delete, sender=Organization)

View File

@ -4,7 +4,7 @@ from functools import reduce
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import F from django.db.models import F
from common.db.models import ChoiceSet from common.db.models import TextChoices
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.db import models from common.db import models
from common.utils import lazyproperty from common.utils import lazyproperty
@ -165,7 +165,7 @@ class AssetPermission(BasePermission):
class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel): class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel):
class NodeFrom(ChoiceSet): class NodeFrom(TextChoices):
granted = 'granted', 'Direct node granted' granted = 'granted', 'Direct node granted'
child = 'child', 'Have children node' child = 'child', 'Have children node'
asset = 'asset', 'Direct asset granted' asset = 'asset', 'Direct asset granted'

View File

@ -3,7 +3,6 @@
from rest_framework import serializers from rest_framework import serializers
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from common.drf.serializers import AdaptedBulkListSerializer
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
__all__ = [ __all__ = [
@ -24,14 +23,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer):
fields.extend(['applicationpermission', "applicationpermission_display"]) fields.extend(['applicationpermission', "applicationpermission_display"])
return fields return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField() user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = ApplicationPermission.users.through model = ApplicationPermission.users.through
fields = [ fields = [
'id', 'user', 'user_display', 'id', 'user', 'user_display',
@ -41,7 +37,7 @@ class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.Mod
class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
usergroup_display = serializers.ReadOnlyField() usergroup_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = ApplicationPermission.user_groups.through model = ApplicationPermission.user_groups.through
fields = [ fields = [
'id', 'usergroup', "usergroup_display", 'id', 'usergroup', "usergroup_display",
@ -51,7 +47,7 @@ class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializer
class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer): class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer):
application_display = serializers.ReadOnlyField() application_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = ApplicationPermission.applications.through model = ApplicationPermission.applications.through
fields = [ fields = [
'id', "application", "application_display", 'id', "application", "application_display",
@ -61,7 +57,7 @@ class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializ
class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
systemuser_display = serializers.ReadOnlyField() systemuser_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = ApplicationPermission.system_users.through model = ApplicationPermission.system_users.through
fields = [ fields = [
'id', 'systemuser', 'systemuser_display' 'id', 'systemuser', 'systemuser_display'

View File

@ -3,7 +3,6 @@
from rest_framework import serializers from rest_framework import serializers
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from common.drf.serializers import AdaptedBulkListSerializer
from assets.models import Asset, Node from assets.models import Asset, Node
from perms.models import AssetPermission from perms.models import AssetPermission
from users.models import User from users.models import User
@ -37,14 +36,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer):
fields.extend(['assetpermission', "assetpermission_display"]) fields.extend(['assetpermission', "assetpermission_display"])
return fields return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField() user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = AssetPermission.users.through model = AssetPermission.users.through
fields = [ fields = [
'id', 'user', 'user_display', 'id', 'user', 'user_display',
@ -66,7 +62,7 @@ class AssetPermissionAllUserSerializer(serializers.Serializer):
class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
usergroup_display = serializers.ReadOnlyField() usergroup_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = AssetPermission.user_groups.through model = AssetPermission.user_groups.through
fields = [ fields = [
'id', 'usergroup', "usergroup_display", 'id', 'usergroup', "usergroup_display",
@ -76,7 +72,7 @@ class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.Mode
class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField() asset_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = AssetPermission.assets.through model = AssetPermission.assets.through
fields = [ fields = [
'id', "asset", "asset_display", 'id', "asset", "asset_display",
@ -98,7 +94,7 @@ class AssetPermissionAllAssetSerializer(serializers.Serializer):
class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
node_display = serializers.CharField(source='node.full_value', read_only=True) node_display = serializers.CharField(source='node.full_value', read_only=True)
class Meta(RelationMixin.Meta): class Meta:
model = AssetPermission.nodes.through model = AssetPermission.nodes.through
fields = [ fields = [
'id', 'node', "node_display", 'id', 'node', "node_display",
@ -108,7 +104,7 @@ class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSeri
class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
systemuser_display = serializers.ReadOnlyField() systemuser_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta): class Meta:
model = AssetPermission.system_users.through model = AssetPermission.system_users.through
fields = [ fields = [
'id', 'systemuser', 'systemuser_display' 'id', 'systemuser', 'systemuser_display'

View File

@ -13,17 +13,17 @@ from django.core.cache import cache
from assets.models import Asset from assets.models import Asset
from users.models import User from users.models import User
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.db.models import ChoiceSet from common.db.models import TextChoices
from ..backends import get_multi_command_storage from ..backends import get_multi_command_storage
class Session(OrgModelMixin): class Session(OrgModelMixin):
class LOGIN_FROM(ChoiceSet): class LOGIN_FROM(TextChoices):
ST = 'ST', 'SSH Terminal' ST = 'ST', 'SSH Terminal'
RT = 'RT', 'RDP Terminal' RT = 'RT', 'RDP Terminal'
WT = 'WT', 'Web Terminal' WT = 'WT', 'Web Terminal'
class PROTOCOL(ChoiceSet): class PROTOCOL(TextChoices):
SSH = 'ssh', 'ssh' SSH = 'ssh', 'ssh'
RDP = 'rdp', 'rdp' RDP = 'rdp', 'rdp'
VNC = 'vnc', 'vnc' VNC = 'vnc', 'vnc'

View File

@ -2,7 +2,6 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import Session from ..models import Session
__all__ = [ __all__ = [
@ -16,7 +15,6 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = Session model = Session
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ["id"] fields_mini = ["id"]
fields_small = fields_mini + [ fields_small = fields_mini + [
"user", "asset", "system_user", "user", "asset", "system_user",

View File

@ -85,7 +85,6 @@ class TaskSerializer(BulkModelSerializer):
class Meta: class Meta:
fields = '__all__' fields = '__all__'
model = Task model = Task
list_serializer_class = AdaptedBulkListSerializer
ref_name = 'TerminalTaskSerializer' ref_name = 'TerminalTaskSerializer'

View File

@ -23,7 +23,7 @@ from orgs.models import OrganizationMember, Organization
from common.utils import date_expired_default, get_logger, lazyproperty, random_string from common.utils import date_expired_default, get_logger, lazyproperty, random_string
from common import fields from common import fields
from common.const import choices from common.const import choices
from common.db.models import ChoiceSet from common.db.models import TextChoices
from users.exceptions import MFANotEnabled from users.exceptions import MFANotEnabled
from ..signals import post_user_change_password from ..signals import post_user_change_password
@ -170,7 +170,7 @@ class AuthMixin:
class RoleMixin: class RoleMixin:
class ROLE(ChoiceSet): class ROLE(TextChoices):
ADMIN = choices.ADMIN, _('System administrator') ADMIN = choices.ADMIN, _('System administrator')
AUDITOR = choices.AUDITOR, _('System auditor') AUDITOR = choices.AUDITOR, _('System auditor')
USER = choices.USER, _('User') USER = choices.USER, _('User')

View File

@ -4,7 +4,6 @@ from django.utils.translation import ugettext_lazy as _
from django.db.models import Prefetch from django.db.models import Prefetch
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from django.db.models import Count from django.db.models import Count
from ..models import User, UserGroup from ..models import User, UserGroup
@ -23,7 +22,6 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = UserGroup model = UserGroup
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + [
'comment', 'date_created', 'created_by' 'comment', 'date_created', 'created_by'

View File

@ -113,3 +113,4 @@ termcolor==1.1.0
azure-identity==1.5.0 azure-identity==1.5.0
azure-mgmt-subscription==1.0.0 azure-mgmt-subscription==1.0.0
qingcloud-sdk==1.2.12 qingcloud-sdk==1.2.12
django-simple-history==3.0.0

View File

@ -28,7 +28,7 @@ class AdminUsersGenerator(FakeDataGenerator):
class SystemUsersGenerator(FakeDataGenerator): class SystemUsersGenerator(FakeDataGenerator):
def do_generate(self, batch, batch_size): def do_generate(self, batch, batch_size):
system_users = [] system_users = []
protocols = list(dict(SystemUser.PROTOCOL_CHOICES).keys()) protocols = list(dict(SystemUser.Protocol.choices).keys())
for i in batch: for i in batch:
username = forgery_py.internet.user_name(True) username = forgery_py.internet.user_name(True)
protocol = random.choice(protocols) protocol = random.choice(protocols)