mirror of https://github.com/jumpserver/jumpserver
Merge branch 'dev' into pr@dev@refactor_system_user_account
commit
fd94713583
|
@ -9,7 +9,7 @@ import paramiko
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger, lazyproperty
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from .base import BaseUser
|
from .base import BaseUser
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class Domain(OrgModelMixin):
|
||||||
def has_gateway(self):
|
def has_gateway(self):
|
||||||
return self.gateway_set.filter(is_active=True).exists()
|
return self.gateway_set.filter(is_active=True).exists()
|
||||||
|
|
||||||
@property
|
@lazyproperty
|
||||||
def gateways(self):
|
def gateways(self):
|
||||||
return self.gateway_set.filter(is_active=True)
|
return self.gateway_set.filter(is_active=True)
|
||||||
|
|
||||||
|
@ -44,8 +44,9 @@ class Domain(OrgModelMixin):
|
||||||
gateways = [gw for gw in self.gateways if gw.is_connective]
|
gateways = [gw for gw in self.gateways if gw.is_connective]
|
||||||
if gateways:
|
if gateways:
|
||||||
return random.choice(gateways)
|
return random.choice(gateways)
|
||||||
else:
|
|
||||||
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
|
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
|
||||||
|
if self.gateways:
|
||||||
return random.choice(self.gateways)
|
return random.choice(self.gateways)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ class AuthBackendLabelMapping(LazyObject):
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
|
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
|
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
|
||||||
|
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
|
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
|
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
|
||||||
return backend_label_mapping
|
return backend_label_mapping
|
||||||
|
|
|
@ -7,11 +7,18 @@ from rest_framework.generics import RetrieveAPIView, CreateAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser, UserConfirmation
|
||||||
from ..const import ConfirmType
|
from ..const import ConfirmType
|
||||||
from ..serializers import ConfirmSerializer
|
from ..serializers import ConfirmSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
|
||||||
|
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
return Response('ok')
|
||||||
|
|
||||||
|
|
||||||
class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
serializer_class = ConfirmSerializer
|
serializer_class = ConfirmSerializer
|
||||||
|
|
|
@ -1,58 +1,61 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
import urllib.parse
|
|
||||||
import json
|
|
||||||
from typing import Callable
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import base64
|
import base64
|
||||||
import ctypes
|
import urllib.parse
|
||||||
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils import timezone
|
from django.shortcuts import get_object_or_404
|
||||||
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 rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework import serializers
|
from rest_framework.decorators import action
|
||||||
from django.conf import settings
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from applications.models import Application
|
from common.drf.api import JMSModelViewSet
|
||||||
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.http import is_true
|
from common.http import is_true
|
||||||
|
from orgs.mixins.api import RootOrgViewMixin
|
||||||
from perms.models.base import Action
|
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 terminal.models import EndpointRule
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
|
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer,
|
||||||
|
ConnectionTokenDisplaySerializer,
|
||||||
)
|
)
|
||||||
|
from ..models import ConnectionToken
|
||||||
logger = get_logger(__name__)
|
|
||||||
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
|
|
||||||
|
|
||||||
|
|
||||||
class ClientProtocolMixin:
|
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||||
"""
|
|
||||||
下载客户端支持的连接文件,里面包含了 token,和 其他连接信息
|
|
||||||
|
|
||||||
- [x] RDP
|
|
||||||
- [ ] KoKo
|
|
||||||
|
|
||||||
本质上,这里还是暴露出 token 来,进行使用
|
class ConnectionTokenMixin:
|
||||||
"""
|
|
||||||
request: Request
|
request: Request
|
||||||
get_serializer: Callable
|
|
||||||
create_token: Callable
|
@staticmethod
|
||||||
get_serializer_context: Callable
|
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):
|
def get_smart_endpoint(self, protocol, asset=None, application=None):
|
||||||
if asset:
|
if asset:
|
||||||
|
@ -64,21 +67,32 @@ class ClientProtocolMixin:
|
||||||
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
|
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
|
||||||
return endpoint
|
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
|
@staticmethod
|
||||||
def parse_env_bool(env_key, env_default, true_value, false_value):
|
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
|
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
|
||||||
|
|
||||||
def get_rdp_file_content(self, serializer):
|
def get_client_protocol_data(self, token: ConnectionToken):
|
||||||
options = {
|
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': '',
|
'full address:s': '',
|
||||||
'username:s': '',
|
'username:s': '',
|
||||||
# 'screen mode id:i': '1',
|
# 'screen mode id:i': '1',
|
||||||
|
@ -111,412 +125,164 @@ class ClientProtocolMixin:
|
||||||
# 'remoteapplicationcmdline:s': '',
|
# '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')
|
height = self.request.query_params.get('height')
|
||||||
width = self.request.query_params.get('width')
|
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:
|
if width and height:
|
||||||
options['desktopwidth:i'] = width
|
rdp_options['desktopwidth:i'] = width
|
||||||
options['desktopheight:i'] = height
|
rdp_options['desktopheight:i'] = height
|
||||||
options['winposstr:s:'] = f'0,1,0,0,{width},{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:
|
if token.asset:
|
||||||
name = asset.hostname
|
name = token.asset.hostname
|
||||||
elif application:
|
elif token.application and token.application.category_remote_app:
|
||||||
name = application.name
|
app = '||jmservisor'
|
||||||
application.get_rdp_remote_app_setting()
|
name = token.application.name
|
||||||
|
rdp_options['remoteapplicationmode:i'] = '1'
|
||||||
app = f'||jmservisor'
|
rdp_options['alternate shell:s'] = app
|
||||||
options['remoteapplicationmode:i'] = '1'
|
rdp_options['remoteapplicationprogram:s'] = app
|
||||||
options['alternate shell:s'] = app
|
rdp_options['remoteapplicationname:s'] = name
|
||||||
options['remoteapplicationprogram:s'] = app
|
|
||||||
options['remoteapplicationname:s'] = name
|
|
||||||
else:
|
else:
|
||||||
name = '*'
|
name = '*'
|
||||||
|
|
||||||
|
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||||
|
filename = urllib.parse.quote(filename)
|
||||||
|
|
||||||
content = ''
|
content = ''
|
||||||
for k, v in options.items():
|
for k, v in rdp_options.items():
|
||||||
content += f'{k}:{v}\n'
|
content += f'{k}:{v}\n'
|
||||||
return name, content
|
|
||||||
|
|
||||||
def get_ssh_token(self, serializer):
|
return filename, content
|
||||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
|
||||||
token, secret = self.create_token(user, asset, application, system_user)
|
def get_ssh_token(self, token: ConnectionToken):
|
||||||
if asset:
|
if token.asset:
|
||||||
name = asset.hostname
|
name = token.asset.hostname
|
||||||
elif application:
|
elif token.application:
|
||||||
name = application.name
|
name = token.application.name
|
||||||
else:
|
else:
|
||||||
name = '*'
|
name = '*'
|
||||||
|
filename = f'{token.user.username}-{name}-jumpserver'
|
||||||
|
|
||||||
endpoint = self.get_smart_endpoint(
|
endpoint = self.get_smart_endpoint(
|
||||||
protocol='ssh', asset=asset, application=application
|
protocol='ssh', asset=token.asset, application=token.application
|
||||||
)
|
)
|
||||||
content = {
|
data = {
|
||||||
'ip': endpoint.host,
|
'ip': endpoint.host,
|
||||||
'port': str(endpoint.ssh_port),
|
'port': str(endpoint.ssh_port),
|
||||||
'username': f'JMS-{token}',
|
'username': 'JMS-{}'.format(str(token.id)),
|
||||||
'password': secret
|
'password': token.secret
|
||||||
}
|
}
|
||||||
token = json.dumps(content)
|
token = json.dumps(data)
|
||||||
return name, token
|
return filename, 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')
|
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||||
lib = ctypes.CDLL(lib_path)
|
filterset_fields = (
|
||||||
lib.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_int]
|
'type',
|
||||||
lib.encrypt.restype = ctypes.c_char_p
|
'user_display', 'system_user_display', 'application_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',
|
||||||
|
}
|
||||||
|
queryset = ConnectionToken.objects.all()
|
||||||
|
|
||||||
rst = lib.encrypt(parameters, len(parameters))
|
def create_connection_token(self):
|
||||||
rst = rst.decode('ascii')
|
data = self.request.query_params if self.request.method == 'GET' else self.request.data
|
||||||
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 = self.get_serializer(data=data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
return serializer
|
self.perform_create(serializer)
|
||||||
|
token: ConnectionToken = serializer.instance
|
||||||
|
return token
|
||||||
|
|
||||||
def get_client_protocol_data(self, serializer):
|
def perform_create(self, serializer):
|
||||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
user, asset, application, system_user = self.get_request_resources(serializer)
|
||||||
protocol = system_user.protocol
|
self.check_user_has_resource_permission(user, asset, application, system_user)
|
||||||
username = user.username
|
return super(ConnectionTokenViewSet, self).perform_create(serializer)
|
||||||
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)
|
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||||
data = {
|
def get_secret_detail(self, request, *args, **kwargs):
|
||||||
"filename": filename,
|
# 非常重要的 api,在逻辑层再判断一下,双重保险
|
||||||
"protocol": system_user.protocol,
|
perm_required = 'authentication.view_connectiontokensecret'
|
||||||
"username": username,
|
if not request.user.has_perm(perm_required):
|
||||||
"token": token,
|
raise PermissionDenied('Not allow to view secret')
|
||||||
"config": config
|
token_id = request.data.get('token') or ''
|
||||||
}
|
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||||
return data
|
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)
|
||||||
|
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||||
def get_rdp_file(self, request, *args, **kwargs):
|
def get_rdp_file(self, request, *args, **kwargs):
|
||||||
if self.request.method == 'GET':
|
token = self.create_connection_token()
|
||||||
data = self.request.query_params
|
self.check_token_valid(token)
|
||||||
else:
|
filename, content = self.get_rdp_file_info(token)
|
||||||
data = self.request.data
|
filename = '{}.rdp'.format(filename)
|
||||||
serializer = self.get_serializer(data=data)
|
response = HttpResponse(content, content_type='application/octet-stream')
|
||||||
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
|
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||||
serializer = self.get_valid_serializer()
|
token = self.create_connection_token()
|
||||||
|
self.check_token_valid(token)
|
||||||
try:
|
try:
|
||||||
protocol_data = self.get_client_protocol_data(serializer)
|
protocol_data = self.get_client_protocol_data(token)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return Response({'error': str(e)}, status=401)
|
return Response(data={'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
protocol_data = json.dumps(protocol_data).encode()
|
protocol_data = json.dumps(protocol_data).encode()
|
||||||
protocol_data = base64.b64encode(protocol_data).decode()
|
protocol_data = base64.b64encode(protocol_data).decode()
|
||||||
data = {
|
data = {
|
||||||
'url': 'jms://{}'.format(protocol_data),
|
'url': 'jms://{}'.format(protocol_data)
|
||||||
}
|
}
|
||||||
return Response(data=data)
|
return Response(data=data)
|
||||||
|
|
||||||
|
@action(methods=['PATCH'], detail=True)
|
||||||
class SecretDetailMixin:
|
def expire(self, request, *args, **kwargs):
|
||||||
valid_token: Callable
|
instance = self.get_object()
|
||||||
request: Request
|
instance.expire()
|
||||||
get_serializer: Callable
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@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:
|
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||||
""" 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):
|
|
||||||
serializer_classes = {
|
|
||||||
'default': ConnectionTokenSerializer,
|
|
||||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
|
||||||
}
|
|
||||||
rbac_perms = {
|
|
||||||
'GET': 'authentication.view_connectiontoken',
|
|
||||||
'create': 'authentication.add_connectiontoken',
|
|
||||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
|
||||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
|
||||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
value = self.get_token_from_cache(token)
|
|
||||||
if not value:
|
|
||||||
raise serializers.ValidationError('Token not found')
|
|
||||||
|
|
||||||
user = get_object_or_404(User, id=value.get('user'))
|
|
||||||
if not user.is_valid:
|
|
||||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if not has_perm:
|
|
||||||
raise serializers.ValidationError('Permission expired or invalid')
|
|
||||||
return value, user, system_user, asset, app, expired_at, actions
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class UserSuperConnectionTokenViewSet(
|
|
||||||
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
|
|
||||||
):
|
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': SuperConnectionTokenSerializer,
|
'default': SuperConnectionTokenSerializer,
|
||||||
}
|
}
|
||||||
|
@ -525,10 +291,19 @@ class UserSuperConnectionTokenViewSet(
|
||||||
'renewal': 'authentication.add_superconnectiontoken'
|
'renewal': 'authentication.add_superconnectiontoken'
|
||||||
}
|
}
|
||||||
|
|
||||||
@action(methods=[PATCH], detail=False)
|
@action(methods=['PATCH'], detail=False)
|
||||||
def renewal(self, request, *args, **kwargs):
|
def renewal(self, request, *args, **kwargs):
|
||||||
""" 续期 Token """
|
from common.utils.timezone import as_current_tz
|
||||||
token = request.data.get('token', '')
|
|
||||||
data = self.renewal_token(token)
|
token_id = request.data.get('token') or ''
|
||||||
status_code = 200 if data.get('ok') else 404
|
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||||
return Response(data=data, status=status_code)
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -443,13 +443,15 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||||
LoginIpBlockUtil(ip).clean_block_if_need()
|
LoginIpBlockUtil(ip).clean_block_if_need()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def mark_password_ok(self, user, auto_login=False):
|
def mark_password_ok(self, user, auto_login=False, auth_backend=None):
|
||||||
request = self.request
|
request = self.request
|
||||||
request.session['auth_password'] = 1
|
request.session['auth_password'] = 1
|
||||||
request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||||
request.session['user_id'] = str(user.id)
|
request.session['user_id'] = str(user.id)
|
||||||
request.session['auto_login'] = auto_login
|
request.session['auto_login'] = auto_login
|
||||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
if not auth_backend:
|
||||||
|
auth_backend = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||||
|
request.session['auth_backend'] = auth_backend
|
||||||
|
|
||||||
def check_oauth2_auth(self, user: User, auth_backend):
|
def check_oauth2_auth(self, user: User, auth_backend):
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
|
@ -469,7 +471,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||||
LoginIpBlockUtil(ip).clean_block_if_need()
|
LoginIpBlockUtil(ip).clean_block_if_need()
|
||||||
MFABlockUtils(user.username, ip).clean_failed_count()
|
MFABlockUtils(user.username, ip).clean_failed_count()
|
||||||
|
|
||||||
self.mark_password_ok(user, False)
|
self.mark_password_ok(user, False, auth_backend)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_user_or_auth(self, valid_data):
|
def get_user_or_auth(self, valid_data):
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
|
||||||
from common.db import models
|
from common.db import models
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from common.utils.timezone import as_current_tz
|
||||||
|
|
||||||
|
|
||||||
class AccessKey(models.Model):
|
class AccessKey(models.Model):
|
||||||
|
@ -54,16 +57,197 @@ class SSOToken(models.JMSBaseModel):
|
||||||
verbose_name = _('SSO token')
|
verbose_name = _('SSO token')
|
||||||
|
|
||||||
|
|
||||||
class ConnectionToken(models.JMSBaseModel):
|
def date_expired_default():
|
||||||
# Todo: 未来可能放到这里,不记录到 redis 了,虽然方便,但是不易于审计
|
return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION)
|
||||||
# Todo: add connection token 可能要授权给 普通用户, 或者放开就行
|
|
||||||
|
|
||||||
|
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:
|
class Meta:
|
||||||
|
ordering = ('-date_expired',)
|
||||||
verbose_name = _('Connection token')
|
verbose_name = _('Connection token')
|
||||||
permissions = [
|
permissions = [
|
||||||
('view_connectiontokensecret', _('Can view connection token secret'))
|
('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()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expire_time(self):
|
||||||
|
interval = self.date_expired - timezone.now()
|
||||||
|
seconds = interval.total_seconds()
|
||||||
|
if seconds < 0:
|
||||||
|
seconds = 0
|
||||||
|
return int(seconds)
|
||||||
|
|
||||||
|
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):
|
class TempToken(models.JMSModel):
|
||||||
username = models.CharField(max_length=128, verbose_name=_("Username"))
|
username = models.CharField(max_length=128, verbose_name=_("Username"))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .token import *
|
from .token import *
|
||||||
from .connect_token import *
|
from .connection_token import *
|
||||||
from .password_mfa import *
|
from .password_mfa import *
|
||||||
from .confirm 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()
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
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', 'ConnectionTokenDisplaySerializer'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||||
|
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
|
||||||
|
is_valid = serializers.BooleanField(read_only=True, label=_('Validity'))
|
||||||
|
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
||||||
|
|
||||||
|
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', 'is_valid', 'expire_time',
|
||||||
|
'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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
|
||||||
|
class Meta(ConnectionTokenSerializer.Meta):
|
||||||
|
extra_kwargs = {
|
||||||
|
'secret': {'write_only': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
|
expired_at = serializers.IntegerField()
|
||||||
|
|
||||||
|
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('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||||
router.register('sso', api.SSOViewSet, 'sso')
|
router.register('sso', api.SSOViewSet, 'sso')
|
||||||
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||||
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
|
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
|
||||||
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
|
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -27,6 +27,7 @@ urlpatterns = [
|
||||||
|
|
||||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||||
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
|
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
|
||||||
|
path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'),
|
||||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||||
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
||||||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||||
|
|
|
@ -300,5 +300,4 @@ class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
|
||||||
msg = e.msg
|
msg = e.msg
|
||||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return self.redirect_to_guard_view()
|
return self.redirect_to_guard_view()
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils import timezone as dj_timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ class ModelJSONFieldEncoder(json.JSONEncoder):
|
||||||
if isinstance(obj, str_cls):
|
if isinstance(obj, str_cls):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
elif isinstance(obj, datetime):
|
elif isinstance(obj, datetime):
|
||||||
|
obj = dj_timezone.localtime(obj)
|
||||||
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
|
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
|
||||||
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
|
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
|
||||||
and isinstance(obj[0], models.Model):
|
and isinstance(obj[0], models.Model):
|
||||||
|
|
|
@ -383,6 +383,7 @@ class Config(dict):
|
||||||
'SESSION_COOKIE_SECURE': False,
|
'SESSION_COOKIE_SECURE': False,
|
||||||
'CSRF_COOKIE_SECURE': False,
|
'CSRF_COOKIE_SECURE': False,
|
||||||
'REFERER_CHECK_ENABLED': False,
|
'REFERER_CHECK_ENABLED': False,
|
||||||
|
'SESSION_ENGINE': 'cache',
|
||||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||||
'SERVER_REPLAY_STORAGE': {},
|
'SERVER_REPLAY_STORAGE': {},
|
||||||
|
|
|
@ -3,6 +3,7 @@ import platform
|
||||||
|
|
||||||
if platform.system() == 'Darwin' and platform.machine() == 'arm64':
|
if platform.system() == 'Darwin' and platform.machine() == 'arm64':
|
||||||
import pymysql
|
import pymysql
|
||||||
|
|
||||||
pymysql.version_info = (1, 4, 2, "final", 0)
|
pymysql.version_info = (1, 4, 2, "final", 0)
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
|
|
||||||
|
@ -109,8 +110,6 @@ MIDDLEWARE = [
|
||||||
'simple_history.middleware.HistoryRequestMiddleware',
|
'simple_history.middleware.HistoryRequestMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'jumpserver.urls'
|
ROOT_URLCONF = 'jumpserver.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
@ -161,7 +160,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
||||||
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
SESSION_ENGINE = "django.contrib.sessions.backends.{}".format(CONFIG.SESSION_ENGINE)
|
||||||
|
|
||||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
||||||
# Database
|
# Database
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ba75ec2955d452a16930dbd845c8091de4d3cbe0c5b5ccb18f8deb28d7bf3809
|
oid sha256:326eef1f3134c1500a6641c6616a9d509befd5db42ead551fe5ca01b3e0273c0
|
||||||
size 126865
|
size 128150
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bfbd815ff09d4b7c5f64af1eb745b81937a267d469ffbcba93c849feada2a8a8
|
oid sha256:1efe7f07b0877357a42a7f93e075c152e2bd8ee7adc20bcab17427a86cce5ed3
|
||||||
size 104695
|
size 105644
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -41,9 +41,21 @@ class OrgManager(models.Manager):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
org = get_current_org()
|
||||||
|
for obj in objs:
|
||||||
|
if org.is_root():
|
||||||
|
if not self.org_id:
|
||||||
|
raise ValidationError('Please save in a organization')
|
||||||
|
else:
|
||||||
|
obj.org_id = org.id
|
||||||
|
return super().bulk_create(objs, batch_size, ignore_conflicts)
|
||||||
|
|
||||||
|
|
||||||
class OrgModelMixin(models.Model):
|
class OrgModelMixin(models.Model):
|
||||||
org_id = models.CharField(max_length=36, blank=True, default='',
|
org_id = models.CharField(
|
||||||
verbose_name=_("Organization"), db_index=True)
|
max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True
|
||||||
|
)
|
||||||
objects = OrgManager()
|
objects = OrgManager()
|
||||||
|
|
||||||
sep = '@'
|
sep = '@'
|
||||||
|
|
|
@ -21,8 +21,12 @@ class ActionsField(serializers.MultipleChoiceField):
|
||||||
return Action.value_to_choices(value)
|
return Action.value_to_choices(value)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if data is None:
|
if not self.allow_empty and not data:
|
||||||
|
self.fail('empty')
|
||||||
|
|
||||||
|
if not data:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
return Action.choices_to_value(data)
|
return Action.choices_to_value(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -80,14 +80,3 @@ def get_application_system_user_ids(user, application):
|
||||||
def has_application_system_permission(user, application, system_user):
|
def has_application_system_permission(user, application, system_user):
|
||||||
system_user_ids = get_application_system_user_ids(user, application)
|
system_user_ids = get_application_system_user_ids(user, application)
|
||||||
return system_user.id in system_user_ids
|
return system_user.id in system_user_ids
|
||||||
|
|
||||||
|
|
||||||
def get_application_actions(user, application, system_user):
|
|
||||||
perm_ids = get_user_all_app_perm_ids(user)
|
|
||||||
actions = ApplicationPermission.objects.filter(
|
|
||||||
applications=application, system_users=system_user,
|
|
||||||
id__in=list(perm_ids)
|
|
||||||
).values_list('actions', flat=True)
|
|
||||||
|
|
||||||
actions = reduce(lambda x, y: x | y, actions, 0)
|
|
||||||
return actions
|
|
||||||
|
|
|
@ -109,9 +109,3 @@ def get_asset_system_user_ids_with_actions_by_group(group: UserGroup, asset: Ass
|
||||||
user_groups=group
|
user_groups=group
|
||||||
).valid().values_list('id', flat=True).distinct()
|
).valid().values_list('id', flat=True).distinct()
|
||||||
return get_asset_system_user_ids_with_actions(asset_perm_ids, asset)
|
return get_asset_system_user_ids_with_actions(asset_perm_ids, asset)
|
||||||
|
|
||||||
|
|
||||||
def get_asset_actions(user, asset, system_user):
|
|
||||||
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
|
||||||
actions = systemuser_actions_mapper.get(system_user.id, 0)
|
|
||||||
return actions
|
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const theme = "{{ INTERFACE.theme }}";
|
const themeInfo = {{ INTERFACE.theme_info | safe }};
|
||||||
if (theme && theme.colors && theme.colors['--color-primary']) {
|
if (themeInfo && themeInfo.colors && themeInfo.colors['--color-primary']) {
|
||||||
document.documentElement.style.setProperty('--primary-color', theme.colors['--color-primary']);
|
document.documentElement.style.setProperty('--primary-color', themeInfo.colors['--color-primary']);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -46,6 +46,15 @@ p {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<h2>JumpServer {% trans 'Offline video player' %} v0.1.5</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/download/JumpServer-Video-Player.dmg">jumpserver-video-player.dmg</a></li>
|
||||||
|
<li><a href="/download/JumpServer-Video-Player.exe">jumpserver-video-player.exe</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
ul {
|
ul {
|
||||||
|
|
|
@ -54,15 +54,15 @@ class SmartEndpointViewMixin:
|
||||||
asset_id = request.GET.get('asset_id')
|
asset_id = request.GET.get('asset_id')
|
||||||
app_id = request.GET.get('app_id')
|
app_id = request.GET.get('app_id')
|
||||||
session_id = request.GET.get('session_id')
|
session_id = request.GET.get('session_id')
|
||||||
token = request.GET.get('token')
|
token_id = request.GET.get('token')
|
||||||
|
if token_id:
|
||||||
|
from authentication.models import ConnectionToken
|
||||||
|
token = ConnectionToken.objects.filter(id=token_id).first()
|
||||||
if token:
|
if token:
|
||||||
from authentication.api.connection_token import TokenCacheMixin as TokenUtil
|
if token.asset:
|
||||||
value = TokenUtil().get_token_from_cache(token)
|
asset_id = token.asset.id
|
||||||
if value:
|
elif token.application:
|
||||||
if value.get('type') == 'asset':
|
app_id = token.application.id
|
||||||
asset_id = value.get('asset')
|
|
||||||
else:
|
|
||||||
app_id = value.get('application')
|
|
||||||
if asset_id:
|
if asset_id:
|
||||||
pk, model = asset_id, Asset
|
pk, model = asset_id, Asset
|
||||||
elif app_id:
|
elif app_id:
|
||||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.exceptions import MethodNotAllowed
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
|
||||||
from common.const.http import POST, PUT
|
from common.const.http import POST, PUT, PATCH
|
||||||
from common.mixins.api import CommonApiMixin
|
from common.mixins.api import CommonApiMixin
|
||||||
from orgs.utils import tmp_to_root_org
|
from orgs.utils import tmp_to_root_org
|
||||||
|
|
||||||
|
@ -71,32 +71,34 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
|
@action(detail=True, methods=[PUT, PATCH], permission_classes=[IsAssignee, ])
|
||||||
def approve(self, request, *args, **kwargs):
|
def approve(self, request, *args, **kwargs):
|
||||||
|
partial = kwargs.pop('partial', False)
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance)
|
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
instance = serializer.save()
|
||||||
instance.approve(processor=request.user)
|
instance.approve(processor=request.user)
|
||||||
return Response(serializer.data)
|
return Response('ok')
|
||||||
|
|
||||||
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
|
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
|
||||||
def reject(self, request, *args, **kwargs):
|
def reject(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance)
|
|
||||||
instance.reject(processor=request.user)
|
instance.reject(processor=request.user)
|
||||||
return Response(serializer.data)
|
return Response('ok')
|
||||||
|
|
||||||
@action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ])
|
@action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ])
|
||||||
def close(self, request, *args, **kwargs):
|
def close(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance)
|
|
||||||
instance.close()
|
instance.close()
|
||||||
return Response(serializer.data)
|
return Response('ok')
|
||||||
|
|
||||||
|
|
||||||
class ApplyAssetTicketViewSet(TicketViewSet):
|
class ApplyAssetTicketViewSet(TicketViewSet):
|
||||||
serializer_class = serializers.ApplyAssetDisplaySerializer
|
serializer_class = serializers.ApplyAssetDisplaySerializer
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'open': serializers.ApplyAssetSerializer
|
'open': serializers.ApplyAssetSerializer,
|
||||||
|
'approve': serializers.ApproveAssetSerializer
|
||||||
}
|
}
|
||||||
model = ApplyAssetTicket
|
model = ApplyAssetTicket
|
||||||
filterset_class = filters.ApplyAssetTicketFilter
|
filterset_class = filters.ApplyAssetTicketFilter
|
||||||
|
@ -105,7 +107,8 @@ class ApplyAssetTicketViewSet(TicketViewSet):
|
||||||
class ApplyApplicationTicketViewSet(TicketViewSet):
|
class ApplyApplicationTicketViewSet(TicketViewSet):
|
||||||
serializer_class = serializers.ApplyApplicationDisplaySerializer
|
serializer_class = serializers.ApplyApplicationDisplaySerializer
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'open': serializers.ApplyApplicationSerializer
|
'open': serializers.ApplyApplicationSerializer,
|
||||||
|
'approve': serializers.ApproveApplicationSerializer
|
||||||
}
|
}
|
||||||
model = ApplyApplicationTicket
|
model = ApplyApplicationTicket
|
||||||
filterset_class = filters.ApplyApplicationTicketFilter
|
filterset_class = filters.ApplyApplicationTicketFilter
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from tickets.utils import (
|
from tickets.utils import (
|
||||||
send_ticket_processed_mail_to_applicant,
|
send_ticket_processed_mail_to_applicant,
|
||||||
send_ticket_applied_mail_to_assignees
|
send_ticket_applied_mail_to_assignees
|
||||||
)
|
)
|
||||||
from tickets.const import TicketState, TicketStatus
|
from tickets.const import TicketState, TicketType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -59,6 +60,25 @@ class BaseHandler:
|
||||||
logger.debug('Send processed mail to applicant: {}'.format(applicant))
|
logger.debug('Send processed mail to applicant: {}'.format(applicant))
|
||||||
send_ticket_processed_mail_to_applicant(self.ticket, processor)
|
send_ticket_processed_mail_to_applicant(self.ticket, processor)
|
||||||
|
|
||||||
|
def _diff_prev_approve_context(self, state):
|
||||||
|
diff_context = {}
|
||||||
|
if state != TicketState.approved:
|
||||||
|
return diff_context
|
||||||
|
if self.ticket.type not in [TicketType.apply_asset, TicketType.apply_application]:
|
||||||
|
return diff_context
|
||||||
|
|
||||||
|
old_rel_snapshot = self.ticket.old_rel_snapshot
|
||||||
|
current_rel_snapshot = self.ticket.get_local_snapshot()
|
||||||
|
diff = set(current_rel_snapshot.items()) - set(old_rel_snapshot.items())
|
||||||
|
if not diff:
|
||||||
|
return diff_context
|
||||||
|
|
||||||
|
content = []
|
||||||
|
for k, v in sorted(list(diff), reverse=True):
|
||||||
|
content.append([k, old_rel_snapshot[k], v])
|
||||||
|
headers = [_('Change field'), _('Before change'), _('After change')]
|
||||||
|
return {'headers': headers, 'content': content}
|
||||||
|
|
||||||
def _create_state_change_comment(self, state):
|
def _create_state_change_comment(self, state):
|
||||||
# 打开或关闭工单,备注显示是自己,其他是受理人
|
# 打开或关闭工单,备注显示是自己,其他是受理人
|
||||||
if state in [TicketState.reopen, TicketState.pending, TicketState.closed]:
|
if state in [TicketState.reopen, TicketState.pending, TicketState.closed]:
|
||||||
|
@ -68,8 +88,11 @@ class BaseHandler:
|
||||||
|
|
||||||
user_display = str(user)
|
user_display = str(user)
|
||||||
state_display = getattr(TicketState, state).label
|
state_display = getattr(TicketState, state).label
|
||||||
|
approve_info = _('{} {} the ticket').format(user_display, state_display)
|
||||||
|
context = self._diff_prev_approve_context(state)
|
||||||
|
context.update({'approve_info': approve_info})
|
||||||
data = {
|
data = {
|
||||||
'body': _('{} {} the ticket').format(user_display, state_display),
|
'body': render_to_string('tickets/ticket_approve_diff.html', context),
|
||||||
'user': user,
|
'user': user,
|
||||||
'user_display': str(user),
|
'user_display': str(user),
|
||||||
'type': 'state',
|
'type': 'state',
|
||||||
|
|
|
@ -102,7 +102,7 @@ def apply_asset_migrate(apps, *args):
|
||||||
'applicant': instance.applicant_display,
|
'applicant': instance.applicant_display,
|
||||||
'apply_nodes': meta.get('apply_nodes_display', []),
|
'apply_nodes': meta.get('apply_nodes_display', []),
|
||||||
'apply_assets': meta.get('apply_assets_display', []),
|
'apply_assets': meta.get('apply_assets_display', []),
|
||||||
'apply_system_users': meta.get('apply_system_users', []),
|
'apply_system_users': meta.get('apply_system_users_display', []),
|
||||||
}
|
}
|
||||||
instance.rel_snapshot = rel_snapshot
|
instance.rel_snapshot = rel_snapshot
|
||||||
instance.save(update_fields=['rel_snapshot'])
|
instance.save(update_fields=['rel_snapshot'])
|
||||||
|
@ -140,7 +140,7 @@ def apply_application_migrate(apps, *args):
|
||||||
rel_snapshot = {
|
rel_snapshot = {
|
||||||
'applicant': instance.applicant_display,
|
'applicant': instance.applicant_display,
|
||||||
'apply_applications': meta.get('apply_applications_display', []),
|
'apply_applications': meta.get('apply_applications_display', []),
|
||||||
'apply_system_users': meta.get('apply_system_users', []),
|
'apply_system_users': meta.get('apply_system_users_display', []),
|
||||||
}
|
}
|
||||||
instance.rel_snapshot = rel_snapshot
|
instance.rel_snapshot = rel_snapshot
|
||||||
instance.save(update_fields=['rel_snapshot'])
|
instance.save(update_fields=['rel_snapshot'])
|
||||||
|
|
|
@ -8,7 +8,7 @@ __all__ = ['ApplyApplicationTicket']
|
||||||
|
|
||||||
|
|
||||||
class ApplyApplicationTicket(Ticket):
|
class ApplyApplicationTicket(Ticket):
|
||||||
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name'))
|
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name'))
|
||||||
# 申请信息
|
# 申请信息
|
||||||
apply_category = models.CharField(
|
apply_category = models.CharField(
|
||||||
max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
|
max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
|
||||||
|
|
|
@ -10,7 +10,7 @@ asset_or_node_help_text = _("Select at least one asset or node")
|
||||||
|
|
||||||
|
|
||||||
class ApplyAssetTicket(Ticket):
|
class ApplyAssetTicket(Ticket):
|
||||||
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name'))
|
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name'))
|
||||||
apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes'))
|
apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes'))
|
||||||
# 申请信息
|
# 申请信息
|
||||||
apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
|
apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
|
||||||
|
@ -26,3 +26,6 @@ class ApplyAssetTicket(Ticket):
|
||||||
@property
|
@property
|
||||||
def apply_actions_display(self):
|
def apply_actions_display(self):
|
||||||
return Action.value_to_choices_display(self.apply_actions)
|
return Action.value_to_choices_display(self.apply_actions)
|
||||||
|
|
||||||
|
def get_apply_actions_display(self):
|
||||||
|
return ', '.join(self.apply_actions_display)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
import json
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -7,6 +8,7 @@ from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.db.models.fields import related
|
from django.db.models.fields import related
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
|
||||||
from common.exceptions import JMSException
|
from common.exceptions import JMSException
|
||||||
from common.utils.timezone import as_current_tz
|
from common.utils.timezone import as_current_tz
|
||||||
|
@ -97,6 +99,7 @@ class StatusMixin:
|
||||||
|
|
||||||
state: str
|
state: str
|
||||||
status: str
|
status: str
|
||||||
|
applicant_id: str
|
||||||
applicant: models.ForeignKey
|
applicant: models.ForeignKey
|
||||||
current_step: TicketStep
|
current_step: TicketStep
|
||||||
save: Callable
|
save: Callable
|
||||||
|
@ -130,6 +133,7 @@ class StatusMixin:
|
||||||
self._open()
|
self._open()
|
||||||
|
|
||||||
def approve(self, processor):
|
def approve(self, processor):
|
||||||
|
self.set_rel_snapshot()
|
||||||
self._change_state(StepState.approved, processor)
|
self._change_state(StepState.approved, processor)
|
||||||
|
|
||||||
def reject(self, processor):
|
def reject(self, processor):
|
||||||
|
@ -178,28 +182,34 @@ class StatusMixin:
|
||||||
@property
|
@property
|
||||||
def process_map(self):
|
def process_map(self):
|
||||||
process_map = []
|
process_map = []
|
||||||
steps = self.ticket_steps.all()
|
for step in self.ticket_steps.all():
|
||||||
for step in steps:
|
processor_id = ''
|
||||||
assignee_ids = []
|
assignee_ids = []
|
||||||
|
processor_display = ''
|
||||||
assignees_display = []
|
assignees_display = []
|
||||||
ticket_assignees = step.ticket_assignees.all()
|
|
||||||
processor = None
|
|
||||||
state = step.state
|
state = step.state
|
||||||
for i in ticket_assignees:
|
for i in step.ticket_assignees.all().prefetch_related('assignee'):
|
||||||
assignee_ids.append(i.assignee_id)
|
assignee_id = i.assignee_id
|
||||||
assignees_display.append(str(i.assignee))
|
assignee_display = str(i.assignee)
|
||||||
|
|
||||||
if state != StepState.pending and state == i.state:
|
if state != StepState.pending and state == i.state:
|
||||||
processor = i.assignee
|
processor_id = assignee_id
|
||||||
|
processor_display = assignee_display
|
||||||
if state == StepState.closed:
|
if state == StepState.closed:
|
||||||
processor = self.applicant
|
processor_id = self.applicant_id
|
||||||
|
processor_display = str(self.applicant)
|
||||||
|
|
||||||
|
assignee_ids.append(assignee_id)
|
||||||
|
assignees_display.append(assignee_display)
|
||||||
|
|
||||||
step_info = {
|
step_info = {
|
||||||
'state': state,
|
'state': state,
|
||||||
'approval_level': step.level,
|
'approval_level': step.level,
|
||||||
'assignees': assignee_ids,
|
'assignees': assignee_ids,
|
||||||
'assignees_display': assignees_display,
|
'assignees_display': assignees_display,
|
||||||
'approval_date': str(step.date_updated),
|
'approval_date': str(step.date_updated),
|
||||||
'processor': processor.id if processor else '',
|
'processor': processor_id,
|
||||||
'processor_display': str(processor) if processor else ''
|
'processor_display': processor_display
|
||||||
}
|
}
|
||||||
process_map.append(step_info)
|
process_map.append(step_info)
|
||||||
return process_map
|
return process_map
|
||||||
|
@ -380,6 +390,30 @@ class Ticket(StatusMixin, CommonModelMixin):
|
||||||
raise JMSException(detail=_('Please try again'), code='please_try_again')
|
raise JMSException(detail=_('Please try again'), code='please_try_again')
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def get_field_display(self, name, field, data: dict):
|
||||||
|
value = data.get(name)
|
||||||
|
if hasattr(self, f'get_{name}_display'):
|
||||||
|
value = getattr(self, f'get_{name}_display')()
|
||||||
|
elif isinstance(field, related.ForeignKey):
|
||||||
|
value = self.rel_snapshot[name]
|
||||||
|
elif isinstance(field, related.ManyToManyField):
|
||||||
|
value = ', '.join(self.rel_snapshot[name])
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_local_snapshot(self):
|
||||||
|
fields = self._meta._forward_fields_map
|
||||||
|
json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder)
|
||||||
|
data = json.loads(json_data)
|
||||||
|
snapshot = {}
|
||||||
|
local_fields = self._meta.local_fields + self._meta.local_many_to_many
|
||||||
|
excludes = ['ticket_ptr']
|
||||||
|
item_names = [field.name for field in local_fields if field.name not in excludes]
|
||||||
|
for name in item_names:
|
||||||
|
field = fields[name]
|
||||||
|
value = self.get_field_display(name, field, data)
|
||||||
|
snapshot[field.verbose_name] = value
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
class SuperTicket(Ticket):
|
class SuperTicket(Ticket):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -4,7 +4,6 @@ import json
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.db.models.fields import related
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.forms import model_to_dict
|
from django.forms import model_to_dict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -75,13 +74,7 @@ class BaseTicketMessage(UserMessage):
|
||||||
for name in item_names:
|
for name in item_names:
|
||||||
field = fields[name]
|
field = fields[name]
|
||||||
item = {'name': name, 'title': field.verbose_name}
|
item = {'name': name, 'title': field.verbose_name}
|
||||||
value = data.get(name)
|
value = self.ticket.get_field_display(name, field, data)
|
||||||
if hasattr(self.ticket, f'get_{name}_display'):
|
|
||||||
value = getattr(self.ticket, f'get_{name}_display')()
|
|
||||||
elif isinstance(field, related.ForeignKey):
|
|
||||||
value = self.ticket.rel_snapshot[name]
|
|
||||||
elif isinstance(field, related.ManyToManyField):
|
|
||||||
value = ', '.join(self.ticket.rel_snapshot[name])
|
|
||||||
item['value'] = value
|
item['value'] = value
|
||||||
items.append(item)
|
items.append(item)
|
||||||
return items
|
return items
|
||||||
|
@ -113,7 +106,7 @@ class TicketAppliedToAssigneeMessage(BaseTicketMessage):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_title(self):
|
def content_title(self):
|
||||||
return _('Your has a new ticket')
|
return _('Your has a new ticket, applicant - {}').format(self.ticket.applicant)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subject(self):
|
def subject(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from perms.models import ApplicationPermission
|
from perms.models import ApplicationPermission
|
||||||
|
@ -7,7 +8,7 @@ from tickets.models import ApplyApplicationTicket
|
||||||
from .ticket import TicketApplySerializer
|
from .ticket import TicketApplySerializer
|
||||||
from .common import BaseApplyAssetApplicationSerializer
|
from .common import BaseApplyAssetApplicationSerializer
|
||||||
|
|
||||||
__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer']
|
__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'ApproveApplicationSerializer']
|
||||||
|
|
||||||
|
|
||||||
class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
|
class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
|
||||||
|
@ -24,15 +25,23 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl
|
||||||
read_only_fields = list(set(fields) - set(writeable_fields))
|
read_only_fields = list(set(fields) - set(writeable_fields))
|
||||||
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
|
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'apply_system_users': {'required': True},
|
'apply_applications': {'required': False, 'allow_empty': True},
|
||||||
|
'apply_system_users': {'required': False, 'allow_empty': True},
|
||||||
}
|
}
|
||||||
extra_kwargs.update(ticket_extra_kwargs)
|
extra_kwargs.update(ticket_extra_kwargs)
|
||||||
|
|
||||||
def validate_apply_applications(self, applications):
|
def validate_apply_applications(self, applications):
|
||||||
|
if self.is_final_approval and not applications:
|
||||||
|
raise serializers.ValidationError(_('This field is required.'))
|
||||||
tp = self.initial_data.get('apply_type')
|
tp = self.initial_data.get('apply_type')
|
||||||
return self.filter_many_to_many_field(Application, applications, type=tp)
|
return self.filter_many_to_many_field(Application, applications, type=tp)
|
||||||
|
|
||||||
|
|
||||||
|
class ApproveApplicationSerializer(ApplyApplicationSerializer):
|
||||||
|
class Meta(ApplyApplicationSerializer.Meta):
|
||||||
|
read_only_fields = ApplyApplicationSerializer.Meta.read_only_fields + ['title', 'type']
|
||||||
|
|
||||||
|
|
||||||
class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
|
class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
|
||||||
apply_applications = serializers.SerializerMethodField()
|
apply_applications = serializers.SerializerMethodField()
|
||||||
apply_system_users = serializers.SerializerMethodField()
|
apply_system_users = serializers.SerializerMethodField()
|
||||||
|
|
|
@ -10,13 +10,13 @@ from tickets.models import ApplyAssetTicket
|
||||||
from .ticket import TicketApplySerializer
|
from .ticket import TicketApplySerializer
|
||||||
from .common import BaseApplyAssetApplicationSerializer
|
from .common import BaseApplyAssetApplicationSerializer
|
||||||
|
|
||||||
__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer']
|
__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer', 'ApproveAssetSerializer']
|
||||||
|
|
||||||
asset_or_node_help_text = _("Select at least one asset or node")
|
asset_or_node_help_text = _("Select at least one asset or node")
|
||||||
|
|
||||||
|
|
||||||
class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
|
class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
|
||||||
apply_actions = ActionsField(required=True, allow_null=True)
|
apply_actions = ActionsField(required=True, allow_empty=False)
|
||||||
permission_model = AssetPermission
|
permission_model = AssetPermission
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -30,9 +30,9 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
|
||||||
read_only_fields = list(set(fields) - set(writeable_fields))
|
read_only_fields = list(set(fields) - set(writeable_fields))
|
||||||
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
|
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'apply_nodes': {'required': False, 'help_text': asset_or_node_help_text},
|
'apply_nodes': {'required': False, 'allow_empty': True},
|
||||||
'apply_assets': {'required': False, 'help_text': asset_or_node_help_text},
|
'apply_assets': {'required': False, 'allow_empty': True},
|
||||||
'apply_system_users': {'required': True},
|
'apply_system_users': {'required': False, 'allow_empty': True},
|
||||||
}
|
}
|
||||||
extra_kwargs.update(ticket_extra_kwargs)
|
extra_kwargs.update(ticket_extra_kwargs)
|
||||||
|
|
||||||
|
@ -44,14 +44,22 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = super().validate(attrs)
|
attrs = super().validate(attrs)
|
||||||
if not attrs.get('apply_nodes') and not attrs.get('apply_assets'):
|
if self.is_final_approval and (
|
||||||
|
not attrs.get('apply_nodes') and not attrs.get('apply_assets')
|
||||||
|
):
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'apply_nodes': asset_or_node_help_text,
|
'apply_nodes': asset_or_node_help_text,
|
||||||
'apply_assets': asset_or_node_help_text,
|
'apply_assets': asset_or_node_help_text,
|
||||||
})
|
})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class ApproveAssetSerializer(ApplyAssetSerializer):
|
||||||
|
class Meta(ApplyAssetSerializer.Meta):
|
||||||
|
read_only_fields = ApplyAssetSerializer.Meta.read_only_fields + ['title', 'type']
|
||||||
|
|
||||||
|
|
||||||
class ApplyAssetDisplaySerializer(ApplyAssetSerializer):
|
class ApplyAssetDisplaySerializer(ApplyAssetSerializer):
|
||||||
apply_nodes = serializers.SerializerMethodField()
|
apply_nodes = serializers.SerializerMethodField()
|
||||||
apply_assets = serializers.SerializerMethodField()
|
apply_assets = serializers.SerializerMethodField()
|
||||||
|
|
|
@ -38,14 +38,25 @@ class DefaultPermissionName(object):
|
||||||
class BaseApplyAssetApplicationSerializer(serializers.Serializer):
|
class BaseApplyAssetApplicationSerializer(serializers.Serializer):
|
||||||
permission_model: Model
|
permission_model: Model
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_final_approval(self):
|
||||||
|
instance = self.instance
|
||||||
|
if not instance:
|
||||||
|
return False
|
||||||
|
if instance.approval_step == instance.ticket_steps.count():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def filter_many_to_many_field(self, model, values: list, **kwargs):
|
def filter_many_to_many_field(self, model, values: list, **kwargs):
|
||||||
org_id = self.initial_data.get('org_id')
|
org_id = self.instance.org_id if self.instance else self.initial_data.get('org_id')
|
||||||
ids = [instance.id for instance in values]
|
ids = [instance.id for instance in values]
|
||||||
with tmp_to_org(org_id):
|
with tmp_to_org(org_id):
|
||||||
qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True)
|
qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True)
|
||||||
return list(qs)
|
return list(qs)
|
||||||
|
|
||||||
def validate_apply_system_users(self, system_users):
|
def validate_apply_system_users(self, system_users):
|
||||||
|
if self.is_final_approval and not system_users:
|
||||||
|
raise serializers.ValidationError(_('This field is required.'))
|
||||||
return self.filter_many_to_many_field(SystemUser, system_users)
|
return self.filter_many_to_many_field(SystemUser, system_users)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
@ -72,3 +83,10 @@ class BaseApplyAssetApplicationSerializer(serializers.Serializer):
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
|
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
|
||||||
|
|
||||||
|
@atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
old_rel_snapshot = instance.get_local_snapshot()
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
instance.old_rel_snapshot = old_rel_snapshot
|
||||||
|
return instance
|
||||||
|
|
|
@ -80,6 +80,9 @@ class TicketApplySerializer(TicketSerializer):
|
||||||
return org_id
|
return org_id
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
if self.instance:
|
||||||
|
return attrs
|
||||||
|
|
||||||
ticket_type = attrs.get('type')
|
ticket_type = attrs.get('type')
|
||||||
org_id = attrs.get('org_id')
|
org_id = attrs.get('org_id')
|
||||||
flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first()
|
flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first()
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<p> {{ approve_info }}</p>
|
||||||
|
<br>
|
||||||
|
<div style="width:100%; overflow-x:scroll;">
|
||||||
|
<table style="width:1000px; text-align:left">
|
||||||
|
<tr>
|
||||||
|
{% for item in headers %}
|
||||||
|
<th> {{ item }} </th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% for item in content %}
|
||||||
|
<tr>
|
||||||
|
{% for child in item %}
|
||||||
|
<td>{{ child }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -6,7 +6,7 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from common.mixins import CommonBulkSerializerMixin
|
from common.mixins import CommonBulkSerializerMixin
|
||||||
from common.validators import PhoneValidator
|
from common.validators import PhoneValidator
|
||||||
from common.utils import pretty_string
|
from common.utils import pretty_string, get_logger
|
||||||
from common.drf.fields import EncryptedField
|
from common.drf.fields import EncryptedField
|
||||||
from rbac.builtin import BuiltinRole
|
from rbac.builtin import BuiltinRole
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
@ -19,6 +19,8 @@ __all__ = [
|
||||||
'InviteSerializer', 'ServiceAccountSerializer',
|
'InviteSerializer', 'ServiceAccountSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class RolesSerializerMixin(serializers.Serializer):
|
class RolesSerializerMixin(serializers.Serializer):
|
||||||
system_roles = serializers.ManyRelatedField(
|
system_roles = serializers.ManyRelatedField(
|
||||||
|
@ -199,8 +201,10 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer
|
||||||
if not disallow_fields:
|
if not disallow_fields:
|
||||||
return attrs
|
return attrs
|
||||||
# 用户自己不能更新自己的一些字段
|
# 用户自己不能更新自己的一些字段
|
||||||
error = _('User cannot self-update fields: {}').format(disallow_fields)
|
logger.debug('Disallow update self fields: %s', disallow_fields)
|
||||||
raise serializers.ValidationError(error)
|
for field in disallow_fields:
|
||||||
|
attrs.pop(field, None)
|
||||||
|
return attrs
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = self.check_disallow_self_update_fields(attrs)
|
attrs = self.check_disallow_self_update_fields(attrs)
|
||||||
|
|
|
@ -16,7 +16,7 @@ router.register(r'users', api.UserViewSet, 'user')
|
||||||
router.register(r'groups', api.UserGroupViewSet, 'user-group')
|
router.register(r'groups', api.UserGroupViewSet, 'user-group')
|
||||||
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
|
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
|
||||||
router.register(r'service-account-registrations', api.ServiceAccountRegistrationViewSet, 'service-account-registration')
|
router.register(r'service-account-registrations', api.ServiceAccountRegistrationViewSet, 'service-account-registration')
|
||||||
router.register(r'connection-token', auth_api.UserConnectionTokenViewSet, 'connection-token')
|
router.register(r'connection-token', auth_api.ConnectionTokenViewSet, 'connection-token')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
Loading…
Reference in New Issue