perf: merge connect token rdp option

pull/9169/head
ibuler 2022-12-07 15:13:32 +08:00
commit da36ce9dfd
16 changed files with 536 additions and 353 deletions

View File

@ -20,13 +20,9 @@ logger = get_logger(__file__)
# ------------------------------------
def get_node_assets_mapping_for_memory_pub_sub():
return RedisPubSub('fm.node_all_asset_ids_memory_mapping')
class NodeAssetsMappingForMemoryPubSub(LazyObject):
def _setup(self):
self._wrapped = get_node_assets_mapping_for_memory_pub_sub()
self._wrapped = RedisPubSub('fm.node_all_asset_ids_memory_mapping')
node_assets_mapping_for_memory_pub_sub = NodeAssetsMappingForMemoryPubSub()

View File

@ -19,12 +19,12 @@ from common.utils import random_string
from common.utils.django import get_request_os
from orgs.mixins.api import RootOrgViewMixin
from perms.models import ActionChoices
from terminal.const import NativeClient, TerminalType
from terminal.connect_methods import NativeClient, ConnectMethodUtil
from terminal.models import EndpointRule, Applet
from ..models import ConnectionToken
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer,
SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer
)
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
@ -115,7 +115,8 @@ class RDPFileClientProtocolURLMixin:
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
# 设置远程应用
self.set_applet_info(token, rdp_options)
remote_app_options = token.get_remote_app_option()
rdp_options.update(remote_app_options)
# 文件名
name = token.asset.name
@ -145,7 +146,7 @@ class RDPFileClientProtocolURLMixin:
_os = get_request_os(self.request)
connect_method_name = token.connect_method
connect_method_dict = TerminalType.get_connect_method(
connect_method_dict = ConnectMethodUtil.get_connect_method(
token.connect_method, token.protocol, _os
)
if connect_method_dict is None:
@ -227,38 +228,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
search_fields = filterset_fields
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'list': 'authentication.view_connectiontoken',
'retrieve': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'expire': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
@action(methods=['POST'], detail=False, url_path='secret')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('id') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
if token.is_expired:
raise ValidationError({'id': 'Token is expired'})
token.is_valid()
serializer = self.get_serializer(instance=token)
expire_now = request.data.get('expire_now', True)
if expire_now:
token.expire()
return Response(serializer.data, status=status.HTTP_200_OK)
def get_queryset(self):
queryset = ConnectionToken.objects \
.filter(user=self.request.user) \
@ -305,10 +284,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
serializer_classes = {
'default': SuperConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken'
'renewal': 'authentication.add_superconnectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
}
def get_queryset(self):
@ -332,3 +315,38 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)
@action(methods=['POST'], detail=False, url_path='secret')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('id') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
if token.is_expired:
raise ValidationError({'id': 'Token is expired'})
token.is_valid()
serializer = self.get_serializer(instance=token)
expire_now = request.data.get('expire_now', True)
if expire_now:
token.expire()
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['POST'], detail=False, url_path='applet-option')
def get_applet_info(self, *args, **kwargs):
token_id = self.request.data.get('id')
token = get_object_or_404(ConnectionToken, pk=token_id)
if token.is_expired:
return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST)
data = token.get_applet_option()
serializer = ConnectTokenAppletOptionSerializer(data)
return Response(serializer.data)
@action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release')
def release_applet_account(self, *args, **kwargs):
account_id = self.request.data.get('id')
msg = ConnectionToken.release_applet_account(account_id)
return Response({'msg': msg})

View File

