mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-01-29 21:51:31 +00:00
@@ -19,12 +19,12 @@ from common.utils import random_string
|
||||
from common.utils.django import get_request_os
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from perms.models import ActionChoices
|
||||
from terminal.const import NativeClient, TerminalType
|
||||
from terminal.models import EndpointRule, Applet
|
||||
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
||||
from terminal.models import EndpointRule
|
||||
from ..models import ConnectionToken
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
SuperConnectionTokenSerializer,
|
||||
SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer
|
||||
)
|
||||
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
@@ -34,30 +34,6 @@ class RDPFileClientProtocolURLMixin:
|
||||
request: Request
|
||||
get_serializer: callable
|
||||
|
||||
@staticmethod
|
||||
def set_applet_info(token, rdp_options):
|
||||
# remote-app
|
||||
applet = Applet.objects.filter(name=token.connect_method).first()
|
||||
if not applet:
|
||||
return rdp_options
|
||||
|
||||
cmdline = {
|
||||
'app_name': applet.name,
|
||||
'user_id': str(token.user.id),
|
||||
'asset_id': str(token.asset.id),
|
||||
'token_id': str(token.id)
|
||||
}
|
||||
|
||||
app = '||tinker'
|
||||
rdp_options['remoteapplicationmode:i'] = '1'
|
||||
rdp_options['alternate shell:s'] = app
|
||||
rdp_options['remoteapplicationprogram:s'] = app
|
||||
rdp_options['remoteapplicationname:s'] = app
|
||||
|
||||
cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode()
|
||||
rdp_options['remoteapplicationcmdline:s'] = cmdline_b64
|
||||
return rdp_options
|
||||
|
||||
def get_rdp_file_info(self, token: ConnectionToken):
|
||||
rdp_options = {
|
||||
'full address:s': '',
|
||||
@@ -114,8 +90,10 @@ class RDPFileClientProtocolURLMixin:
|
||||
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')
|
||||
|
||||
# 设置远程应用
|
||||
self.set_applet_info(token, rdp_options)
|
||||
# 设置远程应用, 不是 Mstsc
|
||||
if token.connect_method != NativeClient.mstsc:
|
||||
remote_app_options = token.get_remote_app_option()
|
||||
rdp_options.update(remote_app_options)
|
||||
|
||||
# 文件名
|
||||
name = token.asset.name
|
||||
@@ -145,7 +123,7 @@ class RDPFileClientProtocolURLMixin:
|
||||
_os = get_request_os(self.request)
|
||||
|
||||
connect_method_name = token.connect_method
|
||||
connect_method_dict = TerminalType.get_connect_method(
|
||||
connect_method_dict = ConnectMethodUtil.get_connect_method(
|
||||
token.connect_method, token.protocol, _os
|
||||
)
|
||||
if connect_method_dict is None:
|
||||
@@ -159,15 +137,17 @@ class RDPFileClientProtocolURLMixin:
|
||||
'file': {}
|
||||
}
|
||||
|
||||
if connect_method_name == NativeClient.mstsc:
|
||||
if connect_method_name == NativeClient.mstsc or connect_method_dict['type'] == 'applet':
|
||||
filename, content = self.get_rdp_file_info(token)
|
||||
data.update({
|
||||
'protocol': 'rdp',
|
||||
'file': {
|
||||
'name': filename,
|
||||
'content': content,
|
||||
}
|
||||
})
|
||||
else:
|
||||
print("Connect method: {}".format(connect_method_dict))
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol=connect_method_dict['endpoint_protocol'],
|
||||
asset=token.asset
|
||||
@@ -227,38 +207,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||
search_fields = filterset_fields
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'list': 'authentication.view_connectiontoken',
|
||||
'retrieve': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'expire': 'authentication.add_connectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='secret')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
|
||||
rbac_perm = 'authentication.view_connectiontokensecret'
|
||||
if not request.user.has_perm(rbac_perm):
|
||||
raise PermissionDenied('Not allow to view secret')
|
||||
|
||||
token_id = request.data.get('id') or ''
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
if token.is_expired:
|
||||
raise ValidationError({'id': 'Token is expired'})
|
||||
|
||||
token.is_valid()
|
||||
serializer = self.get_serializer(instance=token)
|
||||
expire_now = request.data.get('expire_now', True)
|
||||
if expire_now:
|
||||
token.expire()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ConnectionToken.objects \
|
||||
.filter(user=self.request.user) \
|
||||
@@ -287,7 +245,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||
permed_account = util.validate_permission(user, asset, account_name)
|
||||
|
||||
if not permed_account or not permed_account.actions:
|
||||
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
|
||||
msg = 'user `{}` not has asset `{}` permission for account `{}`'.format(
|
||||
user, asset, account_name
|
||||
)
|
||||
raise PermissionDenied(msg)
|
||||
@@ -305,10 +263,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
serializer_classes = {
|
||||
'default': SuperConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'create': 'authentication.add_superconnectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_applet_info': 'authentication.view_superconnectiontoken',
|
||||
'release_applet_account': 'authentication.view_superconnectiontoken',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -332,3 +294,38 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'msg': f'Token is renewed, date expired: {date_expired}'
|
||||
}
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='secret')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
|
||||
rbac_perm = 'authentication.view_connectiontokensecret'
|
||||
if not request.user.has_perm(rbac_perm):
|
||||
raise PermissionDenied('Not allow to view secret')
|
||||
|
||||
token_id = request.data.get('id') or ''
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
if token.is_expired:
|
||||
raise ValidationError({'id': 'Token is expired'})
|
||||
|
||||
token.is_valid()
|
||||
serializer = self.get_serializer(instance=token)
|
||||
expire_now = request.data.get('expire_now', True)
|
||||
if expire_now:
|
||||
token.expire()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='applet-option')
|
||||
def get_applet_info(self, *args, **kwargs):
|
||||
token_id = self.request.data.get('id')
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
if token.is_expired:
|
||||
return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
data = token.get_applet_option()
|
||||
serializer = ConnectTokenAppletOptionSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release')
|
||||
def release_applet_account(self, *args, **kwargs):
|
||||
account_id = self.request.data.get('id')
|
||||
msg = ConnectionToken.release_applet_account(account_id)
|
||||
return Response({'msg': msg})
|
||||
|
||||
@@ -88,7 +88,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
"""
|
||||
|
||||
@ssl_verification
|
||||
def authenticate(self, request, nonce=None, **kwargs):
|
||||
def authenticate(self, request, nonce=None, code_verifier=None, **kwargs):
|
||||
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
|
||||
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
@@ -134,6 +134,8 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
if settings.AUTH_OPENID_PKCE and code_verifier:
|
||||
token_payload['code_verifier'] = code_verifier
|
||||
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
|
||||
token_payload.update({
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
import secrets
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
@@ -38,6 +41,19 @@ class OIDCAuthRequestView(View):
|
||||
|
||||
http_method_names = ['get', ]
|
||||
|
||||
@staticmethod
|
||||
def gen_code_verifier(length=128):
|
||||
# length range 43 ~ 128
|
||||
return secrets.token_urlsafe(length - 32)
|
||||
|
||||
@staticmethod
|
||||
def gen_code_challenge(code_verifier, code_challenge_method):
|
||||
if code_challenge_method == 'plain':
|
||||
return code_verifier
|
||||
h = hashlib.sha256(code_verifier.encode('ascii')).digest()
|
||||
b = base64.urlsafe_b64encode(h)
|
||||
return b.decode('ascii')[:-1]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
|
||||
@@ -56,6 +72,16 @@ class OIDCAuthRequestView(View):
|
||||
)
|
||||
})
|
||||
|
||||
if settings.AUTH_OPENID_PKCE:
|
||||
code_verifier = self.gen_code_verifier()
|
||||
code_challenge_method = settings.AUTH_OPENID_CODE_CHALLENGE_METHOD or 'S256'
|
||||
code_challenge = self.gen_code_challenge(code_verifier, code_challenge_method)
|
||||
authentication_request_params.update({
|
||||
'code_challenge_method': code_challenge_method,
|
||||
'code_challenge': code_challenge
|
||||
})
|
||||
request.session['oidc_auth_code_verifier'] = code_verifier
|
||||
|
||||
# States should be used! They are recommended in order to maintain state between the
|
||||
# authentication request and the callback.
|
||||
if settings.AUTH_OPENID_USE_STATE:
|
||||
@@ -138,8 +164,9 @@ class OIDCAuthCallbackView(View):
|
||||
|
||||
# Authenticates the end-user.
|
||||
next_url = request.session.get('oidc_auth_next_url', None)
|
||||
code_verifier = request.session.get('oidc_auth_code_verifier', None)
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = auth.authenticate(nonce=nonce, request=request)
|
||||
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
|
||||
58
apps/authentication/migrations/0016_auto_20221220_1956.py
Normal file
58
apps/authentication/migrations/0016_auto_20221220_1956.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 3.2.14 on 2022-12-20 11:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0015_auto_20221205_1136'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='connectiontoken',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Comment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ssotoken',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Comment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='temptoken',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Comment'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connectiontoken',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connectiontoken',
|
||||
name='updated_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ssotoken',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ssotoken',
|
||||
name='updated_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='temptoken',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='temptoken',
|
||||
name='updated_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
import base64
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -8,17 +11,17 @@ from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from assets.const import Protocol
|
||||
from common.db.fields import EncryptCharField
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import lazyproperty, pretty_string
|
||||
from common.utils import lazyproperty, pretty_string, bulk_get
|
||||
from common.utils.timezone import as_current_tz
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from terminal.models import Applet
|
||||
|
||||
|
||||
def date_expired_default():
|
||||
return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION)
|
||||
|
||||
|
||||
class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||
class ConnectionToken(JMSOrgBaseModel):
|
||||
value = models.CharField(max_length=64, default='', verbose_name=_("Value"))
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.SET_NULL, null=True, blank=True,
|
||||
@@ -101,6 +104,9 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||
error = _('No account')
|
||||
raise PermissionDenied(error)
|
||||
|
||||
if timezone.now() - self.date_created < timedelta(seconds=60):
|
||||
return True, None
|
||||
|
||||
if not self.permed_account or not self.permed_account.actions:
|
||||
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
|
||||
self.user, self.asset, self.account
|
||||
@@ -115,6 +121,75 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||
def platform(self):
|
||||
return self.asset.platform
|
||||
|
||||
@lazyproperty
|
||||
def connect_method_object(self):
|
||||
from common.utils import get_request_os
|
||||
from jumpserver.utils import get_current_request
|
||||
from terminal.connect_methods import ConnectMethodUtil
|
||||
|
||||
request = get_current_request()
|
||||
os = get_request_os(request) if request else 'windows'
|
||||
method = ConnectMethodUtil.get_connect_method(
|
||||
self.connect_method, protocol=self.protocol, os=os
|
||||
)
|
||||
return method
|
||||
|
||||
def get_remote_app_option(self):
|
||||
cmdline = {
|
||||
'app_name': self.connect_method,
|
||||
'user_id': str(self.user.id),
|
||||
'asset_id': str(self.asset.id),
|
||||
'token_id': str(self.id)
|
||||
}
|
||||
cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode()
|
||||
app = '||tinker'
|
||||
options = {
|
||||
'remoteapplicationmode:i': '1',
|
||||
'remoteapplicationprogram:s': app,
|
||||
'remoteapplicationname:s': app,
|
||||
'alternate shell:s': app,
|
||||
'remoteapplicationcmdline:s': cmdline_b64,
|
||||
}
|
||||
return options
|
||||
|
||||
def get_applet_option(self):
|
||||
method = self.connect_method_object
|
||||
if not method or method.get('type') != 'applet' or method.get('disabled', False):
|
||||
return None
|
||||
|
||||
applet = Applet.objects.filter(name=method.get('value')).first()
|
||||
if not applet:
|
||||
return None
|
||||
|
||||
host_account = applet.select_host_account()
|
||||
if not host_account:
|
||||
return None
|
||||
|
||||
host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl'))
|
||||
gateway = host.gateway.select_gateway() if host.domain else None
|
||||
|
||||
data = {
|
||||
'id': account.id,
|
||||
'applet': applet,
|
||||
'host': host,
|
||||
'gateway': gateway,
|
||||
'account': account,
|
||||
'remote_app_option': self.get_remote_app_option()
|
||||
}
|
||||
token_account_relate_key = f'token_account_relate_{account.id}'
|
||||
cache.set(token_account_relate_key, lock_key, ttl)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def release_applet_account(account_id):
|
||||
token_account_relate_key = f'token_account_relate_{account_id}'
|
||||
lock_key = cache.get(token_account_relate_key)
|
||||
if lock_key:
|
||||
cache.delete(lock_key)
|
||||
cache.delete(token_account_relate_key)
|
||||
return 'released'
|
||||
return 'not found or expired'
|
||||
|
||||
@lazyproperty
|
||||
def account_object(self):
|
||||
from assets.models import Account
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import ObjectRelatedField
|
||||
from acls.models import CommandGroup, CommandFilterACL
|
||||
from assets.const import SecretType
|
||||
from assets.models import Asset, Account, Platform, Gateway, Domain
|
||||
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
|
||||
from users.models import User
|
||||
from perms.serializers.permission import ActionChoicesField
|
||||
from common.drf.fields import LabeledChoiceField
|
||||
from common.drf.fields import ObjectRelatedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
|
||||
from perms.serializers.permission import ActionChoicesField
|
||||
from users.models import User
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ConnectionTokenSecretSerializer',
|
||||
'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer'
|
||||
]
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class _SimpleAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
@@ -45,6 +46,7 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
|
||||
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
@@ -96,6 +98,24 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer):
|
||||
return names
|
||||
|
||||
|
||||
class _ConnectionTokenConnectMethodSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(label=_('Name'))
|
||||
protocol = serializers.CharField(label=_('Protocol'))
|
||||
os = serializers.CharField(label=_('OS'))
|
||||
is_builtin = serializers.BooleanField(label=_('Is builtin'))
|
||||
is_active = serializers.BooleanField(label=_('Is active'))
|
||||
platform = _ConnectionTokenPlatformSerializer(label=_('Platform'))
|
||||
action = ActionChoicesField(label=_('Action'))
|
||||
options = serializers.JSONField(label=_('Options'))
|
||||
|
||||
|
||||
class _ConnectTokenConnectMethodSerializer(serializers.Serializer):
|
||||
label = serializers.CharField(label=_('Label'))
|
||||
value = serializers.CharField(label=_('Value'))
|
||||
type = serializers.CharField(label=_('Type'))
|
||||
component = serializers.CharField(label=_('Component'))
|
||||
|
||||
|
||||
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||
user = _ConnectionTokenUserSerializer(read_only=True)
|
||||
asset = _ConnectionTokenAssetSerializer(read_only=True)
|
||||
@@ -104,30 +124,28 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||
platform = _ConnectionTokenPlatformSerializer(read_only=True)
|
||||
domain = ObjectRelatedField(queryset=Domain.objects, required=False, label=_('Domain'))
|
||||
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
|
||||
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
|
||||
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
|
||||
actions = ActionChoicesField()
|
||||
expire_at = serializers.IntegerField()
|
||||
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
|
||||
connect_method = serializers.SerializerMethodField(label=_('Connect method'))
|
||||
|
||||
class Meta:
|
||||
model = ConnectionToken
|
||||
fields = [
|
||||
'id', 'value', 'user', 'asset', 'account',
|
||||
'platform', 'command_filter_acls', 'protocol',
|
||||
'domain', 'gateway', 'actions', 'expire_at', 'expire_now',
|
||||
'connect_method'
|
||||
'domain', 'gateway', 'actions', 'expire_at',
|
||||
'expire_now', 'connect_method',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'value': {'read_only': True},
|
||||
}
|
||||
|
||||
def get_connect_method(self, obj):
|
||||
from terminal.const import TerminalType
|
||||
from common.utils import get_request_os
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
os = get_request_os(request)
|
||||
else:
|
||||
os = 'windows'
|
||||
method = TerminalType.get_connect_method(obj.connect_method, protocol=obj.protocol, os=os)
|
||||
return method
|
||||
|
||||
class ConnectTokenAppletOptionSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(label=_('ID'))
|
||||
applet = ObjectRelatedField(read_only=True)
|
||||
host = _ConnectionTokenAssetSerializer(read_only=True)
|
||||
account = _ConnectionTokenAccountSerializer(read_only=True)
|
||||
gateway = _ConnectionTokenGatewaySerializer(read_only=True)
|
||||
remote_app_option = serializers.JSONField(read_only=True)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
|
||||
from ..models import ConnectionToken
|
||||
|
||||
__all__ = [
|
||||
@@ -12,6 +12,9 @@ __all__ = [
|
||||
|
||||
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
||||
input_secret = EncryptedField(
|
||||
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConnectionToken
|
||||
|
||||
@@ -18,6 +18,23 @@
|
||||
|
||||
<style>
|
||||
.login-content {
|
||||
{#box-shadow: 0 5px 5px -3px rgb(0 0 0 / 15%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);#}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
height: 50px;
|
||||
width: 1000px;
|
||||
margin: 40px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-item {
|
||||
padding: 5px 20px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.footer-item a {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
@@ -52,15 +69,15 @@
|
||||
}
|
||||
|
||||
.login-content {
|
||||
height: 490px;
|
||||
width: 1066px;
|
||||
height: 500px;
|
||||
width: 1000px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-top: calc((100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
background-color: #f3f3f3;
|
||||
height: calc(100vh - (100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
@@ -70,7 +87,7 @@
|
||||
|
||||
.right-image-box {
|
||||
height: 100%;
|
||||
width: 56%;
|
||||
width: 50%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -78,7 +95,7 @@
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
width: 44%;
|
||||
width: 50%;
|
||||
border-right: 1px solid #EFF0F1;
|
||||
}
|
||||
|
||||
@@ -120,12 +137,12 @@
|
||||
}
|
||||
|
||||
.login-page-language {
|
||||
font-size: 12px!important;
|
||||
font-size: 12px !important;
|
||||
margin-right: -32px !important;
|
||||
padding-top: 12px !important;
|
||||
padding-left: 0 !important;
|
||||
padding-bottom: 8px !important;
|
||||
color:#8F959E !important;
|
||||
color: #8F959E !important;
|
||||
font-weight: 350 !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
@@ -137,14 +154,16 @@
|
||||
|
||||
.jms-title {
|
||||
font-size: 21px;
|
||||
font-weight:400;
|
||||
font-weight: 400;
|
||||
color: #151515;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.more-methods-title {
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.more-methods-title:before, .more-methods-title:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -153,18 +172,23 @@
|
||||
border: 1px dashed #e7eaec;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.more-methods-title:before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.more-methods-title:after {
|
||||
right: 0;
|
||||
}
|
||||
.more-methods-title.ja:before, .more-methods-title.ja:after{
|
||||
|
||||
.more-methods-title.ja:before, .more-methods-title.ja:after {
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
.captcha-field .form-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.auto-login.form-group .checkbox {
|
||||
margin: 5px 0;
|
||||
}
|
||||
@@ -176,16 +200,20 @@
|
||||
.has-error .more-login {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
color: #646A73;
|
||||
}
|
||||
|
||||
.navbar-right .dropdown-menu {
|
||||
right: -24px!important;
|
||||
right: -24px !important;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.auto_login_box {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.auto-login input[type=checkbox] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@@ -201,9 +229,14 @@
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-login input[type=checkbox]:checked {
|
||||
border: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.auto-login > .row::after {
|
||||
clear: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -215,11 +248,31 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
||||
<div style="background-color: white">
|
||||
<div class="jms-title">
|
||||
<img src="{{ INTERFACE.logo_text_green }}" class="jms-title-img" />
|
||||
<div style="position: relative;top: 50%;transform: translateY(-50%);">
|
||||
<div style='padding: 15px 60px; text-align: left'>
|
||||
<h2 style='font-weight: 400;display: inline'>
|
||||
{% trans 'Login' %}
|
||||
</h2>
|
||||
<ul class=" nav navbar-top-links navbar-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#"
|
||||
target="_blank">
|
||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||
<span>{{ current_lang.title }}<b class="caret"></b></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||
{% for lang in langs %}
|
||||
<li>
|
||||
<a href="{% url 'i18n-switch' lang=lang.code %}">
|
||||
<span>{{ lang.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contact-form col-md-10 col-md-offset-1">
|
||||
<div class="contact-form col-md-10 col-md-offset-1" style='float: none; overflow: hidden'>
|
||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
@@ -227,37 +280,17 @@
|
||||
<p class="help-block red-fonts">
|
||||
{{ form.non_field_errors.as_text }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="welcome-message">
|
||||
{% trans 'Welcome back, please enter username and password to login' %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<ul class="nav navbar-top-links navbar-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||
<span>{{ current_lang.title }}<b class="caret"></b></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||
{% for lang in langs %}
|
||||
<li>
|
||||
<a href="{% url 'i18n-switch' lang=lang.code %}">
|
||||
<span>{{ lang.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% bootstrap_field form.username show_label=False %}
|
||||
<div class="form-group {% if form.password.errors %} has-error {% endif %}">
|
||||
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required>
|
||||
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
||||
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}"
|
||||
required>
|
||||
<input id="password-hidden" type="text" style="display:none"
|
||||
name="{{ form.password.html_name }}">
|
||||
{% if form.password.errors %}
|
||||
<p class="help-block" style="text-align: left">
|
||||
<p class="help-block" style="text-align: left">
|
||||
{{ form.password.errors.as_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -274,7 +307,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group auto-login" style="margin-bottom: 10px">
|
||||
<div class="row">
|
||||
<div class="row" style="overflow: hidden;">
|
||||
<div class="col-md-6" style="text-align: left">
|
||||
{% if form.auto_login %}
|
||||
{% bootstrap_field form.auto_login form_group_class='auto_login_box' %}
|
||||
@@ -303,7 +336,8 @@
|
||||
<div class="more-login-items">
|
||||
{% for method in auth_methods %}
|
||||
<a href="{{ method.url }}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ method.logo }}" height="15" width="15"></i> {{ method.name }}
|
||||
<i class="fa">
|
||||
<img src="{{ method.logo }}" height="15" width="15"></i> {{ method.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -317,6 +351,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user