jumpserver/apps/authentication/api/connection_token.py

330 lines
13 KiB
Python
Raw Normal View History

import abc
import os
import json
2021-09-24 07:31:25 +00:00
import base64
import urllib.parse
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import PermissionDenied
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from rest_framework.request import Request
from common.drf.api import JMSModelViewSet
from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin
2022-08-17 03:54:18 +00:00
from perms.models import Action
from terminal.models import EndpointRule
from ..serializers import (
2022-07-20 05:18:56 +00:00
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
)
from ..models import ConnectionToken
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
fix: fix rbac to dev (#7636) * feat: 添加 RBAC 应用模块 * feat: 添加 RBAC Model、API * feat: 添加 RBAC Model、API 2 * feat: 添加 RBAC Model、API 3 * feat: 添加 RBAC Model、API 4 * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC 整理权限位 * feat: RBAC 整理权限位2 * feat: RBAC 整理权限位2 * feat: RBAC 整理权限位 * feat: RBAC 添加默认角色 * feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定 * feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定 * feat: RBAC 修改用户模块API * feat: RBAC 添加组织模块迁移文件 & 修改组织模块API * feat: RBAC 添加组织模块迁移文件 & 修改组织模块API * feat: RBAC 修改用户角色属性的使用 * feat: RBAC No.1 * xxx * perf: 暂存 * perf: ... * perf(rbac): 添加 perms 到 profile serializer 中 * stash * perf: 使用init * perf: 修改migrations * perf: rbac * stash * stash * pref: 修改rbac * stash it * stash: 先去修复其他bug * perf: 修改 role 添加 users * pref: 修改 RBAC Model * feat: 添加权限的 tree api * stash: 暂存一下 * stash: 暂存一下 * perf: 修改 model verbose name * feat: 添加model各种 verbose name * perf: 生成 migrations * perf: 优化权限位 * perf: 添加迁移脚本 * feat: 添加组织角色迁移 * perf: 添加迁移脚本 * stash * perf: 添加migrateion * perf: 暂存一下 * perf: 修改rbac * perf: stash it * fix: 迁移冲突 * fix: 迁移冲突 * perf: 暂存一下 * perf: 修改 rbac 逻辑 * stash: 暂存一下 * perf: 修改内置角色 * perf: 解决 root 组织的问题 * perf: stash it * perf: 优化 rbac * perf: 优化 rolebinding 处理 * perf: 完成用户离开组织的问题 * perf: 暂存一下 * perf: 修改翻译 * perf: 去掉了 IsSuperUser * perf: IsAppUser 去掉完成 * perf: 修改 connection token 的权限 * perf: 去掉导入的问题 * perf: perms define 格式,修改 app 用户 的全新啊 * perf: 修改 permission * perf: 去掉一些 org admin * perf: 去掉部分 org admin * perf: 再去掉点 org admin role * perf: 再去掉部分 org admin * perf: user 角色搜索 * perf: 去掉很多 js * perf: 添加权限位 * perf: 修改权限 * perf: 去掉一个 todo * merge: with dev * fix: 修复冲突 Co-authored-by: Bai <bugatti_it@163.com> Co-authored-by: Michael Bai <baijiangjie@gmail.com> Co-authored-by: ibuler <ibuler@qq.com>
2022-02-17 12:13:31 +00:00
class ConnectionTokenMixin:
request: Request
@staticmethod
def check_token_valid(token: ConnectionToken):
is_valid, error = token.check_valid()
if not is_valid:
raise PermissionDenied(error)
@abc.abstractmethod
def get_request_resource_user(self, serializer):
raise NotImplementedError
def get_request_resources(self, serializer):
user = self.get_request_resource_user(serializer)
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
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)
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
target_ip = asset.get_target_ip()
elif application:
target_ip = application.get_target_ip()
else:
target_ip = ''
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
2021-02-23 06:37:42 +00:00
@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_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',
2021-06-08 04:45:36 +00:00
# 'desktopwidth:i': '1280',
# 'desktopheight:i': '800',
2021-07-16 06:15:10 +00:00
'use multimon:i': '0',
'session bpp:i': '32',
'audiomode:i': '0',
'disable wallpaper:i': '0',
'disable full window drag:i': '0',
'disable menu anims:i': '0',
'disable themes:i': '0',
'alternate shell:s': '',
'shell working directory:s': '',
'authentication level:i': '2',
'connect to console:i': '0',
'disable cursor setting:i': '0',
'allow font smoothing:i': '1',
'allow desktop composition:i': '1',
'redirectprinters:i': '0',
'prompt for credentials on client:i': '0',
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
2022-03-25 02:43:48 +00:00
'smart sizing:i': '1',
2022-04-24 09:00:48 +00:00
# 'drivestoredirect:s': '*',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
# 'remoteapplicationcmdline:s': '',
}
# 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
2022-02-16 07:48:28 +00:00
if drives_redirect:
actions = Action.choices_to_value(token.actions)
2022-02-16 07:48:28 +00:00
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')
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
# 设置其他选项
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 token.asset:
2022-08-11 07:45:03 +00:00
name = token.asset.name
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
2021-06-17 05:20:34 +00:00
else:
name = '*'
2022-08-10 10:17:59 +00:00
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
2021-09-24 07:31:25 +00:00
content = ''
for k, v in rdp_options.items():
2021-09-24 07:31:25 +00:00
content += f'{k}:{v}\n'
2021-07-27 05:35:25 +00:00
return filename, content
2022-08-10 10:17:59 +00:00
@staticmethod
def get_connect_filename(prefix_name):
prefix_name = prefix_name.replace('/', '_')
prefix_name = prefix_name.replace('\\', '_')
prefix_name = prefix_name.replace('.', '_')
filename = f'{prefix_name}-jumpserver'
filename = urllib.parse.quote(filename)
return filename
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
2022-08-11 07:45:03 +00:00
name = token.asset.name
elif token.application:
name = token.application.name
2022-03-28 11:52:48 +00:00
else:
name = '*'
2022-08-10 10:17:59 +00:00
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
2022-03-28 11:52:48 +00:00
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=token.asset, application=token.application
)
data = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': 'JMS-{}'.format(str(token.id)),
'password': token.secret
2022-03-28 11:52:48 +00:00
}
token = json.dumps(data)
return filename, token
2022-03-28 11:52:48 +00:00
2021-09-24 07:31:25 +00:00
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
2022-08-19 10:49:00 +00:00
'type', 'user_display', 'asset_display'
)
search_fields = filterset_fields
serializer_classes = {
'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'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',
}
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
2021-09-24 07:31:25 +00:00
def get_request_resource_user(self, serializer):
return self.request.user
def get_object(self):
if self.request.user.is_service_account:
# TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
else:
obj = super(ConnectionTokenViewSet, self).get_object()
return obj
2021-09-24 07:31:25 +00:00
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
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)
@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)
2022-04-24 09:00:48 +00:00
@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')
2022-04-24 09:00:48 +00:00
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
fix: fix rbac to dev (#7636) * feat: 添加 RBAC 应用模块 * feat: 添加 RBAC Model、API * feat: 添加 RBAC Model、API 2 * feat: 添加 RBAC Model、API 3 * feat: 添加 RBAC Model、API 4 * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC * feat: RBAC 整理权限位 * feat: RBAC 整理权限位2 * feat: RBAC 整理权限位2 * feat: RBAC 整理权限位 * feat: RBAC 添加默认角色 * feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定 * feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定 * feat: RBAC 修改用户模块API * feat: RBAC 添加组织模块迁移文件 & 修改组织模块API * feat: RBAC 添加组织模块迁移文件 & 修改组织模块API * feat: RBAC 修改用户角色属性的使用 * feat: RBAC No.1 * xxx * perf: 暂存 * perf: ... * perf(rbac): 添加 perms 到 profile serializer 中 * stash * perf: 使用init * perf: 修改migrations * perf: rbac * stash * stash * pref: 修改rbac * stash it * stash: 先去修复其他bug * perf: 修改 role 添加 users * pref: 修改 RBAC Model * feat: 添加权限的 tree api * stash: 暂存一下 * stash: 暂存一下 * perf: 修改 model verbose name * feat: 添加model各种 verbose name * perf: 生成 migrations * perf: 优化权限位 * perf: 添加迁移脚本 * feat: 添加组织角色迁移 * perf: 添加迁移脚本 * stash * perf: 添加migrateion * perf: 暂存一下 * perf: 修改rbac * perf: stash it * fix: 迁移冲突 * fix: 迁移冲突 * perf: 暂存一下 * perf: 修改 rbac 逻辑 * stash: 暂存一下 * perf: 修改内置角色 * perf: 解决 root 组织的问题 * perf: stash it * perf: 优化 rbac * perf: 优化 rolebinding 处理 * perf: 完成用户离开组织的问题 * perf: 暂存一下 * perf: 修改翻译 * perf: 去掉了 IsSuperUser * perf: IsAppUser 去掉完成 * perf: 修改 connection token 的权限 * perf: 去掉导入的问题 * perf: perms define 格式,修改 app 用户 的全新啊 * perf: 修改 permission * perf: 去掉一些 org admin * perf: 去掉部分 org admin * perf: 再去掉点 org admin role * perf: 再去掉部分 org admin * perf: user 角色搜索 * perf: 去掉很多 js * perf: 添加权限位 * perf: 修改权限 * perf: 去掉一个 todo * merge: with dev * fix: 修复冲突 Co-authored-by: Bai <bugatti_it@163.com> Co-authored-by: Michael Bai <baijiangjie@gmail.com> Co-authored-by: ibuler <ibuler@qq.com>
2022-02-17 12:13:31 +00:00
@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)
@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)
2022-03-30 03:08:50 +00:00
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
2022-04-24 09:00:48 +00:00
serializer_classes = {
'default': SuperConnectionTokenSerializer,
}
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken'
}
def get_request_resource_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['PATCH'], detail=False)
2022-04-24 09:00:48 +00:00
def renewal(self, request, *args, **kwargs):
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)