@ -1,6 +1,9 @@
import base64
import json
from datetime import timedelta
from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
@ -9,9 +12,10 @@ from rest_framework.exceptions import PermissionDenied
from assets.const import Protocol
from common.db.fields import EncryptCharField
from common.db.models import JMSBaseModel
from common.utils import lazyproperty, pretty_string
from common.utils import lazyproperty, pretty_string, bulk_get
from common.utils.timezone import as_current_tz
from orgs.mixins.models import OrgModelMixin
from terminal.models import Applet
def date_expired_default():
@ -101,6 +105,9 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
error = _('No account')
raise PermissionDenied(error)
if timezone.now() - self.date_created < timedelta(seconds=60):
return True, None
if not self.permed_account or not self.permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.account
@ -115,6 +122,75 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
def platform(self):
return self.asset.platform
@lazyproperty
def connect_method_object(self):
from common.utils import get_request_os
from jumpserver.utils import get_current_request
from terminal.connect_methods import ConnectMethodUtil
request = get_current_request()
os = get_request_os(request) if request else 'windows'
method = ConnectMethodUtil.get_connect_method(
self.connect_method, protocol=self.protocol, os=os
)
return method
def get_remote_app_option(self):
cmdline = {
'app_name': self.connect_method,
'user_id': str(self.user.id),
'asset_id': str(self.asset.id),
'token_id': str(self.id)
}
cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode()
app = '||tinker'
options = {
'remoteapplicationmode:i': '1',
'remoteapplicationprogram:s': app,
'remoteapplicationname:s': app,
'alternate shell:s': app,
'remoteapplicationcmdline:s': cmdline_b64,
}
return options
def get_applet_option(self):
method = self.connect_method_object
if not method or method.get('type') != 'applet' or method.get('disabled', False):
return None
applet = Applet.objects.filter(name=method.get('value')).first()
if not applet:
return None
host_account = applet.select_host_account()
if not host_account:
return None
host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl'))
gateway = host.gateway.select_gateway() if host.domain else None
data = {
'id': account.id,
'applet': applet,
'host': host,
'gateway': gateway,
'account': account,
'remote_app_option': self.get_remote_app_option()
}
token_account_relate_key = f'token_account_relate_{account.id}'
cache.set(token_account_relate_key, lock_key, ttl)
return data
@staticmethod
def release_applet_account(account_id):
token_account_relate_key = f'token_account_relate_{account_id}'
lock_key = cache.get(token_account_relate_key)
if lock_key:
cache.delete(lock_key)
cache.delete(token_account_relate_key)
return 'released'
return 'not found or expired'
@lazyproperty
def account_object(self):
from assets.models import Account

View File

@ -1,19 +1,17 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import ObjectRelatedField
from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Account, Platform, Gateway, Domain
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
from users.models import User
from perms.serializers.permission import ActionChoicesField
from common.drf.fields import ObjectRelatedField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.serializers.permission import ActionChoicesField
from users.models import User
from ..models import ConnectionToken
__all__ = [
'ConnectionTokenSecretSerializer',
'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer'
]
@ -96,6 +94,24 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer):
return names
class _ConnectionTokenConnectMethodSerializer(serializers.Serializer):
name = serializers.CharField(label=_('Name'))
protocol = serializers.CharField(label=_('Protocol'))
os = serializers.CharField(label=_('OS'))
is_builtin = serializers.BooleanField(label=_('Is builtin'))
is_active = serializers.BooleanField(label=_('Is active'))
platform = _ConnectionTokenPlatformSerializer(label=_('Platform'))
action = ActionChoicesField(label=_('Action'))
options = serializers.JSONField(label=_('Options'))
class _ConnectTokenConnectMethodSerializer(serializers.Serializer):
label = serializers.CharField(label=_('Label'))
value = serializers.CharField(label=_('Value'))
type = serializers.CharField(label=_('Type'))
component = serializers.CharField(label=_('Component'))
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = _ConnectionTokenUserSerializer(read_only=True)
asset = _ConnectionTokenAssetSerializer(read_only=True)
@ -104,30 +120,28 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
platform = _ConnectionTokenPlatformSerializer(read_only=True)
domain = ObjectRelatedField(queryset=Domain.objects, required=False, label=_('Domain'))
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
actions = ActionChoicesField()
expire_at = serializers.IntegerField()
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
connect_method = serializers.SerializerMethodField(label=_('Connect method'))
class Meta:
model = ConnectionToken
fields = [
'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at', 'expire_now',
'connect_method'
'domain', 'gateway', 'actions', 'expire_at',
'expire_now', 'connect_method',
]
extra_kwargs = {
'value': {'read_only': True},
}
def get_connect_method(self, obj):
from terminal.const import TerminalType
from common.utils import get_request_os
request = self.context.get('request')
if request:
os = get_request_os(request)
else:
os = 'windows'
method = TerminalType.get_connect_method(obj.connect_method, protocol=obj.protocol, os=os)
return method
class ConnectTokenAppletOptionSerializer(serializers.Serializer):
id = serializers.CharField(label=_('ID'))
applet = ObjectRelatedField(read_only=True)
host = _ConnectionTokenAssetSerializer(read_only=True)
account = _ConnectionTokenAccountSerializer(read_only=True)
gateway = _ConnectionTokenGatewaySerializer(read_only=True)
remote_app_option = serializers.JSONField(read_only=True)

