mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-22 11:58:29 +00:00
refactor: 重构 Connection Token 模块 (完成获取 Super connection token API 逻辑) (#8559)
* refactor: 重构 Connection Token 模块 (完成 Model 设计和创建 Token 的API逻辑) * refactor: 重构 Connection Token 模块 (完成获取 Token 详细信息的 API 逻辑) * refactor: 重构 Connection Token 模块 (完成获取 RDP 文件 API 逻辑) * refactor: 重构 Connection Token 模块 (完成获取 Client url API 逻辑) * refactor: 重构 Connection Token 模块 (完成获取 Super connection token API 逻辑) * refactor: 重构 Connection Token 模块 (完成删除原 Connection token 逻辑) * refactor: 重构 Connection Token 模块 (完成删除原 Connection) * refactor: 重构 Connection Token 模块 (完善序列类字段) * refactor: 重构 Connection Token 模块 (完善expire API) * refactor: 重构 Connection Token 模块 (完善迁移文件) * refactor: 重构 Connection Token 模块 (完善翻译文件) * refactor: 重构 Connection Token 模块 (拆分Connection ViewSet) * refactor: 重构 Connection Token 模块 (修改翻译) * refactor: 重构 Connection Token 模块 (优化) Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
This commit is contained in:
@@ -1,58 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import urllib.parse
|
||||
import json
|
||||
from typing import Callable
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import ctypes
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
import urllib.parse
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.request import Request
|
||||
|
||||
from applications.models import Application
|
||||
from authentication.signals import post_auth_failed
|
||||
from common.utils import get_logger, random_string
|
||||
from common.mixins.api import SerializerMixin
|
||||
from common.utils.common import get_file_by_arch
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from common.drf.api import JMSModelViewSet
|
||||
from common.http import is_true
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from perms.models.base import Action
|
||||
from perms.utils.application.permission import get_application_actions
|
||||
from perms.utils.asset.permission import get_asset_actions
|
||||
from common.const.http import PATCH
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
"""
|
||||
下载客户端支持的连接文件,里面包含了 token,和 其他连接信息
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
|
||||
- [x] RDP
|
||||
- [ ] KoKo
|
||||
|
||||
本质上,这里还是暴露出 token 来,进行使用
|
||||
"""
|
||||
class ConnectionTokenMixin:
|
||||
request: Request
|
||||
get_serializer: Callable
|
||||
create_token: Callable
|
||||
get_serializer_context: Callable
|
||||
|
||||
@staticmethod
|
||||
def check_token_valid(token: ConnectionToken):
|
||||
is_valid, error = token.check_valid()
|
||||
if not is_valid:
|
||||
raise PermissionDenied(error)
|
||||
|
||||
@staticmethod
|
||||
def get_request_resources(serializer):
|
||||
user = serializer.validated_data.get('user')
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data.get('system_user')
|
||||
return user, asset, application, system_user
|
||||
|
||||
@staticmethod
|
||||
def check_user_has_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
from perms.utils.application import has_application_system_permission
|
||||
|
||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||
error = f'User not has this asset and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||
raise PermissionDenied(error)
|
||||
|
||||
if application and not has_application_system_permission(user, application, system_user):
|
||||
error = f'User not has this application and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||
raise PermissionDenied(error)
|
||||
|
||||
def get_smart_endpoint(self, protocol, asset=None, application=None):
|
||||
if asset:
|
||||
@@ -64,21 +66,32 @@ class ClientProtocolMixin:
|
||||
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
|
||||
return endpoint
|
||||
|
||||
def get_request_resource(self, serializer):
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data['system_user']
|
||||
|
||||
user = serializer.validated_data.get('user')
|
||||
user = user if user else self.request.user
|
||||
return asset, application, system_user, user
|
||||
|
||||
@staticmethod
|
||||
def parse_env_bool(env_key, env_default, true_value, false_value):
|
||||
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
|
||||
|
||||
def get_rdp_file_content(self, serializer):
|
||||
options = {
|
||||
def get_client_protocol_data(self, token: ConnectionToken):
|
||||
from assets.models import SystemUser
|
||||
protocol = token.system_user.protocol
|
||||
username = token.user.username
|
||||
rdp_config = ssh_token = ''
|
||||
if protocol == SystemUser.Protocol.rdp:
|
||||
filename, rdp_config = self.get_rdp_file_info(token)
|
||||
elif protocol == SystemUser.Protocol.ssh:
|
||||
filename, ssh_token = self.get_ssh_token(token)
|
||||
else:
|
||||
raise ValueError('Protocol not support: {}'.format(protocol))
|
||||
|
||||
return {
|
||||
"filename": filename,
|
||||
"protocol": protocol,
|
||||
"username": username,
|
||||
"token": ssh_token,
|
||||
"config": rdp_config
|
||||
}
|
||||
|
||||
def get_rdp_file_info(self, token: ConnectionToken):
|
||||
rdp_options = {
|
||||
'full address:s': '',
|
||||
'username:s': '',
|
||||
# 'screen mode id:i': '1',
|
||||
@@ -111,412 +124,162 @@ class ClientProtocolMixin:
|
||||
# 'remoteapplicationcmdline:s': '',
|
||||
}
|
||||
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
# 设置磁盘挂载
|
||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||
if drives_redirect:
|
||||
actions = Action.choices_to_value(token.actions)
|
||||
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
|
||||
rdp_options['drivestoredirect:s'] = '*'
|
||||
|
||||
# 设置全屏
|
||||
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||
rdp_options['screen mode id:i'] = '2' if full_screen else '1'
|
||||
|
||||
# 设置 RDP Server 地址
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='rdp', asset=token.asset, application=token.application
|
||||
)
|
||||
rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
|
||||
|
||||
# 设置用户名
|
||||
rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id))
|
||||
if token.system_user.ad_domain:
|
||||
rdp_options['domain:s'] = token.system_user.ad_domain
|
||||
|
||||
# 设置宽高
|
||||
height = self.request.query_params.get('height')
|
||||
width = self.request.query_params.get('width')
|
||||
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
|
||||
# 设置磁盘挂载
|
||||
if drives_redirect:
|
||||
actions = 0
|
||||
if asset:
|
||||
actions = get_asset_actions(user, asset, system_user)
|
||||
elif application:
|
||||
actions = get_application_actions(user, application, system_user)
|
||||
|
||||
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
|
||||
options['drivestoredirect:s'] = '*'
|
||||
|
||||
# 全屏
|
||||
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||
|
||||
# RDP Server 地址
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='rdp', asset=asset, application=application
|
||||
)
|
||||
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
|
||||
# 用户名
|
||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||
if system_user.ad_domain:
|
||||
options['domain:s'] = system_user.ad_domain
|
||||
# 宽高
|
||||
if width and height:
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
|
||||
rdp_options['desktopwidth:i'] = width
|
||||
rdp_options['desktopheight:i'] = height
|
||||
rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
|
||||
|
||||
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
||||
# 设置其他选项
|
||||
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
||||
|
||||
if asset:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
application.get_rdp_remote_app_setting()
|
||||
|
||||
app = f'||jmservisor'
|
||||
options['remoteapplicationmode:i'] = '1'
|
||||
options['alternate shell:s'] = app
|
||||
options['remoteapplicationprogram:s'] = app
|
||||
options['remoteapplicationname:s'] = name
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
elif token.application and token.application.category_remote_app:
|
||||
app = '||jmservisor'
|
||||
name = token.application.name
|
||||
rdp_options['remoteapplicationmode:i'] = '1'
|
||||
rdp_options['alternate shell:s'] = app
|
||||
rdp_options['remoteapplicationprogram:s'] = app
|
||||
rdp_options['remoteapplicationname:s'] = name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
|
||||
content = ''
|
||||
for k, v in options.items():
|
||||
for k, v in rdp_options.items():
|
||||
content += f'{k}:{v}\n'
|
||||
return name, content
|
||||
|
||||
def get_ssh_token(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
if asset:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
return filename, content
|
||||
|
||||
def get_ssh_token(self, token: ConnectionToken):
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
elif token.application:
|
||||
name = token.application.name
|
||||
else:
|
||||
name = '*'
|
||||
filename = f'{token.user.username}-{name}-jumpserver'
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=asset, application=application
|
||||
protocol='ssh', asset=token.asset, application=token.application
|
||||
)
|
||||
content = {
|
||||
data = {
|
||||
'ip': endpoint.host,
|
||||
'port': str(endpoint.ssh_port),
|
||||
'username': f'JMS-{token}',
|
||||
'password': secret
|
||||
'username': 'JMS-{}'.format(str(token.id)),
|
||||
'password': token.secret
|
||||
}
|
||||
token = json.dumps(content)
|
||||
return name, token
|
||||
|
||||
def get_encrypt_cmdline(self, app: Application):
|
||||
parameters = app.get_rdp_remote_app_setting()['parameters']
|
||||
parameters = parameters.encode('ascii')
|
||||
|
||||
lib_path = get_file_by_arch('xpack/libs', 'librailencrypt.so')
|
||||
lib = ctypes.CDLL(lib_path)
|
||||
lib.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_int]
|
||||
lib.encrypt.restype = ctypes.c_char_p
|
||||
|
||||
rst = lib.encrypt(parameters, len(parameters))
|
||||
rst = rst.decode('ascii')
|
||||
return rst
|
||||
|
||||
def get_valid_serializer(self):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
def get_client_protocol_data(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
protocol = system_user.protocol
|
||||
username = user.username
|
||||
config, token = '', ''
|
||||
if protocol == 'rdp':
|
||||
name, config = self.get_rdp_file_content(serializer)
|
||||
elif protocol == 'ssh':
|
||||
name, token = self.get_ssh_token(serializer)
|
||||
else:
|
||||
raise ValueError('Protocol not support: {}'.format(protocol))
|
||||
|
||||
filename = "{}-{}-jumpserver".format(username, name)
|
||||
data = {
|
||||
"filename": filename,
|
||||
"protocol": system_user.protocol,
|
||||
"username": username,
|
||||
"token": token,
|
||||
"config": config
|
||||
}
|
||||
return data
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
serializer = self.get_valid_serializer()
|
||||
try:
|
||||
protocol_data = self.get_client_protocol_data(serializer)
|
||||
except ValueError as e:
|
||||
return Response({'error': str(e)}, status=401)
|
||||
|
||||
protocol_data = json.dumps(protocol_data).encode()
|
||||
protocol_data = base64.b64encode(protocol_data).decode()
|
||||
data = {
|
||||
'url': 'jms://{}'.format(protocol_data),
|
||||
}
|
||||
return Response(data=data)
|
||||
token = json.dumps(data)
|
||||
return filename, token
|
||||
|
||||
|
||||
class SecretDetailMixin:
|
||||
valid_token: Callable
|
||||
request: Request
|
||||
get_serializer: Callable
|
||||
|
||||
@staticmethod
|
||||
def _get_application_secret_detail(application):
|
||||
gateway = None
|
||||
remote_app = None
|
||||
asset = None
|
||||
|
||||
if application.category_remote_app:
|
||||
remote_app = application.get_rdp_remote_app_setting()
|
||||
asset = application.get_remote_app_asset()
|
||||
domain = asset.domain
|
||||
else:
|
||||
domain = application.domain
|
||||
|
||||
if domain and domain.has_gateway():
|
||||
gateway = domain.random_gateway()
|
||||
|
||||
return {
|
||||
'asset': asset,
|
||||
'application': application,
|
||||
'gateway': gateway,
|
||||
'domain': domain,
|
||||
'remote_app': remote_app,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_asset_secret_detail(asset):
|
||||
gateway = None
|
||||
if asset and asset.domain and asset.domain.has_gateway():
|
||||
gateway = asset.domain.random_gateway()
|
||||
|
||||
return {
|
||||
'asset': asset,
|
||||
'application': None,
|
||||
'domain': asset.domain,
|
||||
'gateway': gateway,
|
||||
'remote_app': None,
|
||||
}
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
perm_required = 'authentication.view_connectiontokensecret'
|
||||
|
||||
# 非常重要的 api,再逻辑层再判断一下,双重保险
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Not allow to view secret')
|
||||
|
||||
token = request.data.get('token', '')
|
||||
try:
|
||||
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
|
||||
except serializers.ValidationError as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username='', request=self.request,
|
||||
reason=_('Invalid token')
|
||||
)
|
||||
raise e
|
||||
|
||||
data = dict(
|
||||
id=token, secret=value.get('secret', ''),
|
||||
user=user, system_user=system_user,
|
||||
expired_at=expired_at, actions=actions
|
||||
)
|
||||
cmd_filter_kwargs = {
|
||||
'system_user_id': system_user.id,
|
||||
'user_id': user.id,
|
||||
}
|
||||
if asset:
|
||||
asset_detail = self._get_asset_secret_detail(asset)
|
||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||
data['type'] = 'asset'
|
||||
data.update(asset_detail)
|
||||
cmd_filter_kwargs['asset_id'] = asset.id
|
||||
else:
|
||||
app_detail = self._get_application_secret_detail(app)
|
||||
system_user.load_app_more_auth(app.id, user.username, user.id)
|
||||
data['type'] = 'application'
|
||||
data.update(app_detail)
|
||||
cmd_filter_kwargs['application_id'] = app.id
|
||||
|
||||
from assets.models import CommandFilterRule
|
||||
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
|
||||
data['cmd_filter_rules'] = cmd_filter_rules
|
||||
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(data=serializer.data, status=200)
|
||||
|
||||
|
||||
class TokenCacheMixin:
|
||||
""" endpoint smart view 用到此类来解析token中的资产、应用 """
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
|
||||
def renewal_token(self, token, ttl=None):
|
||||
value = self.get_token_from_cache(token)
|
||||
if value:
|
||||
pre_ttl = self.get_token_ttl(token)
|
||||
self.set_token_to_cache(token, value, ttl)
|
||||
post_ttl = self.get_token_ttl(token)
|
||||
ok = True
|
||||
msg = f'{pre_ttl}s is renewed to {post_ttl}s.'
|
||||
else:
|
||||
ok = False
|
||||
msg = 'Token is not found.'
|
||||
data = {
|
||||
'ok': ok,
|
||||
'msg': msg
|
||||
}
|
||||
return data
|
||||
|
||||
def get_token_ttl(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
return cache.ttl(key)
|
||||
|
||||
def set_token_to_cache(self, token, value, ttl=None):
|
||||
key = self.get_token_cache_key(token)
|
||||
ttl = ttl or settings.CONNECTION_TOKEN_EXPIRATION
|
||||
cache.set(key, value, timeout=ttl)
|
||||
|
||||
def get_token_from_cache(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
value = cache.get(key, None)
|
||||
return value
|
||||
|
||||
def get_token_cache_key(self, token):
|
||||
return self.CACHE_KEY_PREFIX.format(token)
|
||||
|
||||
|
||||
class BaseUserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
from perms.utils.application import has_application_system_permission
|
||||
|
||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||
error = f'User not has this asset and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||
raise PermissionDenied(error)
|
||||
if application and not has_application_system_permission(user, application, system_user):
|
||||
error = f'User not has this application and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=None):
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
value = {
|
||||
'id': token,
|
||||
'secret': secret,
|
||||
'user': str(user.id),
|
||||
'username': user.username,
|
||||
'system_user': str(system_user.id),
|
||||
'system_user_name': system_user.name,
|
||||
'created_by': str(self.request.user),
|
||||
'date_created': str(timezone.now())
|
||||
}
|
||||
|
||||
if asset:
|
||||
value.update({
|
||||
'type': 'asset',
|
||||
'asset': str(asset.id),
|
||||
'hostname': asset.hostname,
|
||||
})
|
||||
elif application:
|
||||
value.update({
|
||||
'type': 'application',
|
||||
'application': application.id,
|
||||
'application_name': str(application)
|
||||
})
|
||||
|
||||
self.set_token_to_cache(token, value, ttl)
|
||||
return token, secret
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
tp = 'app' if application else 'asset'
|
||||
data = {
|
||||
"id": token, 'secret': secret,
|
||||
'type': tp, 'protocol': system_user.protocol,
|
||||
'expire_time': self.get_token_ttl(token),
|
||||
}
|
||||
return Response(data, status=201)
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
|
||||
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||
filterset_fields = (
|
||||
'type',
|
||||
'user_display', 'system_user_display', 'application_display', 'asset_display'
|
||||
)
|
||||
search_fields = filterset_fields
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': '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',
|
||||
}
|
||||
queryset = ConnectionToken.objects.all()
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
from applications.models import Application
|
||||
from perms.utils.asset.permission import validate_permission as asset_validate_permission
|
||||
from perms.utils.application.permission import validate_permission as app_validate_permission
|
||||
def create_connection_token(self):
|
||||
data = self.request.query_params if self.request.method == 'GET' else self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
token: ConnectionToken = serializer.instance
|
||||
return token
|
||||
|
||||
value = self.get_token_from_cache(token)
|
||||
if not value:
|
||||
raise serializers.ValidationError('Token not found')
|
||||
def perform_create(self, serializer):
|
||||
user, asset, application, system_user = self.get_request_resources(serializer)
|
||||
self.check_user_has_resource_permission(user, asset, application, system_user)
|
||||
return super(ConnectionTokenViewSet, self).perform_create(serializer)
|
||||
|
||||
user = get_object_or_404(User, id=value.get('user'))
|
||||
if not user.is_valid:
|
||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
# 非常重要的 api,在逻辑层再判断一下,双重保险
|
||||
perm_required = 'authentication.view_connectiontokensecret'
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Not allow to view secret')
|
||||
token_id = request.data.get('token') or ''
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
self.check_token_valid(token)
|
||||
token.load_system_user_auth()
|
||||
serializer = self.get_serializer(instance=token)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
asset = None
|
||||
app = None
|
||||
if value.get('type') == 'asset':
|
||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||
if not asset.is_active:
|
||||
raise serializers.ValidationError("Asset disabled")
|
||||
has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user)
|
||||
else:
|
||||
app = get_object_or_404(Application, id=value.get('application'))
|
||||
has_perm, actions, expired_at = app_validate_permission(user, app, system_user)
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
token = self.create_connection_token()
|
||||
self.check_token_valid(token)
|
||||
filename, content = self.get_rdp_file_info(token)
|
||||
filename = '{}.rdp'.format(filename)
|
||||
response = HttpResponse(content, content_type='application/octet-stream')
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
if not has_perm:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app, expired_at, actions
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
token = self.create_connection_token()
|
||||
self.check_token_valid(token)
|
||||
try:
|
||||
protocol_data = self.get_client_protocol_data(token)
|
||||
except ValueError as e:
|
||||
return Response(data={'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
protocol_data = json.dumps(protocol_data).encode()
|
||||
protocol_data = base64.b64encode(protocol_data).decode()
|
||||
data = {
|
||||
'url': 'jms://{}'.format(protocol_data)
|
||||
}
|
||||
return Response(data=data)
|
||||
|
||||
def get(self, request):
|
||||
token = request.query_params.get('token')
|
||||
value = self.get_token_from_cache(token)
|
||||
if not value:
|
||||
return Response('', status=404)
|
||||
return Response(value)
|
||||
@action(methods=['PATCH'], detail=True)
|
||||
def expire(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.expire()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UserSuperConnectionTokenViewSet(
|
||||
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
serializer_classes = {
|
||||
'default': SuperConnectionTokenSerializer,
|
||||
}
|
||||
@@ -525,10 +288,19 @@ class UserSuperConnectionTokenViewSet(
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
}
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
@action(methods=['PATCH'], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
from common.utils.timezone import as_current_tz
|
||||
|
||||
token_id = request.data.get('token') or ''
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
date_expired = as_current_tz(token.date_expired)
|
||||
if token.is_expired:
|
||||
raise PermissionDenied('Token is expired at: {}'.format(date_expired))
|
||||
token.renewal()
|
||||
data = {
|
||||
'ok': True,
|
||||
'msg': f'Token is renewed, date expired: {date_expired}'
|
||||
}
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
|
89
apps/authentication/migrations/0011_auto_20220705_1940.py
Normal file
89
apps/authentication/migrations/0011_auto_20220705_1940.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# Generated by Django 3.2.12 on 2022-07-05 11:40
|
||||
|
||||
import authentication.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0021_auto_20220629_1826'),
|
||||
('assets', '0091_auto_20220629_1826'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('authentication', '0010_temptoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='application',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='applications.application', verbose_name='Application'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='application_display',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='Application display'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='asset',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.asset', verbose_name='Asset'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='asset_display',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='Asset display'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='date_expired',
|
||||
field=models.DateTimeField(default=authentication.models.date_expired_default, verbose_name='Date expired'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='secret',
|
||||
field=models.CharField(default='', max_length=64, verbose_name='Secret'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='system_user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.systemuser', verbose_name='System user'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='system_user_display',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='System user display'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('asset', 'Asset'), ('application', 'Application')], default='asset', max_length=16, verbose_name='Type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='user_display',
|
||||
field=models.CharField(default='', max_length=128, verbose_name='User display'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connectiontoken',
|
||||
name='id',
|
||||
field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='connectiontoken',
|
||||
options={'ordering': ('-date_expired',), 'permissions': [('view_connectiontokensecret', 'Can view connection token secret')], 'verbose_name': 'Connection token'},
|
||||
),
|
||||
]
|
@@ -1,11 +1,14 @@
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from rest_framework.authtoken.models import Token
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
from common.db import models
|
||||
from common.utils import lazyproperty
|
||||
from common.utils.timezone import as_current_tz
|
||||
|
||||
|
||||
class AccessKey(models.Model):
|
||||
@@ -54,16 +57,189 @@ class SSOToken(models.JMSBaseModel):
|
||||
verbose_name = _('SSO token')
|
||||
|
||||
|
||||
class ConnectionToken(models.JMSBaseModel):
|
||||
# Todo: 未来可能放到这里,不记录到 redis 了,虽然方便,但是不易于审计
|
||||
# Todo: add connection token 可能要授权给 普通用户, 或者放开就行
|
||||
def date_expired_default():
|
||||
return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION)
|
||||
|
||||
|
||||
class ConnectionToken(OrgModelMixin, models.JMSModel):
|
||||
class Type(models.TextChoices):
|
||||
asset = 'asset', _('Asset')
|
||||
application = 'application', _('Application')
|
||||
|
||||
type = models.CharField(
|
||||
max_length=16, default=Type.asset, choices=Type.choices, verbose_name=_("Type")
|
||||
)
|
||||
secret = models.CharField(max_length=64, default='', verbose_name=_("Secret"))
|
||||
date_expired = models.DateTimeField(
|
||||
default=date_expired_default, verbose_name=_("Date expired")
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.SET_NULL, verbose_name=_('User'),
|
||||
related_name='connection_tokens', null=True, blank=True
|
||||
)
|
||||
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
|
||||
system_user = models.ForeignKey(
|
||||
'assets.SystemUser', on_delete=models.SET_NULL, verbose_name=_('System user'),
|
||||
related_name='connection_tokens', null=True, blank=True
|
||||
)
|
||||
system_user_display = models.CharField(
|
||||
max_length=128, default='', verbose_name=_("System user display")
|
||||
)
|
||||
asset = models.ForeignKey(
|
||||
'assets.Asset', on_delete=models.SET_NULL, verbose_name=_('Asset'),
|
||||
related_name='connection_tokens', null=True, blank=True
|
||||
)
|
||||
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
|
||||
application = models.ForeignKey(
|
||||
'applications.Application', on_delete=models.SET_NULL, verbose_name=_('Application'),
|
||||
related_name='connection_tokens', null=True, blank=True
|
||||
)
|
||||
application_display = models.CharField(
|
||||
max_length=128, default='', verbose_name=_("Application display")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-date_expired',)
|
||||
verbose_name = _('Connection token')
|
||||
permissions = [
|
||||
('view_connectiontokensecret', _('Can view connection token secret'))
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_default_date_expired(cls):
|
||||
return date_expired_default()
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.date_expired < timezone.now()
|
||||
|
||||
def expire(self):
|
||||
self.date_expired = timezone.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return not self.is_expired
|
||||
|
||||
def is_type(self, tp):
|
||||
return self.type == tp
|
||||
|
||||
def renewal(self):
|
||||
""" 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """
|
||||
self.date_expired = self.get_default_date_expired()
|
||||
self.save()
|
||||
|
||||
actions = expired_at = None # actions 和 expired_at 在 check_valid() 中赋值
|
||||
|
||||
def check_valid(self):
|
||||
from perms.utils.asset.permission import validate_permission as asset_validate_permission
|
||||
from perms.utils.application.permission import validate_permission as app_validate_permission
|
||||
|
||||
if self.is_expired:
|
||||
is_valid = False
|
||||
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
|
||||
return is_valid, error
|
||||
|
||||
if not self.user:
|
||||
is_valid = False
|
||||
error = _('User not exists')
|
||||
return is_valid, error
|
||||
if not self.user.is_valid:
|
||||
is_valid = False
|
||||
error = _('User invalid, disabled or expired')
|
||||
return is_valid, error
|
||||
|
||||
if not self.system_user:
|
||||
is_valid = False
|
||||
error = _('System user not exists')
|
||||
return is_valid, error
|
||||
|
||||
if self.is_type(self.Type.asset):
|
||||
if not self.asset:
|
||||
is_valid = False
|
||||
error = _('Asset not exists')
|
||||
return is_valid, error
|
||||
if not self.asset.is_active:
|
||||
is_valid = False
|
||||
error = _('Asset inactive')
|
||||
return is_valid, error
|
||||
has_perm, actions, expired_at = asset_validate_permission(
|
||||
self.user, self.asset, self.system_user
|
||||
)
|
||||
if not has_perm:
|
||||
is_valid = False
|
||||
error = _('User has no permission to access asset or permission expired')
|
||||
return is_valid, error
|
||||
self.actions = actions
|
||||
self.expired_at = expired_at
|
||||
|
||||
elif self.is_type(self.Type.application):
|
||||
if not self.application:
|
||||
is_valid = False
|
||||
error = _('Application not exists')
|
||||
return is_valid, error
|
||||
has_perm, actions, expired_at = app_validate_permission(
|
||||
self.user, self.application, self.system_user
|
||||
)
|
||||
if not has_perm:
|
||||
is_valid = False
|
||||
error = _('User has no permission to access application or permission expired')
|
||||
return is_valid, error
|
||||
self.actions = actions
|
||||
self.expired_at = expired_at
|
||||
|
||||
return True, ''
|
||||
|
||||
@lazyproperty
|
||||
def domain(self):
|
||||
if self.asset:
|
||||
return self.asset.domain
|
||||
if not self.application:
|
||||
return
|
||||
if self.application.category_remote_app:
|
||||
asset = self.application.get_remote_app_asset()
|
||||
domain = asset.domain if asset else None
|
||||
else:
|
||||
domain = self.application.domain
|
||||
return domain
|
||||
|
||||
@lazyproperty
|
||||
def gateway(self):
|
||||
from assets.models import Domain
|
||||
if not self.domain:
|
||||
return
|
||||
self.domain: Domain
|
||||
return self.domain.random_gateway()
|
||||
|
||||
@lazyproperty
|
||||
def remote_app(self):
|
||||
if not self.application:
|
||||
return {}
|
||||
if not self.application.category_remote_app:
|
||||
return {}
|
||||
return self.application.get_rdp_remote_app_setting()
|
||||
|
||||
@lazyproperty
|
||||
def cmd_filter_rules(self):
|
||||
from assets.models import CommandFilterRule
|
||||
kwargs = {
|
||||
'user_id': self.user.id,
|
||||
'system_user_id': self.system_user.id,
|
||||
}
|
||||
if self.asset:
|
||||
kwargs['asset_id'] = self.asset.id
|
||||
elif self.application:
|
||||
kwargs['application_id'] = self.application_id
|
||||
rules = CommandFilterRule.get_queryset(**kwargs)
|
||||
return rules
|
||||
|
||||
def load_system_user_auth(self):
|
||||
if self.asset:
|
||||
self.system_user.load_asset_more_auth(self.asset.id, self.user.username, self.user.id)
|
||||
elif self.application:
|
||||
self.system_user.load_app_more_auth(self.application.id, self.user.username, self.user.id)
|
||||
|
||||
|
||||
class TempToken(models.JMSModel):
|
||||
username = models.CharField(max_length=128, verbose_name=_("Username"))
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from .token import *
|
||||
from .connect_token import *
|
||||
from .connection_token import *
|
||||
from .password_mfa import *
|
||||
from .confirm import *
|
||||
|
@@ -1,145 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
|
||||
from applications.models import Application
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.base import ActionsField
|
||||
|
||||
__all__ = [
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenApplicationSerializer',
|
||||
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
|
||||
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
|
||||
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
|
||||
'SuperConnectionTokenSerializer'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(serializers.Serializer):
|
||||
system_user = serializers.CharField(max_length=128, required=True)
|
||||
asset = serializers.CharField(max_length=128, required=False)
|
||||
application = serializers.CharField(max_length=128, required=False)
|
||||
|
||||
@staticmethod
|
||||
def validate_system_user(system_user_id):
|
||||
from assets.models import SystemUser
|
||||
system_user = SystemUser.objects.filter(id=system_user_id).first()
|
||||
if system_user is None:
|
||||
raise serializers.ValidationError('system_user id not exist')
|
||||
return system_user
|
||||
|
||||
@staticmethod
|
||||
def validate_asset(asset_id):
|
||||
from assets.models import Asset
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
if asset is None:
|
||||
raise serializers.ValidationError('asset id not exist')
|
||||
return asset
|
||||
|
||||
@staticmethod
|
||||
def validate_application(app_id):
|
||||
from applications.models import Application
|
||||
app = Application.objects.filter(id=app_id).first()
|
||||
if app is None:
|
||||
raise serializers.ValidationError('app id not exist')
|
||||
return app
|
||||
|
||||
def validate(self, attrs):
|
||||
asset = attrs.get('asset')
|
||||
application = attrs.get('application')
|
||||
if not asset and not application:
|
||||
raise serializers.ValidationError('asset or application required')
|
||||
if asset and application:
|
||||
raise serializers.ValidationError('asset and application should only one')
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user_id):
|
||||
from users.models import User
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if user is None:
|
||||
raise serializers.ValidationError('user id not exist')
|
||||
return user
|
||||
|
||||
|
||||
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'name', 'username', 'email']
|
||||
|
||||
|
||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
protocols = ProtocolsField(label='Protocols', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key',
|
||||
'protocol', 'ad_domain', 'org_id'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key']
|
||||
|
||||
|
||||
class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
|
||||
program = serializers.CharField()
|
||||
working_directory = serializers.CharField()
|
||||
parameters = serializers.CharField()
|
||||
|
||||
|
||||
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
|
||||
attrs = serializers.JSONField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
|
||||
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = ['id', 'name', 'gateways']
|
||||
|
||||
|
||||
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
'id', 'type', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action', 'date_created',
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSecretSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(read_only=True)
|
||||
secret = serializers.CharField(read_only=True)
|
||||
type = serializers.ChoiceField(choices=[('application', 'Application'), ('asset', 'Asset')])
|
||||
user = ConnectionTokenUserSerializer(read_only=True)
|
||||
asset = ConnectionTokenAssetSerializer(read_only=True)
|
||||
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
|
||||
application = ConnectionTokenApplicationSerializer(read_only=True)
|
||||
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
|
||||
cmd_filter_rules = ConnectionTokenFilterRuleSerializer(many=True)
|
||||
domain = ConnectionTokenDomainSerializer(read_only=True)
|
||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
actions = ActionsField()
|
||||
expired_at = serializers.IntegerField()
|
186
apps/authentication/serializers/connection_token.py
Normal file
186
apps/authentication/serializers/connection_token.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from authentication.models import ConnectionToken
|
||||
from common.utils import pretty_string
|
||||
from common.utils.random import random_string
|
||||
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
|
||||
from users.models import User
|
||||
from applications.models import Application
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.base import ActionsField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
|
||||
'SuperConnectionTokenSerializer'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
|
||||
validity = serializers.BooleanField(source='is_valid', read_only=True, label=_('Validity'))
|
||||
|
||||
class Meta:
|
||||
model = ConnectionToken
|
||||
fields_mini = ['id', 'type']
|
||||
fields_small = fields_mini + [
|
||||
'secret', 'date_expired',
|
||||
'date_created', 'date_updated', 'created_by', 'updated_by',
|
||||
'org_id', 'org_name',
|
||||
]
|
||||
fields_fk = [
|
||||
'user', 'system_user', 'asset', 'application',
|
||||
]
|
||||
read_only_fields = [
|
||||
# 普通 Token 不支持指定 user
|
||||
'user', 'validity',
|
||||
'type_display', 'user_display', 'system_user_display', 'asset_display',
|
||||
'application_display',
|
||||
]
|
||||
fields = fields_small + fields_fk + read_only_fields
|
||||
|
||||
def validate(self, attrs):
|
||||
fields_attrs = self.construct_internal_fields_attrs(attrs)
|
||||
attrs.update(fields_attrs)
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def request_user(self):
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.user
|
||||
|
||||
def get_user(self, attrs):
|
||||
return self.request_user
|
||||
|
||||
def construct_internal_fields_attrs(self, attrs):
|
||||
user = self.get_user(attrs)
|
||||
system_user = attrs.get('system_user') or ''
|
||||
asset = attrs.get('asset') or ''
|
||||
application = attrs.get('application') or ''
|
||||
secret = attrs.get('secret') or random_string(64)
|
||||
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
|
||||
|
||||
if isinstance(asset, Asset):
|
||||
tp = ConnectionToken.Type.asset
|
||||
org_id = asset.org_id
|
||||
elif isinstance(application, Application):
|
||||
tp = ConnectionToken.Type.application
|
||||
org_id = application.org_id
|
||||
else:
|
||||
raise serializers.ValidationError(_('Asset or application required'))
|
||||
|
||||
return {
|
||||
'type': tp,
|
||||
'user': user,
|
||||
'secret': secret,
|
||||
'date_expired': date_expired,
|
||||
'user_display': pretty_string(str(user), max_length=128),
|
||||
'system_user_display': pretty_string(str(system_user), max_length=128),
|
||||
'asset_display': pretty_string(str(asset), max_length=128),
|
||||
'application_display': pretty_string(str(application), max_length=128),
|
||||
'org_id': org_id,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# SuperConnectionTokenSerializer
|
||||
#
|
||||
|
||||
|
||||
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
|
||||
|
||||
class Meta(ConnectionTokenSerializer.Meta):
|
||||
read_only_fields = [
|
||||
'validity',
|
||||
'user_display', 'system_user_display', 'asset_display', 'application_display',
|
||||
]
|
||||
|
||||
def get_user(self, attrs):
|
||||
return attrs.get('user') or self.request_user
|
||||
|
||||
|
||||
#
|
||||
# Connection Token Secret
|
||||
#
|
||||
|
||||
|
||||
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'name', 'username', 'email']
|
||||
|
||||
|
||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
protocols = ProtocolsField(label='Protocols', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key',
|
||||
'protocol', 'ad_domain', 'org_id'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key']
|
||||
|
||||
|
||||
class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
|
||||
program = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
working_directory = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
parameters = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
|
||||
|
||||
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
|
||||
attrs = serializers.JSONField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id']
|
||||
|
||||
|
||||
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
|
||||
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = ['id', 'name', 'gateways']
|
||||
|
||||
|
||||
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
'id', 'type', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action', 'date_created',
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||
user = ConnectionTokenUserSerializer(read_only=True)
|
||||
asset = ConnectionTokenAssetSerializer(read_only=True)
|
||||
application = ConnectionTokenApplicationSerializer(read_only=True)
|
||||
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
|
||||
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
|
||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
domain = ConnectionTokenDomainSerializer(read_only=True)
|
||||
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
||||
actions = ActionsField()
|
||||
|
||||
class Meta:
|
||||
model = ConnectionToken
|
||||
fields = [
|
||||
'id', 'secret', 'type', 'user', 'asset', 'application', 'system_user',
|
||||
'remote_app', 'cmd_filter_rules', 'domain', 'gateway', 'actions', 'expired_at',
|
||||
]
|
@@ -10,8 +10,8 @@ router = DefaultRouter()
|
||||
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
|
||||
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
|
||||
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
|
||||
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
Reference in New Issue
Block a user