Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/9169/head
Bai 2022-12-07 18:38:12 +08:00
commit d252ee41ed
26 changed files with 4798 additions and 3334 deletions

View File

@ -12,7 +12,6 @@ def migrate_platform_type_to_lower(apps, *args):
class Migration(migrations.Migration):
dependencies = [
('assets', '0094_auto_20220402_1736'),
]
@ -51,7 +50,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='platform',
name='su_method',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='SU method'),
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Su method'),
),
migrations.RunPython(migrate_platform_type_to_lower)
]

View File

@ -2,9 +2,8 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from assets.const import AllTypes
from common.db.fields import JsonDictTextField
from assets.const import Protocol
from common.db.fields import JsonDictTextField
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
@ -83,7 +82,7 @@ class Platform(models.Model):
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
# 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("SU method"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
blank=True, null=True, verbose_name=_("Automation"))

View File

@ -25,7 +25,7 @@ class ProtocolSettingSerializer(serializers.Serializer):
sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home"))
# HTTP
auto_fill = serializers.BooleanField(default=False, label=_("Auto fill"))
autofile = serializers.BooleanField(default=False, label=_("Autofill"))
username_selector = serializers.CharField(
default="", allow_blank=True, label=_("Username selector")
)
@ -38,7 +38,6 @@ class ProtocolSettingSerializer(serializers.Serializer):
class PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta:
model = PlatformAutomation
fields = [

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,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5cc8f923c01a87b106a54f8a7c53abdb98683b1c4b4f975f9a3ae8af5fae73c8
size 373
oid sha256:03b1fcb75dae7e070f662f2ad554774d51311d2561367f5d28addc3b14899195
size 119767

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c0ba1103efe746ecf579fe27832b5d2969858508f4aabdcc42723b13c1b01f8
size 383
oid sha256:6bd3c45b4301a45fa1b110716b3ea78bcbb53a2913f707ab754882df061256cc
size 98368

File diff suppressed because it is too large Load Diff

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

@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
#
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from common.utils import get_logger
from common.db.models import JMSBaseModel
from common.utils import get_logger, lazyproperty
from ..models import Organization
from ..utils import (
set_current_org, get_current_org, current_org, filter_org_queryset
)
from ..models import Organization
logger = get_logger(__file__)
@ -75,7 +75,7 @@ class OrgModelMixin(models.Model):
self.org_id = org.id
return super().save(*args, **kwargs)
@property
@lazyproperty
def org(self):
return Organization.get_instance(self.org_id)

View File

@ -3,8 +3,8 @@ import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty, settings
from common.tree import TreeNode
from common.utils import lazyproperty, settings
class OrgRoleMixin:
@ -33,7 +33,6 @@ class OrgRoleMixin:
def get_origin_role_members(self, role_name):
from rbac.models import OrgRoleBinding
from users.models import User
from rbac.builtin import BuiltinRole
from .utils import tmp_to_org
@ -132,6 +131,7 @@ class Organization(OrgRoleMixin, models.Model):
@classmethod
def expire_orgs_mapping(cls):
print("Expire orgs mapping: ")
cls.orgs_mapping = None
def org_id(self):

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()
@ -61,6 +56,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
def on_org_create_or_update(sender, instance, created=False, **kwargs):
# 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到
expire_orgs_mapping_for_memory(instance.id)
old_org = get_current_org()
set_current_org(instance)
node_root = Node.org_root()

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()

View File

@ -39,7 +39,6 @@ jms-storage==0.0.44
simplejson==3.17.6
six==1.16.0
sshpubkeys==3.3.1
sshtunnel==0.4.0
uritemplate==4.1.1
urllib3==1.26.9
vine==5.0.0