View File

@ -2,7 +2,6 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from ..models import ConnectionToken
__all__ = [

View File

@ -3,7 +3,6 @@ import platform
from redis.sentinel import SentinelManagedSSLConnection
if platform.system() == 'Darwin' and platform.machine() == 'arm64':
import pymysql
@ -308,17 +307,22 @@ else:
REDIS_SENTINEL_SOCKET_TIMEOUT = None
# Cache config
REDIS_OPTIONS = {
"REDIS_CLIENT_KWARGS": {
"health_check_interval": 30
},
"CONNECTION_POOL_KWARGS": {
'max_connections': 100,
}
}
if REDIS_USE_SSL:
REDIS_OPTIONS['CONNECTION_POOL_KWARGS'].update({
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA
} if REDIS_USE_SSL else {}
}
})
if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS:
REDIS_LOCATION_NO_DB = "%(protocol)s://%(service_name)s/{}" % {
@ -348,7 +352,6 @@ else:
'host': CONFIG.REDIS_HOST, 'port': CONFIG.REDIS_PORT,
}
REDIS_CACHE_DEFAULT = {
'BACKEND': 'redis_lock.django_cache.RedisCache',
'LOCATION': REDIS_LOCATION_NO_DB.format(CONFIG.REDIS_DB_CACHE),

View File

@ -1,32 +1,26 @@
import json
from importlib import import_module
import inspect
from importlib import import_module
from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.functional import LazyObject
from common.decorator import on_transaction_commit
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from notifications.backends import BACKEND
from users.models import User
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription
from .notifications import SystemMessage
logger = get_logger(__name__)
def new_site_msg_pub_sub():
return RedisPubSub('notifications.SiteMessageCome')
class NewSiteMsgSubPub(LazyObject):
def _setup(self):
self._wrapped = new_site_msg_pub_sub()
self._wrapped = RedisPubSub('notifications.SiteMessageCome')
new_site_msg_chan = NewSiteMsgSubPub()
@ -78,7 +72,8 @@ def create_system_messages(app_config: AppConfig, **kwargs):
sub, created = SystemMsgSubscription.objects.get_or_create(message_type=message_type)
if created:
obj.post_insert_to_db(sub)
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
logger.info(
f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
except ModuleNotFoundError:
pass

View File

@ -3,35 +3,30 @@
from collections import defaultdict
from functools import partial
import django.db.utils
from django.dispatch import receiver
from django.conf import settings
from django.db.utils import ProgrammingError, OperationalError
from django.utils.functional import LazyObject
from django.db.models.signals import post_save, pre_delete, m2m_changed
from django.db.utils import ProgrammingError, OperationalError
from django.dispatch import receiver
from django.utils.functional import LazyObject
from orgs.utils import tmp_to_org, set_to_default_org
from orgs.models import Organization
from orgs.hands import set_current_org, Node, get_current_org
from perms.models import AssetPermission
from users.models import UserGroup, User
from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.decorator import on_transaction_commit
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from orgs.hands import set_current_org, Node, get_current_org
from orgs.models import Organization
from orgs.utils import tmp_to_org, set_to_default_org
from perms.models import AssetPermission
from users.models import UserGroup, User
from users.signals import post_user_leave_org
logger = get_logger(__file__)
def get_orgs_mapping_for_memory_pub_sub():
return RedisPubSub('fm.orgs_mapping')
class OrgsMappingForMemoryPubSub(LazyObject):
def _setup(self):
self._wrapped = get_orgs_mapping_for_memory_pub_sub()
self._wrapped = RedisPubSub('fm.orgs_mapping')
orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub()

View File

@ -18,13 +18,9 @@ from .models import Setting
logger = get_logger(__file__)
def get_settings_pub_sub():
return RedisPubSub('settings')
class SettingSubPub(LazyObject):
def _setup(self):
self._wrapped = get_settings_pub_sub()
self._wrapped = RedisPubSub('settings')
setting_pub_sub = SettingSubPub()

View File

@ -1,4 +1,5 @@
from .terminal import *
from .storage import *
from .status import *
from .connect_methods import *
from .endpoint import *
from .status import *
from .storage import *
from .terminal import *

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
#
from rest_framework import generics
from rest_framework.views import Response
from common.permissions import IsValidUser
from common.utils import get_request_os
from terminal import serializers
from terminal.connect_methods import ConnectMethodUtil
__all__ = ['ConnectMethodListApi']
class ConnectMethodListApi(generics.ListAPIView):
serializer_class = serializers.ConnectMethodSerializer
permission_classes = [IsValidUser]
def get_queryset(self):
os = get_request_os(self.request)
return ConnectMethodUtil.get_protocols_connect_methods(os)
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
return Response(queryset)

View File

@ -10,16 +10,13 @@ from rest_framework.views import APIView, Response
from common.drf.api import JMSBulkModelViewSet
from common.exceptions import JMSException
from common.permissions import IsValidUser
from common.permissions import WithBootstrapToken
from common.utils import get_request_os
from terminal import serializers
from terminal.const import TerminalType
from terminal.models import Terminal
__all__ = [
'TerminalViewSet', 'TerminalConfig',
'TerminalRegistrationApi', 'ConnectMethodListApi'
'TerminalRegistrationApi',
]
logger = logging.getLogger(__file__)
@ -72,15 +69,3 @@ class TerminalRegistrationApi(generics.CreateAPIView):
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return super().create(request, *args, **kwargs)
class ConnectMethodListApi(generics.ListAPIView):
serializer_class = serializers.ConnectMethodSerializer
permission_classes = [IsValidUser]
def get_queryset(self):
os = get_request_os(self.request)
return TerminalType.get_protocols_connect_methods(os)
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
return Response(queryset)

View File

@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from assets.const import Protocol
from .const import TerminalType
class WebMethod(TextChoices):
web_gui = 'web_gui', 'Web GUI'
web_cli = 'web_cli', 'Web CLI'
web_sftp = 'web_sftp', 'Web SFTP'
@classmethod
def get_methods(cls):
return {
Protocol.ssh: [cls.web_cli, cls.web_sftp],
Protocol.telnet: [cls.web_cli],
Protocol.rdp: [cls.web_gui],
Protocol.vnc: [cls.web_gui],
Protocol.mysql: [cls.web_cli, cls.web_gui],
Protocol.mariadb: [cls.web_cli, cls.web_gui],
Protocol.oracle: [cls.web_cli, cls.web_gui],
Protocol.postgresql: [cls.web_cli, cls.web_gui],
Protocol.sqlserver: [cls.web_cli, cls.web_gui],
Protocol.redis: [cls.web_cli],
Protocol.mongodb: [cls.web_cli],
Protocol.k8s: [cls.web_gui],
Protocol.http: []
}
class NativeClient(TextChoices):
# Koko
ssh = 'ssh', 'SSH'
putty = 'putty', 'PuTTY'
xshell = 'xshell', 'Xshell'
# Magnus
mysql = 'db_client_mysql', _('DB Client')
psql = 'db_client_psql', _('DB Client')
sqlplus = 'db_client_sqlplus', _('DB Client')
redis = 'db_client_redis', _('DB Client')
mongodb = 'db_client_mongodb', _('DB Client')
# Razor
mstsc = 'mstsc', 'Remote Desktop'
@classmethod
def get_native_clients(cls):
# native client 关注的是 endpoint 的 protocol,
# 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议
clients = {
Protocol.ssh: {
'default': [cls.ssh],
'windows': [cls.putty],
},
Protocol.rdp: [cls.mstsc],
Protocol.mysql: [cls.mysql],
Protocol.oracle: [cls.sqlplus],
Protocol.postgresql: [cls.psql],
Protocol.redis: [cls.redis],
Protocol.mongodb: [cls.mongodb],
}
return clients
@classmethod
def get_target_protocol(cls, name, os):
for protocol, clients in cls.get_native_clients().items():
if isinstance(clients, dict):
clients = clients.get(os) or clients.get('default')
if name in clients:
return protocol
return None
@classmethod
def get_methods(cls, os='windows'):
clients_map = cls.get_native_clients()
methods = defaultdict(list)
for protocol, _clients in clients_map.items():
if isinstance(_clients, dict):
_clients = _clients.get(os, _clients['default'])
for client in _clients:
methods[protocol].append({
'value': client.value,
'label': client.label,
'type': 'native',
})
return methods
@classmethod
def get_launch_command(cls, name, token, endpoint, os='windows'):
username = f'JMS-{token.id}'
commands = {
cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}',
cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}',
cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}',
# cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p',
# cls.psql: {
# 'default': 'psql -h {hostname} -p {port} -U {username} -W',
# 'windows': 'psql /h {hostname} /p {port} /U {username} -W',
# },
# cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}',
# cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}',
}
command = commands.get(name)
if isinstance(command, dict):
command = command.get(os, command.get('default'))
return command
class AppletMethod:
@classmethod
def get_methods(cls):
from .models import Applet, AppletHost
methods = defaultdict(list)
has_applet_hosts = AppletHost.objects.all().exists()
applets = Applet.objects.filter(is_active=True)
for applet in applets:
for protocol in applet.protocols:
methods[protocol].append({
'value': applet.name,
'label': applet.display_name,
'type': 'applet',
'icon': applet.icon,
'disabled': not applet.is_active or not has_applet_hosts,
})
return methods
class ConnectMethodUtil:
_all_methods = None
@classmethod
def protocols(cls):
protocols = {
TerminalType.koko: {
'web_methods': [WebMethod.web_cli, WebMethod.web_sftp],
'listen': [Protocol.ssh, Protocol.http],
'support': [
Protocol.ssh, Protocol.telnet,
Protocol.mysql, Protocol.postgresql,
Protocol.oracle, Protocol.sqlserver,
Protocol.mariadb, Protocol.redis,
Protocol.mongodb, Protocol.k8s,
Protocol.clickhouse,
],
'match': 'm2m'
},
TerminalType.omnidb: {
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
Protocol.sqlserver, Protocol.mariadb
],
'match': 'm2m'
},
TerminalType.lion: {
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [Protocol.rdp, Protocol.vnc],
'match': 'm2m'
},
TerminalType.magnus: {
'listen': [],
'support': [
Protocol.mysql, Protocol.postgresql,
Protocol.oracle, Protocol.mariadb
],
'match': 'map'
},
TerminalType.razor: {
'listen': [Protocol.rdp],
'support': [Protocol.rdp],
'match': 'map'
},
}
return protocols
@classmethod
def get_connect_method(cls, name, protocol, os='linux'):
methods = cls.get_protocols_connect_methods(os)
protocol_methods = methods.get(protocol, [])
for method in protocol_methods:
if method['value'] == name:
return method
return None
@classmethod
def refresh_methods(cls):
cls._all_methods = None
@classmethod
def get_protocols_connect_methods(cls, os):
if cls._all_methods is not None:
return cls._all_methods
methods = defaultdict(list)
web_methods = WebMethod.get_methods()
native_methods = NativeClient.get_methods(os)
applet_methods = AppletMethod.get_methods()
for component, component_protocol in cls.protocols().items():
support = component_protocol['support']
for protocol in support:
# Web 方式
protocol_web_methods = set(web_methods.get(protocol, [])) \
& set(component_protocol.get('web_methods', []))
methods[protocol.value].extend([
{
'component': component.value,
'type': 'web',
'endpoint_protocol': 'http',
'value': method.value,
'label': method.label,
}
for method in protocol_web_methods
])
# 客户端方式
if component_protocol['match'] == 'map':
listen = [protocol]
else:
listen = component_protocol['listen']
for listen_protocol in listen:
# Native method
methods[protocol.value].extend([
{
'component': component.value,
'type': 'native',
'endpoint_protocol': listen_protocol,
**method
}
for method in native_methods[listen_protocol]
])
# 远程应用方式,这个只有 tinker 提供
for protocol, applet_methods in applet_methods.items():
for method in applet_methods:
method['listen'] = 'rdp'
method['component'] = TerminalType.tinker.value
methods[protocol].extend(applet_methods)
cls._all_methods = methods
return methods

