mirror of https://github.com/jumpserver/jumpserver
[Feature] 添加资产用户管理器 (#2489)
* [Feature] 1. 资产用户管理器 * [Feature] 2. 资产用户管理器: 更新AuthBook * [Feature] 3. 资产用户管理器: 添加 AssetUser API * [Feature] 4. AssetUser Model: 添加方法 load_related_asset_auth * [Feature] 5. AdminUser: 更新管理用户获取认证信息时,先加载相关资产的认证 * [Feature] 6. SystemUser: 更新系统用户获取认证信息时,先加载相关资产的认证 * [Feature] 前端页面: 添加资产用户列表页面 * [Feature] 前端页面: 管理用户的资产管理页面添加按钮: 修改资产用户认证信息 * [Feature] 前端页面: 系统用户的资产管理页面添加按钮: 修改资产用户认证信息 * [Feature] 优化: 从管理用户和系统用户的backend中获取相关资产用户的逻辑 * [Update] Fix 1 * [Feature] 优化: SystemUserBackend之filter功能 * [Feature] 优化: AdminUserBackend之filter功能 * [Feature] 优化: AdminUserBackend和SystemUserBackend功能 * [Feature] 更新翻译: 资产用户管理器 * [Update] 更新资产用户列表页名称为: asset_asset_user_list.html * [Bugfix] 修改bug: SystemUserBackend 根据用户名过滤系统用户 * [Feature] 添加: 资产用户列表中可测试资产用户的连接性 * [Update] 修改: AdHoc model的run_as字段从SystemUser外键修改为username字符串 * [Feature] 添加: 获取系统用户认证信息(对应某个资产)API * [Update] 更新: API获取asset user时进行排序 * [Bugfix] 修改: 资产用户可连接性CACHE_KEY * [Update] 更新翻译信息 * [Update] 修改获取资产用户认证信息API的返回响应(200/400) * [Update] 修改BaseUser获取特定资产的方法名 * [Update] 修改logger输出,AuthBook set_version_and_latest * [Update] 修改日志输出添加exc_info参数 * [Update] 移除AuthBook迁移文件0026 * [Bugfix] 修复AdminUserBackend获取instances为空的bugpull/2506/head^2
parent
9bb58afee1
commit
4e705a52eb
|
@ -5,3 +5,4 @@ from .system_user import *
|
|||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets, status, generics
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
|
||||
from ..backends.multi import AssetUserManager
|
||||
from ..models import Asset
|
||||
from .. import serializers
|
||||
from ..tasks import test_asset_users_connectivity_manual
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AssetUserViewSet(viewsets.GenericViewSet):
|
||||
pagination_class = LimitOffsetPagination
|
||||
serializer_class = serializers.AssetUserSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser, )
|
||||
http_method_names = ['get', 'post']
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def get_queryset(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
queryset = AssetUserManager.filter(username=username, asset=asset)
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = sorted(
|
||||
queryset,
|
||||
key=lambda q: (q.asset.hostname, q.connectivity, q.username)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.AssetUserAuthInfoSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
status_code = status.HTTP_200_OK
|
||||
if not instance:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
return Response(serializer.data, status=status_code)
|
||||
|
||||
def get_object(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
try:
|
||||
instance = AssetUserManager.get(username, asset)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return None
|
||||
else:
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset users connective
|
||||
"""
|
||||
|
||||
def get_asset_users(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
asset_users = AssetUserManager.filter(username=username, asset=asset)
|
||||
return asset_users
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_users = self.get_asset_users()
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ from ..tasks import push_system_user_to_assets_manual, \
|
|||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi',
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
|
||||
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
|
||||
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
|
@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
|||
return Response(status=204)
|
||||
|
||||
|
||||
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
aid = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=aid)
|
||||
instance.load_specific_asset_auth(asset)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserPushApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class NotSupportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseBackend:
|
||||
ObjectDoesNotExist = ObjectDoesNotExist
|
||||
MultipleObjectsReturned = MultipleObjectsReturned
|
||||
NotSupportError = NotSupportError
|
||||
MSG_NOT_EXIST = '{} Object matching query does not exist'
|
||||
MSG_MULTIPLE = '{} get() returned more than one object ' \
|
||||
'-- it returned {}!'
|
||||
|
||||
@classmethod
|
||||
def get(cls, username, asset):
|
||||
instances = cls.filter(username, asset)
|
||||
if len(instances) == 1:
|
||||
return instances[0]
|
||||
elif len(instances) == 0:
|
||||
cls.raise_does_not_exist(cls.__name__)
|
||||
else:
|
||||
cls.raise_multiple_return(cls.__name__, len(instances))
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
"""
|
||||
:param username: 用户名
|
||||
:param asset: <Asset>对象
|
||||
:param latest: 是否是最新记录
|
||||
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create(cls, **kwargs):
|
||||
"""
|
||||
:param kwargs:
|
||||
{
|
||||
name, username, asset, comment, password, public_key, private_key,
|
||||
(org_id)
|
||||
}
|
||||
:return: <AuthBook>对象
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def raise_does_not_exist(cls, name):
|
||||
raise cls.ObjectDoesNotExist(cls.MSG_NOT_EXIST.format(name))
|
||||
|
||||
@classmethod
|
||||
def raise_multiple_return(cls, name, length):
|
||||
raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length))
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import AuthBook
|
||||
|
||||
from ..base import BaseBackend
|
||||
|
||||
|
||||
class AuthBookBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
queryset = AuthBook.objects.all()
|
||||
if username:
|
||||
queryset = queryset.filter(username=username)
|
||||
if asset:
|
||||
queryset = queryset.filter(asset=asset)
|
||||
if latest:
|
||||
queryset = queryset.latest_version()
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
auth_info = {
|
||||
'password': kwargs.pop('password', ''),
|
||||
'public_key': kwargs.pop('public_key', ''),
|
||||
'private_key': kwargs.pop('private_key', '')
|
||||
}
|
||||
obj = AuthBook.objects.create(**kwargs)
|
||||
obj.set_auth(**auth_info)
|
||||
return obj
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# from django.conf import settings
|
||||
|
||||
from .db import AuthBookBackend
|
||||
# from .vault import VaultBackend
|
||||
|
||||
|
||||
def get_backend():
|
||||
default_backend = AuthBookBackend
|
||||
|
||||
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
|
||||
# return VaultBackend
|
||||
|
||||
return default_backend
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from ..base import BaseBackend
|
||||
|
||||
|
||||
class VaultBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def get(cls, username, asset):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
pass
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import Asset
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .utils import construct_authbook_object
|
||||
|
||||
|
||||
class AdminUserBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
instances = cls.construct_authbook_objects(username, asset)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _get_assets(cls, asset):
|
||||
if not asset:
|
||||
assets = Asset.objects.all().prefetch_related('admin_user')
|
||||
else:
|
||||
assets = [asset]
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def construct_authbook_objects(cls, username, asset):
|
||||
instances = []
|
||||
assets = cls._get_assets(asset)
|
||||
for asset in assets:
|
||||
if username and asset.admin_user.username != username:
|
||||
continue
|
||||
instance = construct_authbook_object(asset.admin_user, asset)
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise cls.NotSupportError("Not support create")
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .admin_user import AdminUserBackend
|
||||
from .system_user import SystemUserBackend
|
||||
|
||||
|
||||
class AssetUserBackend(BaseBackend):
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
admin_user_instances = AdminUserBackend.filter(username, asset)
|
||||
system_user_instances = SystemUserBackend.filter(username, asset)
|
||||
instances = cls._merge_instances(admin_user_instances, system_user_instances)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _merge_instances(cls, admin_user_instances, system_user_instances):
|
||||
admin_user_instances_keyword_list = [
|
||||
{'username': instance.username, 'asset': instance.asset}
|
||||
for instance in admin_user_instances
|
||||
]
|
||||
instances = [
|
||||
instance for instance in system_user_instances
|
||||
if instance.keyword not in admin_user_instances_keyword_list
|
||||
]
|
||||
admin_user_instances.extend(instances)
|
||||
return admin_user_instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise cls.NotSupportError("Not support create")
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import itertools
|
||||
|
||||
from assets.models import Asset
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .utils import construct_authbook_object
|
||||
|
||||
|
||||
class SystemUserBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
instances = cls.construct_authbook_objects(username, asset)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _distinct_system_users_by_username(cls, system_users):
|
||||
system_users = sorted(
|
||||
system_users,
|
||||
key=lambda su: (su.username, su.priority, su.date_updated),
|
||||
reverse=True,
|
||||
)
|
||||
results = itertools.groupby(system_users, key=lambda su: su.username)
|
||||
system_users = [next(result[1]) for result in results]
|
||||
return system_users
|
||||
|
||||
@classmethod
|
||||
def _filter_system_users_by_username(cls, system_users, username):
|
||||
_system_users = cls._distinct_system_users_by_username(system_users)
|
||||
if username:
|
||||
_system_users = [su for su in _system_users if username == su.username]
|
||||
return _system_users
|
||||
|
||||
@classmethod
|
||||
def _construct_authbook_objects(cls, system_users, asset):
|
||||
instances = []
|
||||
for system_user in system_users:
|
||||
instance = construct_authbook_object(system_user, asset)
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _get_assets_with_system_users(cls, asset=None):
|
||||
"""
|
||||
{ 'asset': set(<SystemUser>, <SystemUser>, ...) }
|
||||
"""
|
||||
if not asset:
|
||||
_assets = Asset.objects.all().prefetch_related('systemuser_set')
|
||||
else:
|
||||
_assets = [asset]
|
||||
|
||||
assets = {asset: set(asset.systemuser_set.all()) for asset in _assets}
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def construct_authbook_objects(cls, username, asset):
|
||||
"""
|
||||
:return: [<AuthBook>, <AuthBook>, ...]
|
||||
"""
|
||||
instances = []
|
||||
assets = cls._get_assets_with_system_users(asset)
|
||||
for _asset, _system_users in assets.items():
|
||||
_system_users = cls._filter_system_users_by_username(_system_users, username)
|
||||
_instances = cls._construct_authbook_objects(_system_users, _asset)
|
||||
instances.extend(_instances)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise Exception("Not support create")
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import AuthBook
|
||||
|
||||
|
||||
def construct_authbook_object(asset_user, asset):
|
||||
"""
|
||||
作用: 将<AssetUser>对象构造成为<AuthBook>对象并返回
|
||||
|
||||
:param asset_user: <AdminUser>或<SystemUser>对象
|
||||
:param asset: <Asset>对象
|
||||
:return: <AuthBook>对象
|
||||
"""
|
||||
fields = [
|
||||
'id', 'name', 'username', 'comment', 'org_id',
|
||||
'_password', '_private_key', '_public_key',
|
||||
'date_created', 'date_updated', 'created_by'
|
||||
]
|
||||
|
||||
obj = AuthBook(asset=asset, version=0, is_latest=True)
|
||||
for field in fields:
|
||||
value = getattr(asset_user, field)
|
||||
setattr(obj, field, value)
|
||||
return obj
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .base import BaseBackend
|
||||
|
||||
from .external.utils import get_backend
|
||||
from .internal.asset_user import AssetUserBackend
|
||||
|
||||
|
||||
class AssetUserManager(BaseBackend):
|
||||
"""
|
||||
资产用户管理器
|
||||
"""
|
||||
external_backend = get_backend()
|
||||
internal_backend = AssetUserBackend
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
external_instance = list(cls.external_backend.filter(username, asset))
|
||||
internal_instance = list(cls.internal_backend.filter(username, asset))
|
||||
instances = cls._merge_instances(external_instance, internal_instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
instance = cls.external_backend.create(**kwargs)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def _merge_instances(cls, external_instances, internal_instances):
|
||||
external_instances_keyword_list = [
|
||||
{'username': instance.username, 'asset': instance.asset}
|
||||
for instance in external_instances
|
||||
]
|
||||
instances = [
|
||||
instance for instance in internal_instances
|
||||
if instance.keyword not in external_instances_keyword_list
|
||||
]
|
||||
external_instances.extend(instances)
|
||||
return external_instances
|
|
@ -32,6 +32,18 @@ TEST_SYSTEM_USER_CONN_TASKS = [
|
|||
}
|
||||
]
|
||||
|
||||
|
||||
ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}'
|
||||
TEST_ASSET_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
TASK_OPTIONS = {
|
||||
'timeout': 10,
|
||||
'forks': 10,
|
||||
|
|
|
@ -7,3 +7,4 @@ from .node import *
|
|||
from .asset import *
|
||||
from .cmd_filter import *
|
||||
from .utils import *
|
||||
from .authbook import *
|
||||
|
|
|
@ -197,6 +197,7 @@ class Asset(OrgModelMixin):
|
|||
|
||||
def get_auth_info(self):
|
||||
if self.admin_user:
|
||||
self.admin_user.load_specific_asset_auth(self)
|
||||
return {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
|
@ -232,6 +233,7 @@ class Asset(OrgModelMixin):
|
|||
"""
|
||||
data = self.to_json()
|
||||
if self.admin_user:
|
||||
self.admin_user.load_specific_asset_auth(self)
|
||||
admin_user = self.admin_user
|
||||
data.update({
|
||||
'username': admin_user.username,
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.mixins import OrgManager
|
||||
|
||||
from .base import AssetUser
|
||||
from ..const import ASSET_USER_CONN_CACHE_KEY
|
||||
|
||||
__all__ = ['AuthBook']
|
||||
|
||||
|
||||
class AuthBookQuerySet(models.QuerySet):
|
||||
|
||||
def latest_version(self):
|
||||
return self.filter(is_latest=True)
|
||||
|
||||
|
||||
class AuthBookManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
class AuthBook(AssetUser):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
|
||||
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
|
||||
def _set_latest(self):
|
||||
self._remove_pre_obj_latest()
|
||||
self.is_latest = True
|
||||
self.save()
|
||||
|
||||
def _get_pre_obj(self):
|
||||
pre_obj = self.__class__.objects.filter(
|
||||
username=self.username, asset=self.asset).latest_version().first()
|
||||
return pre_obj
|
||||
|
||||
def _remove_pre_obj_latest(self):
|
||||
pre_obj = self._get_pre_obj()
|
||||
if pre_obj:
|
||||
pre_obj.is_latest = False
|
||||
pre_obj.save()
|
||||
|
||||
def _set_version(self):
|
||||
pre_obj = self._get_pre_obj()
|
||||
if pre_obj:
|
||||
self.version = pre_obj.version + 1
|
||||
else:
|
||||
self.version = 1
|
||||
self.save()
|
||||
|
||||
def set_version_and_latest(self):
|
||||
self._set_version()
|
||||
self._set_latest()
|
||||
|
||||
@property
|
||||
def _conn_cache_key(self):
|
||||
return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id)
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
value = cache.get(self._conn_cache_key, self.UNKNOWN)
|
||||
return value
|
||||
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
_connectivity = self.UNKNOWN
|
||||
|
||||
for host in value.get('dark', {}).keys():
|
||||
if host == self.asset.hostname:
|
||||
_connectivity = self.UNREACHABLE
|
||||
|
||||
for host in value.get('contacted', {}).keys():
|
||||
if host == self.asset.hostname:
|
||||
_connectivity = self.REACHABLE
|
||||
|
||||
cache.set(self._conn_cache_key, _connectivity, 3600)
|
||||
|
||||
@property
|
||||
def keyword(self):
|
||||
return {'username': self.username, 'asset': self.asset}
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.asset)
|
||||
|
|
@ -9,13 +9,17 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen
|
||||
from common.utils import (
|
||||
get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
|
||||
)
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins import OrgModelMixin
|
||||
from .utils import private_key_validator
|
||||
|
||||
signer = get_signer()
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AssetUser(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
|
@ -45,8 +49,8 @@ class AssetUser(OrgModelMixin):
|
|||
|
||||
@password.setter
|
||||
def password(self, password_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._password = signer.sign(password_raw)
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._password = signer.sign(password_raw)
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
|
@ -55,8 +59,8 @@ class AssetUser(OrgModelMixin):
|
|||
|
||||
@private_key.setter
|
||||
def private_key(self, private_key_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._private_key = signer.sign(private_key_raw)
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._private_key = signer.sign(private_key_raw)
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
|
@ -88,6 +92,11 @@ class AssetUser(OrgModelMixin):
|
|||
else:
|
||||
return None
|
||||
|
||||
@public_key.setter
|
||||
def public_key(self, public_key_raw):
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._public_key = signer.sign(public_key_raw)
|
||||
|
||||
@property
|
||||
def public_key_obj(self):
|
||||
if self.public_key:
|
||||
|
@ -115,6 +124,25 @@ class AssetUser(OrgModelMixin):
|
|||
def get_auth(self, asset=None):
|
||||
pass
|
||||
|
||||
def load_specific_asset_auth(self, asset):
|
||||
from ..backends.multi import AssetUserManager
|
||||
try:
|
||||
other = AssetUserManager.get(username=self.username, asset=asset)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
else:
|
||||
self._merge_auth(other)
|
||||
|
||||
def _merge_auth(self, other):
|
||||
if not other:
|
||||
return
|
||||
if other.password:
|
||||
self.password = other.password
|
||||
if other.public_key:
|
||||
self.public_key = other.public_key
|
||||
if other.private_key:
|
||||
self.private_key = other.private_key
|
||||
|
||||
def clear_auth(self):
|
||||
self._password = ''
|
||||
self._private_key = ''
|
||||
|
|
|
@ -8,3 +8,4 @@ from .system_user import *
|
|||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import AuthBook
|
||||
from ..backends.multi import AssetUserManager
|
||||
|
||||
__all__ = [
|
||||
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
|
||||
]
|
||||
|
||||
|
||||
class AssetUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
password = serializers.CharField(
|
||||
max_length=256, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Password')
|
||||
)
|
||||
public_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Public key')
|
||||
)
|
||||
private_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Private key')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated', 'created_by',
|
||||
'is_latest', 'version', 'connectivity',
|
||||
)
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'username': {'required': True}
|
||||
}
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields = [f for f in fields if not f.startswith('_') and f != 'id']
|
||||
fields.extend(['connectivity'])
|
||||
return fields
|
||||
|
||||
def create(self, validated_data):
|
||||
kwargs = {
|
||||
'name': validated_data.get('name'),
|
||||
'username': validated_data.get('username'),
|
||||
'asset': validated_data.get('asset'),
|
||||
'comment': validated_data.get('comment', ''),
|
||||
'org_id': validated_data.get('org_id', ''),
|
||||
'password': validated_data.get('password'),
|
||||
'public_key': validated_data.get('public_key'),
|
||||
'private_key': validated_data.get('private_key')
|
||||
}
|
||||
instance = AssetUserManager.create(**kwargs)
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields = ['password', 'private_key', 'public_key']
|
|
@ -5,9 +5,12 @@ from django.db.models.signals import post_save, m2m_changed, post_delete
|
|||
from django.dispatch import receiver
|
||||
|
||||
from common.utils import get_logger
|
||||
from .models import Asset, SystemUser, Node
|
||||
from .tasks import update_assets_hardware_info_util, \
|
||||
test_asset_connectivity_util, push_system_user_to_assets
|
||||
from .models import Asset, SystemUser, Node, AuthBook
|
||||
from .tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
push_system_user_to_assets
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -109,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
|
|||
def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
|
||||
if instance and not created:
|
||||
instance.expire_full_value()
|
||||
|
||||
|
||||
@receiver(post_save, sender=AuthBook)
|
||||
def on_auth_book_created(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
logger.debug('Receive create auth book object signal.')
|
||||
instance.set_version_and_latest()
|
||||
|
|
|
@ -26,16 +26,22 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd')
|
|||
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
|
||||
|
||||
|
||||
def check_asset_can_run_ansible(asset):
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
return False
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def clean_hosts(assets):
|
||||
clean_assets = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
if not check_asset_can_run_ansible(asset):
|
||||
continue
|
||||
clean_assets.append(asset)
|
||||
if not clean_assets:
|
||||
|
@ -259,7 +265,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
|||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=system_user, created_by=system_user.org_id,
|
||||
run_as=system_user.username, created_by=system_user.org_id,
|
||||
)
|
||||
result = task.run()
|
||||
set_system_user_connectivity_info(system_user, result)
|
||||
|
@ -370,16 +376,18 @@ def push_system_user_util(system_user, assets, task_name):
|
|||
logger.info(msg)
|
||||
return
|
||||
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
return task.run()
|
||||
for host in hosts:
|
||||
system_user.load_specific_asset_auth(host)
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
task.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
|
@ -415,6 +423,43 @@ def test_admin_user_connectability_period():
|
|||
pass
|
||||
|
||||
|
||||
@shared_task
|
||||
def set_asset_user_connectivity_info(asset_user, result):
|
||||
summary = result[1]
|
||||
asset_user.connectivity = summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_user_connectivity_util(asset_user, task_name):
|
||||
"""
|
||||
:param asset_user: <AuthBook>对象
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
tasks = const.TEST_ASSET_USER_CONN_TASKS
|
||||
if not check_asset_can_run_ansible(asset_user.asset):
|
||||
return
|
||||
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=asset_user.username, created_by=asset_user.org_id
|
||||
)
|
||||
result = task.run()
|
||||
set_asset_user_connectivity_info(asset_user, result)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_users_connectivity_manual(asset_users):
|
||||
"""
|
||||
:param asset_users: <AuthBook>对象
|
||||
"""
|
||||
for asset_user in asset_users:
|
||||
task_name = _("Test asset user connectivity: {}").format(asset_user)
|
||||
test_asset_user_connectivity_util(asset_user, task_name)
|
||||
|
||||
|
||||
# @shared_task
|
||||
# @register_as_period_task(interval=3600)
|
||||
# @after_app_ready_start
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_user_auth_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_modal_confirm').trigger('click'); return false;}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_hostname_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_username_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" id="id_password" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_user_auth_modal_confirm{% endblock %}
|
|
@ -84,6 +84,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
@ -109,9 +110,10 @@ function initTable() {
|
|||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
$(td).html(test_btn);
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
|
||||
$(td).html(test_btn + update_auth_btn);
|
||||
}}
|
||||
],
|
||||
|
||||
|
@ -124,6 +126,15 @@ function initTable() {
|
|||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
function initAssetUserAuthModalForm(hostname, username){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
|
||||
var assetId ;
|
||||
|
||||
$(document).ready(function () {
|
||||
initTable();
|
||||
})
|
||||
|
@ -156,5 +167,38 @@ $(document).ready(function () {
|
|||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
assetId = $(this).data('aid');
|
||||
var hostname = $(this).data('hostname');
|
||||
var username = '{{ admin_user.username }}';
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': "{{ admin_user.username }}",
|
||||
'asset': assetId,
|
||||
'username': "{{ admin_user.username }}",
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load common_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset users of' %} <b>{{ asset.hostname }} </b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table table-hover" id="asset_user_list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Version' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Date updated' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% if asset.protocol == 'ssh' %}
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetUserAuthModalForm(hostname){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
function initAssetUserTable() {
|
||||
var reachable = {{ asset.admin_user.REACHABLE }};
|
||||
var unreachable = {{ asset.admin_user.UNREACHABLE }};
|
||||
var options = {
|
||||
ele: $('#asset_user_list'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (cellData === unreachable) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else if (cellData === reachable) {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
$(td).html(cellData.slice(0, -6));
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
|
||||
{% if asset.protocol == 'ssh' %}
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
|
||||
$(td).html(test_btn + update_auth_btn);
|
||||
{% else %}
|
||||
$(td).html(update_auth_btn);
|
||||
{% endif %}
|
||||
{#var check_btn = ' <a class="btn btn-xs btn-info btn-check-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Check auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);#}
|
||||
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-user-list" %}' + '?asset_id={{ asset.id }}',
|
||||
columns: [
|
||||
{data: function (){return ''}}, {data: "username" },
|
||||
{data: "version"}, {data: "connectivity"}, {data: "date_updated"},
|
||||
{data: "username", orderable: false}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
var username;
|
||||
$(document).ready(function () {
|
||||
initAssetUserTable();
|
||||
})
|
||||
{#.on('click', '.btn-check-asset-user-auth', function(){#}
|
||||
{# var username = $(this).data('username');#}
|
||||
{# var the_url = "{% url 'api-assets:asset-user-auth-info' %}" + '?asset_id={{ asset.id }}' + '&username=' + username;#}
|
||||
{# $.ajax({#}
|
||||
{# url: the_url,#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function (data) {#}
|
||||
{# alert("Password: " + data.password);#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# })#}
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
username = $(this).data('username');
|
||||
var hostname = "{{ asset.hostname }}";
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': username,
|
||||
'asset': "{{ asset.id }}",
|
||||
'username': username,
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var username = $(this).data('username');
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}" + "&username=" + username;
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '#btn-bulk-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -19,6 +19,9 @@
|
|||
<li class="active">
|
||||
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset user list' %} </a>
|
||||
</li>
|
||||
{% if user.is_superuser %}
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
|
@ -32,7 +35,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-7" style="padding-left: 0">
|
||||
<div class="col-sm-8" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ asset.hostname }}</b></span>
|
||||
|
@ -139,7 +142,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% if user.is_superuser or user.is_org_admin %}
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
@ -155,14 +156,15 @@ function initAssetsTable() {
|
|||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var push_btn = '';
|
||||
{% if system_user.auto_push %}
|
||||
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{% endif %}
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
|
||||
$(td).html(push_btn + test_btn);
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
|
||||
$(td).html(push_btn + test_btn + update_auth_btn);
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
|
||||
|
@ -202,6 +204,15 @@ function updateSystemUserNode(nodes) {
|
|||
}
|
||||
jumpserver.nodes_selected = {};
|
||||
|
||||
function initAssetUserAuthModalForm(hostname, username){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
|
||||
var assetId;
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
|
@ -315,6 +326,38 @@ $(document).ready(function () {
|
|||
error: error
|
||||
})
|
||||
})
|
||||
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
assetId = $(this).data('aid');
|
||||
var hostname = $(this).data('hostname');
|
||||
var username = '{{ system_user.username }}';
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': "{{ system_user.username }}",
|
||||
'asset': assetId,
|
||||
'username': "{{ system_user.username }}",
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node')
|
|||
router.register(r'domain', api.DomainViewSet, 'domain')
|
||||
router.register(r'gateway', api.GatewayViewSet, 'gateway')
|
||||
router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter')
|
||||
router.register(r'asset-user', api.AssetUserViewSet, 'asset-user')
|
||||
|
||||
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
|
||||
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
|
||||
|
@ -31,6 +32,12 @@ urlpatterns = [
|
|||
path('assets/<uuid:pk>/gateway/',
|
||||
api.AssetGatewayApi.as_view(), name='asset-gateway'),
|
||||
|
||||
path('asset-user/auth-info/',
|
||||
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
|
||||
path('asset-user/test-connective/',
|
||||
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
|
||||
|
||||
|
||||
path('admin-user/<uuid:pk>/nodes/',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
path('admin-user/<uuid:pk>/auth/',
|
||||
|
@ -42,6 +49,8 @@ urlpatterns = [
|
|||
|
||||
path('system-user/<uuid:pk>/auth-info/',
|
||||
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/auth-info/',
|
||||
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
|
||||
path('system-user/<uuid:pk>/assets/',
|
||||
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('system-user/<uuid:pk>/push/',
|
||||
|
@ -79,6 +88,7 @@ urlpatterns = [
|
|||
|
||||
path('gateway/<uuid:pk>/test-connective/',
|
||||
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + cmd_filter_router.urls
|
||||
|
|
|
@ -15,6 +15,8 @@ urlpatterns = [
|
|||
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
|
||||
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
|
||||
path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
|
||||
# Asset user view
|
||||
path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'),
|
||||
|
||||
# User asset view
|
||||
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),
|
||||
|
|
|
@ -34,7 +34,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
|
|||
|
||||
|
||||
__all__ = [
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView',
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView',
|
||||
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
|
||||
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
|
||||
]
|
||||
|
@ -56,6 +56,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AssetUserListView(AdminUserRequiredMixin, DetailView):
|
||||
model = Asset
|
||||
context_object_name = 'asset'
|
||||
template_name = 'assets/asset_asset_user_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Asset user list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserAssetListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'assets/user_asset_list.html'
|
||||
|
||||
|
|
|
@ -572,3 +572,6 @@ LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
|
|||
|
||||
# User or user group permission cache time, default 3600 seconds
|
||||
ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME
|
||||
|
||||
# Asset user auth external backend, default AuthBook backend
|
||||
BACKEND_ASSET_USER_AUTH_VAULT = False
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -4,11 +4,16 @@
|
|||
from .ansible.inventory import BaseInventory
|
||||
from assets.utils import get_assets_by_id_list, get_system_user_by_id
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory'
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class JMSInventory(BaseInventory):
|
||||
"""
|
||||
JMS Inventory is the manager with jumpserver assets, so you can
|
||||
|
@ -18,7 +23,7 @@ class JMSInventory(BaseInventory):
|
|||
"""
|
||||
:param host_id_list: ["test1", ]
|
||||
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
|
||||
:param run_as: 是否统一使用某个系统用户去执行
|
||||
:param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username)
|
||||
:param become_info: 是否become成某个用户去执行
|
||||
"""
|
||||
self.assets = assets
|
||||
|
@ -33,8 +38,8 @@ class JMSInventory(BaseInventory):
|
|||
host_list.append(info)
|
||||
|
||||
if run_as:
|
||||
run_user_info = self.get_run_user_info()
|
||||
for host in host_list:
|
||||
run_user_info = self.get_run_user_info(host)
|
||||
host.update(run_user_info)
|
||||
|
||||
if become_info:
|
||||
|
@ -69,12 +74,20 @@ class JMSInventory(BaseInventory):
|
|||
info["groups"].append("domain_"+asset.domain.name)
|
||||
return info
|
||||
|
||||
def get_run_user_info(self):
|
||||
system_user = self.run_as
|
||||
if not system_user:
|
||||
def get_run_user_info(self, host):
|
||||
from assets.backends.multi import AssetUserManager
|
||||
|
||||
if not self.run_as:
|
||||
return {}
|
||||
|
||||
try:
|
||||
asset = self.assets.get(id=host.get('id'))
|
||||
run_user = AssetUserManager.get(self.run_as, asset)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return {}
|
||||
else:
|
||||
return system_user._to_secret_json()
|
||||
return run_user._to_secret_json()
|
||||
|
||||
@staticmethod
|
||||
def make_proxy_command(asset):
|
||||
|
|
|
@ -149,7 +149,7 @@ class AdHoc(models.Model):
|
|||
_options: ansible options, more see ops.ansible.runner.Options
|
||||
_hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb
|
||||
run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level
|
||||
run_as: if not run as admin, it run it as a system/common user from cmdb
|
||||
run_as: username(Add the uniform AssetUserManager <AssetUserManager> and change it to username)
|
||||
_become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"]
|
||||
pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts
|
||||
"""
|
||||
|
@ -161,7 +161,7 @@ class AdHoc(models.Model):
|
|||
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
|
||||
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
|
||||
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
|
||||
run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
|
||||
run_as = models.CharField(max_length=64, default='', null=True, verbose_name=_('Username'))
|
||||
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
|
||||
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
|
Loading…
Reference in New Issue