Merge branch 'dev' into pr@dev@refactor_system_user_account

pull/8023/head^2
ibuler 2022-07-17 14:17:18 +08:00
commit fd94713583
40 changed files with 1423 additions and 1084 deletions

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -0,0 +1,89 @@
# Generated by Django 3.2.12 on 2022-07-05 11:40
import authentication.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('applications', '0021_auto_20220629_1826'),
('assets', '0091_auto_20220629_1826'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0010_temptoken'),
]
operations = [
migrations.AddField(
model_name='connectiontoken',
name='application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='applications.application', verbose_name='Application'),
),
migrations.AddField(
model_name='connectiontoken',
name='application_display',
field=models.CharField(default='', max_length=128, verbose_name='Application display'),
),
migrations.AddField(
model_name='connectiontoken',
name='asset',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.asset', verbose_name='Asset'),
),
migrations.AddField(
model_name='connectiontoken',
name='asset_display',
field=models.CharField(default='', max_length=128, verbose_name='Asset display'),
),
migrations.AddField(
model_name='connectiontoken',
name='date_expired',
field=models.DateTimeField(default=authentication.models.date_expired_default, verbose_name='Date expired'),
),
migrations.AddField(
model_name='connectiontoken',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AddField(
model_name='connectiontoken',
name='secret',
field=models.CharField(default='', max_length=64, verbose_name='Secret'),
),
migrations.AddField(
model_name='connectiontoken',
name='system_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.systemuser', verbose_name='System user'),
),
migrations.AddField(
model_name='connectiontoken',
name='system_user_display',
field=models.CharField(default='', max_length=128, verbose_name='System user display'),
),
migrations.AddField(
model_name='connectiontoken',
name='type',
field=models.CharField(choices=[('asset', 'Asset'), ('application', 'Application')], default='asset', max_length=16, verbose_name='Type'),
),
migrations.AddField(
model_name='connectiontoken',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AddField(
model_name='connectiontoken',
name='user_display',
field=models.CharField(default='', max_length=128, verbose_name='User display'),
),
migrations.AlterField(
model_name='connectiontoken',
name='id',
field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
),
migrations.AlterModelOptions(
name='connectiontoken',
options={'ordering': ('-date_expired',), 'permissions': [('view_connectiontokensecret', 'Can view connection token secret')], 'verbose_name': 'Connection token'},
),
]

View File

@ -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):

View File

@ -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"))

View File

@ -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 *

View File

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

View File

@ -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',
]

View File

@ -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'),

View File

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

View File

@ -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):

View File

@ -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': {},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = '@'

View File

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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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 {

View File

@ -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:

View File

@ -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

View File

@ -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',

View File

@ -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'])

View File

@ -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')

View File

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

View File

@ -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:

View File

@ -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):

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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>

View File

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

View File

@ -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 = [