View File

@ -1,16 +1,9 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from assets.const import Protocol
# Replay & Command Storage Choices
# --------------------------------
class ReplayStorageType(TextChoices):
null = 'null', 'Null',
@ -44,129 +37,6 @@ class ComponentLoad(TextChoices):
return set(dict(cls.choices).keys())
class WebMethod(TextChoices):
web_gui = 'web_gui', 'Web GUI'
web_cli = 'web_cli', 'Web CLI'
web_sftp = 'web_sftp', 'Web SFTP'
@classmethod
def get_methods(cls):
return {
Protocol.ssh: [cls.web_cli, cls.web_sftp],
Protocol.telnet: [cls.web_cli],
Protocol.rdp: [cls.web_gui],
Protocol.vnc: [cls.web_gui],
Protocol.mysql: [cls.web_cli, cls.web_gui],
Protocol.mariadb: [cls.web_cli, cls.web_gui],
Protocol.oracle: [cls.web_cli, cls.web_gui],
Protocol.postgresql: [cls.web_cli, cls.web_gui],
Protocol.sqlserver: [cls.web_cli, cls.web_gui],
Protocol.clickhouse: [cls.web_cli],
Protocol.redis: [cls.web_cli],
Protocol.mongodb: [cls.web_cli],
Protocol.k8s: [cls.web_gui],
Protocol.http: []
}
class NativeClient(TextChoices):
# Koko
ssh = 'ssh', 'SSH'
putty = 'putty', 'PuTTY'
xshell = 'xshell', 'Xshell'
# Magnus
mysql = 'db_client_mysql', _('DB Client')
psql = 'db_client_psql', _('DB Client')
sqlplus = 'db_client_sqlplus', _('DB Client')
redis = 'db_client_redis', _('DB Client')
mongodb = 'db_client_mongodb', _('DB Client')
# Razor
mstsc = 'mstsc', 'Remote Desktop'
@classmethod
def get_native_clients(cls):
# native client 关注的是 endpoint 的 protocol,
# 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议
clients = {
Protocol.ssh: {
'default': [cls.ssh],
'windows': [cls.putty],
},
Protocol.rdp: [cls.mstsc],
Protocol.mysql: [cls.mysql],
Protocol.oracle: [cls.sqlplus],
Protocol.postgresql: [cls.psql],
Protocol.redis: [cls.redis],
Protocol.mongodb: [cls.mongodb],
}
return clients
@classmethod
def get_target_protocol(cls, name, os):
for protocol, clients in cls.get_native_clients().items():
if isinstance(clients, dict):
clients = clients.get(os) or clients.get('default')
if name in clients:
return protocol
return None
@classmethod
def get_methods(cls, os='windows'):
clients_map = cls.get_native_clients()
methods = defaultdict(list)
for protocol, _clients in clients_map.items():
if isinstance(_clients, dict):
_clients = _clients.get(os, _clients['default'])
for client in _clients:
methods[protocol].append({
'value': client.value,
'label': client.label,
'type': 'native',
})
return methods
@classmethod
def get_launch_command(cls, name, token, endpoint, os='windows'):
username = f'JMS-{token.id}'
commands = {
cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}',
cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}',
cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}',
# cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p',
# cls.psql: {
# 'default': 'psql -h {hostname} -p {port} -U {username} -W',
# 'windows': 'psql /h {hostname} /p {port} /U {username} -W',
# },
# cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}',
# cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}',
}
command = commands.get(name)
if isinstance(command, dict):
command = command.get(os, command.get('default'))
return command
class AppletMethod:
@classmethod
def get_methods(cls):
from .models import Applet
applets = Applet.objects.all()
methods = defaultdict(list)
for applet in applets:
for protocol in applet.protocols:
methods[protocol].append({
'value': applet.name,
'label': applet.display_name,
'icon': applet.icon,
})
return methods
class TerminalType(TextChoices):
koko = 'koko', 'KoKo'
guacamole = 'guacamole', 'Guacamole'
@ -182,107 +52,3 @@ class TerminalType(TextChoices):
@classmethod
def types(cls):
return set(dict(cls.choices).keys())
@classmethod
def protocols(cls):
protocols = {
cls.koko: {
'web_methods': [WebMethod.web_cli, WebMethod.web_sftp],
'listen': [Protocol.ssh, Protocol.http],
'support': [
Protocol.ssh, Protocol.telnet,
Protocol.mysql, Protocol.postgresql,
Protocol.oracle, Protocol.sqlserver,
Protocol.mariadb, Protocol.clickhouse,
Protocol.redis, Protocol.mongodb, Protocol.k8s,
],
'match': 'm2m'
},
cls.omnidb: {
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
Protocol.sqlserver, Protocol.mariadb
],
'match': 'm2m'
},
cls.lion: {
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [Protocol.rdp, Protocol.vnc],
'match': 'm2m'
},
cls.magnus: {
'listen': [],
'support': [
Protocol.mysql, Protocol.postgresql,
Protocol.oracle, Protocol.mariadb
],
'match': 'map'
},
cls.razor: {
'listen': [Protocol.rdp],
'support': [Protocol.rdp],
'match': 'map'
},
}
return protocols
@classmethod
def get_connect_method(cls, name, protocol, os='linux'):
methods = cls.get_protocols_connect_methods(os)
protocol_methods = methods.get(protocol, [])
for method in protocol_methods:
if method['value'] == name:
return method
return None
@classmethod
def get_protocols_connect_methods(cls, os):
methods = defaultdict(list)
web_methods = WebMethod.get_methods()
native_methods = NativeClient.get_methods(os)
applet_methods = AppletMethod.get_methods()
for component, component_protocol in cls.protocols().items():
support = component_protocol['support']
for protocol in support:
if component_protocol['match'] == 'map':
listen = [protocol]
else:
listen = component_protocol['listen']
for listen_protocol in listen:
# Native method
methods[protocol.value].extend([
{
'component': component.value,
'type': 'native',
'endpoint_protocol': listen_protocol,
**method
}
for method in native_methods[listen_protocol]
])
protocol_web_methods = set(web_methods.get(protocol, [])) \
& set(component_protocol.get('web_methods', []))
methods[protocol.value].extend([
{
'component': component.value,
'type': 'web',
'endpoint_protocol': 'http',
'value': method.value,
'label': method.label,
}
for method in protocol_web_methods
])
for protocol, applet_methods in applet_methods.items():
for method in applet_methods:
method['type'] = 'applet'
method['listen'] = 'rdp'
method['component'] = cls.tinker.value
methods[protocol].extend(applet_methods)
return methods

