Merge pull request #9250 from jumpserver/v3

v3 to dev
This commit is contained in:
老广
2022-12-28 13:26:25 +08:00
committed by GitHub
284 changed files with 20975 additions and 3445 deletions

View File

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

View File

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

View File

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

View 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'),
),
]

View File

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

View File

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

View File

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

View File

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