View File

@ -1,14 +1,15 @@
import yaml
import os.path
import random
import yaml
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
__all__ = ['Applet', 'AppletPublication']
@ -53,10 +54,43 @@ class Applet(JMSBaseModel):
return None
return os.path.join(settings.MEDIA_URL, 'applets', self.name, 'icon.png')
def select_host_account(self):
hosts = list(self.hosts.all())
if not hosts:
return None
host = random.choice(hosts)
using_keys = cache.keys('host_accounts_{}_*'.format(host.id)) or []
accounts_used = cache.get_many(using_keys)
accounts = host.accounts.all().exclude(username__in=accounts_used)
if not accounts:
accounts = host.accounts.all()
if not accounts:
return None
account = random.choice(accounts)
ttl = 60 * 60 * 24
lock_key = 'applet_host_accounts_{}_{}'.format(host.id, account.username)
cache.set(lock_key, account.username, ttl)
return {
'host': host,
'account': account,
'lock_key': lock_key,
'ttl': ttl
}
@staticmethod
def release_host_and_account(host_id, username):
key = 'applet_host_accounts_{}_{}'.format(host_id, username)
cache.delete(key)
class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet'))
host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host'))
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications',
verbose_name=_('Applet'))
host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications',
verbose_name=_('Host'))
status = models.CharField(max_length=16, default='ready', verbose_name=_('Status'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))

View File

@ -4,17 +4,18 @@
from django.db.models.signals import post_save, post_delete
from django.db.utils import ProgrammingError
from django.dispatch import receiver
from django.utils.functional import LazyObject
from assets.models import Asset
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from orgs.utils import tmp_to_builtin_org
from assets.models import Asset
from .utils import db_port_manager, DBPortManager
from .models import Applet, AppletHost
from .utils import db_port_manager, DBPortManager
db_port_manager: DBPortManager
logger = get_logger(__file__)
@ -27,6 +28,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs):
with tmp_to_builtin_org(system=1):
instance.generate_accounts()
applet_host_change_pub_sub.publish(True)
@receiver(post_save, sender=Applet)
def on_applet_create(sender, instance, created=False, **kwargs):
@ -35,6 +38,8 @@ def on_applet_create(sender, instance, created=False, **kwargs):
hosts = AppletHost.objects.all()
instance.hosts.set(hosts)
applet_host_change_pub_sub.publish(True)
@receiver(django_ready)
def init_db_port_mapper(sender, **kwargs):
@ -59,3 +64,22 @@ def on_db_app_delete(sender, instance, **kwargs):
if not instance.category != 'database':
return
db_port_manager.pop(instance)
class AppletHostPubSub(LazyObject):
def _setup(self):
self._wrapped = RedisPubSub('fm.applet_host_change')
@receiver(django_ready)
def subscribe_applet_host_change(sender, **kwargs):
logger.debug("Start subscribe for expire node assets id mapping from memory")
def on_change(message):
from terminal.connect_methods import ConnectMethodUtil
ConnectMethodUtil.refresh_methods()
applet_host_change_pub_sub.subscribe(on_change)
applet_host_change_pub_sub = AppletHostPubSub()