[Feature] 支持otp

pull/1214/head^2
BaiJiangjie 2018-04-18 12:48:07 +08:00
parent 7fc2ef00ee
commit 0bbfc7433d
27 changed files with 935 additions and 88 deletions

146
apps/static/css/otp.css Normal file
View File

@ -0,0 +1,146 @@
/*公共样式*/
*{
margin:0;
padding: 0;
outline: none;
}
a{
text-decoration: none;
color:black
}
li{
list-style:none;
}
button{
outline: none;
}
.red-fonts{
color: #ed5565;
font-size: 15px;
text-align: center;
}
/*header样式*/
header{
overflow:hidden ;
background: #dedede;
padding:15px 200px;
}
header .logo a{
float:left;
}
header .logo a:nth-child(2){
padding-top: 13px;
}
header div:nth-child(1){
float:left;
}
header div:nth-child(2){
float:right;
font-size: 12px;
padding-top: 20px;
}
header div:nth-child(2) a:hover{
color:#1ab394;
}
/*article样式*/
article{
padding-top: 50px;
padding:50px 370px
}
article ul{
float: left;
position: relative;
left: 50%;
margin-bottom: 50px;
}
article ul li{
float: left;
position: relative;
right: 50%;
}
article ul li span,article ul li i{
display: block;
float: left;
}
article ul li span{
width: 150px;
height: 4px;
margin: 15px 0;
background: black;
}
article ul li:last-child{
padding-left: 2px;
}
.iconfont{
font-size: 30px;
color: grey;
}
.back{
margin-left:-15px;
}
.active{
color:#1ab394;
}
.clearfix:after {
content:"";
height:0;
visibility:hidden;
display:block;
clear:both;
}
.verify{
text-align: center;
font-size: 14px;
/*padding-left:70px;*/
color: grey;
}
.verify span{
color:red;
}
.line{
width: 500px;
height:1px;
margin-left:100px;
margin-top:10px ;
background: grey;
}
/*输入框样式*/
.form-input{
text-align: center;
margin: 20px auto;
}
.form-input input{
width: 200px;
height: 30px;
padding-left: 10px;
outline: none;
}
/*身份验证*/
/*安装应用*/
.verify div{
display: inline-block;
}
.verify div:nth-child(3){
margin-left: 58px;
}
.next{
margin: 20px auto;
display: block;
width: 214px;
line-height: 34px;
background: #1ab394;
text-align: center;
border-radius: 6px;
color: white;
}
/*绑定TOTP*/
/*版权信息*/
footer{
text-align:center;
font-size: 14px;
color: #1a1a1a;
}

View File

@ -0,0 +1,25 @@
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1523776860888'); /* IE9*/
src: url('iconfont.eot?t=1523776860888#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
url('iconfont.ttf?t=1523776860888') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg?t=1523776860888#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-duigou:before { content: "\e632"; }
.icon-step:before { content: "\e60e"; }
.icon-step1:before { content: "\e60f"; }
.icon-step2:before { content: "\e610"; }

Binary file not shown.

View File

@ -0,0 +1 @@
(function(window){var svgSprite='<svg><symbol id="icon-duigou" viewBox="0 0 1024 1024"><path d="M512 0C228.266667 0 0 228.266667 0 512c0 283.733333 228.266667 512 512 512 283.733333 0 512-228.266667 512-512C1024 228.266667 795.733333 0 512 0zM832 384 492.8 723.2C469.333333 746.666667 426.666667 746.666667 403.2 723.2L192 512c0 0-32-32 0-64s64 0 64 0l192 192 320-320c0 0 32-32 64 0S832 384 832 384z" ></path></symbol><symbol id="icon-step" viewBox="0 0 1024 1024"><path d="M511.3 64.6h-1.8c-0.7-0.4-1.6-0.7-2.5-0.7H188.3c-69 0-125.5 56.5-125.5 125.5v289.1c-0.9 11.9-1.4 24-1.4 36.1 0 248.5 201.5 450 450 450s450-201.5 450-450-201.5-450-450.1-450z m162.1 724.8H326.5v-66H462V264l-139 40.2v-70.3l215.2-62.5v552h135.2v66z" ></path></symbol><symbol id="icon-step1" viewBox="0 0 1024 1024"><path d="M511.3 64.6h-1.8c-0.7-0.4-1.6-0.7-2.5-0.7H188.3c-69 0-125.5 56.5-125.5 125.5v289.1c-0.9 11.9-1.4 24-1.4 36.1 0 248.5 201.5 450 450 450s450-201.5 450-450-201.5-450-450.1-450z m91.4 684.9c-39.2 33.3-91.3 50-156.4 50-57.3 0-103.5-10.7-138.7-32v-79.3c41.4 32 88.1 48 140.2 48 41.7 0 74.7-10.2 99-30.7 24.3-20.4 36.5-47.9 36.5-82.2 0-76.6-54.8-114.8-164.5-114.8h-50.4v-63.3h48c97.1 0 145.7-35.9 145.7-107.8 0-66.4-37.1-99.6-111.3-99.6-42.5 0-82.4 14.3-119.9 43v-72.3c39.6-22.9 85.8-34.4 138.7-34.4 51.6 0 93 13.5 124.2 40.4s46.9 61.9 46.9 104.9c0 79.2-40.4 130.1-121.1 152.7v1.6c43.8 4.7 78.3 20.1 103.7 46.3s38.1 58.8 38.1 97.9c0 54.4-19.6 98.3-58.7 131.6z" ></path></symbol><symbol id="icon-step2" viewBox="0 0 1024 1024"><path d="M511.3 64.6h-1.8c-0.7-0.4-1.6-0.7-2.5-0.7H188.3c-69 0-125.5 56.5-125.5 125.5v289.1c-0.9 11.9-1.4 24-1.4 36.1 0 248.5 201.5 450 450 450s450-201.5 450-450-201.5-450-450.1-450z m150.8 656.8v68h-368V723l175.8-175.4c48.4-48.4 80.9-86.8 97.3-115 16.4-28.3 24.6-57.5 24.6-87.7 0-34.4-9.6-60.7-28.9-79.1-19.3-18.4-47.1-27.5-83.6-27.5-53.9 0-105.3 22.9-154.3 68.8v-77.7c47.7-36.7 103.1-55.1 166.4-55.1 54.4 0 97.4 14.7 128.9 44.1 31.5 29.4 47.3 69 47.3 118.8 0 37.5-10.1 74.3-30.3 110.4-20.2 36.1-58.4 82-114.6 137.7l-138 134.5v1.6h277.4z" ></path></symbol></svg>';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window)

View File

@ -0,0 +1,45 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="x" unicode="x" horiz-adv-x="1001"
d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
<glyph glyph-name="duigou" unicode="&#58930;" d="M512 896C228.266667 896 0 667.733333 0 384c0-283.733333 228.266667-512 512-512 283.733333 0 512 228.266667 512 512C1024 667.733333 795.733333 896 512 896zM832 512 492.8 172.8C469.333333 149.333333 426.666667 149.333333 403.2 172.8L192 384c0 0-32 32 0 64s64 0 64 0l192-192 320 320c0 0 32 32 64 0S832 512 832 512z" horiz-adv-x="1024" />
<glyph glyph-name="step" unicode="&#58894;" d="M511.3 831.4h-1.8c-0.7 0.4-1.6 0.7-2.5 0.7H188.3c-69 0-125.5-56.5-125.5-125.5v-289.1c-0.9-11.9-1.4-24-1.4-36.1 0-248.5 201.5-450 450-450s450 201.5 450 450-201.5 450-450.1 450z m162.1-724.8H326.5v66H462V632l-139-40.2v70.3l215.2 62.5v-552h135.2v-66z" horiz-adv-x="1024" />
<glyph glyph-name="step1" unicode="&#58895;" d="M511.3 831.4h-1.8c-0.7 0.4-1.6 0.7-2.5 0.7H188.3c-69 0-125.5-56.5-125.5-125.5v-289.1c-0.9-11.9-1.4-24-1.4-36.1 0-248.5 201.5-450 450-450s450 201.5 450 450-201.5 450-450.1 450z m91.4-684.9c-39.2-33.3-91.3-50-156.4-50-57.3 0-103.5 10.7-138.7 32v79.3c41.4-32 88.1-48 140.2-48 41.7 0 74.7 10.2 99 30.7 24.3 20.4 36.5 47.9 36.5 82.2 0 76.6-54.8 114.8-164.5 114.8h-50.4v63.3h48c97.1 0 145.7 35.9 145.7 107.8 0 66.4-37.1 99.6-111.3 99.6-42.5 0-82.4-14.3-119.9-43v72.3c39.6 22.9 85.8 34.4 138.7 34.4 51.6 0 93-13.5 124.2-40.4s46.9-61.9 46.9-104.9c0-79.2-40.4-130.1-121.1-152.7v-1.6c43.8-4.7 78.3-20.1 103.7-46.3s38.1-58.8 38.1-97.9c0-54.4-19.6-98.3-58.7-131.6z" horiz-adv-x="1024" />
<glyph glyph-name="step2" unicode="&#58896;" d="M511.3 831.4h-1.8c-0.7 0.4-1.6 0.7-2.5 0.7H188.3c-69 0-125.5-56.5-125.5-125.5v-289.1c-0.9-11.9-1.4-24-1.4-36.1 0-248.5 201.5-450 450-450s450 201.5 450 450-201.5 450-450.1 450z m150.8-656.8v-68h-368V173l175.8 175.4c48.4 48.4 80.9 86.8 97.3 115 16.4 28.3 24.6 57.5 24.6 87.7 0 34.4-9.6 60.7-28.9 79.1-19.3 18.4-47.1 27.5-83.6 27.5-53.9 0-105.3-22.9-154.3-68.8v77.7c47.7 36.7 103.1 55.1 166.4 55.1 54.4 0 97.4-14.7 128.9-44.1 31.5-29.4 47.3-69 47.3-118.8 0-37.5-10.1-74.3-30.3-110.4-20.2-36.1-58.4-82-114.6-137.7l-138-134.5v-1.6h277.4z" horiz-adv-x="1024" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

1
apps/static/js/plugins/qrcode/qrcode.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@ from .tasks import write_login_log_async
from .models import User, UserGroup from .models import User, UserGroup
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \ from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
IsSuperUserOrAppUser IsSuperUserOrAppUser
from .utils import check_user_valid, generate_token from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
from common.utils import get_logger from common.utils import get_logger
@ -129,47 +129,75 @@ class UserToken(APIView):
class UserProfile(APIView): class UserProfile(APIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = UserSerializer
def get(self, request): def get(self, request):
return Response(request.user.to_json()) # return Response(request.user.to_json())
return Response(self.serializer_class(request.user).data)
def post(self, request): def post(self, request):
return Response(request.user.to_json()) return Response(self.serializer_class(request.user).data)
class UserAuthApi(APIView): class UserAuthApi(APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request): def post(self, request):
otp_check = request.data.get('otp_check', None)
if otp_check:
# otp验证
return self.check_auth_otp(request)
else:
# password验证
return self.check_auth_password(request)
def check_auth_password(self, request):
user, msg = self.check_user_valid(request)
if user:
token = generate_token(request, user)
if not user.otp_enabled:
self.write_login_log(request, user)
return Response({'token': token, 'user': self.serializer_class(user).data})
else:
return Response({'msg': msg}, status=401)
def check_auth_otp(self, request):
otp_code = request.data.get('otp_code', '')
user, msg = self.check_user_valid(request)
if user:
token = generate_token(request, user)
if check_otp_code(user.otp_secret_key, otp_code):
self.write_login_log(request, user)
return Response({'token': token, 'user': self.serializer_class(user).data})
return Response({'msg': msg}, status=401)
@staticmethod
def check_user_valid(request):
username = request.data.get('username', '') username = request.data.get('username', '')
password = request.data.get('password', '') password = request.data.get('password', '')
public_key = request.data.get('public_key', '') public_key = request.data.get('public_key', '')
login_type = request.data.get('login_type', '')
login_ip = request.data.get('remote_addr', None)
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get("REMOTE_ADDR")
user, msg = check_user_valid( user, msg = check_user_valid(
username=username, password=password, username=username, password=password,
public_key=public_key public_key=public_key
) )
return user, msg
@staticmethod
def write_login_log(request, user):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_login_ip(request)
if user:
token = generate_token(request, user)
write_login_log_async.delay( write_login_log_async.delay(
user.username, ip=login_ip, user.username, ip=login_ip,
type=login_type, user_agent=user_agent, type=login_type, user_agent=user_agent,
) )
return Response({'token': token, 'user': user.to_json()})
else:
return Response({'msg': msg}, status=401)
class UserConnectionTokenApi(APIView): class UserConnectionTokenApi(APIView):

View File

@ -18,6 +18,18 @@ class UserLoginForm(AuthenticationForm):
captcha = CaptchaField() captcha = CaptchaField()
class UserCheckPasswordForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
class UserCheckOtpCodeForm(forms.Form):
otp_code = forms.CharField(label=_('Otp_code'), max_length=6)
class UserCreateUpdateForm(forms.ModelForm): class UserCreateUpdateForm(forms.ModelForm):
role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP)
password = forms.CharField( password = forms.CharField(

View File

@ -45,7 +45,7 @@ class User(AbstractUser):
wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat')) wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat'))
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_('Phone')) phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_('Phone'))
otp_level = models.SmallIntegerField(default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('Enable OTP')) otp_level = models.SmallIntegerField(default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('Enable OTP'))
otp_secret_key = models.CharField(max_length=16, blank=True) otp_secret_key = models.CharField(max_length=16, blank=True, null=True)
# Todo: Auto generate key, let user download # Todo: Auto generate key, let user download
_private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Private key')) _private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Private key'))
_public_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Public key')) _public_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Public key'))
@ -211,15 +211,20 @@ class User(AbstractUser):
def otp_enabled(self): def otp_enabled(self):
return self.otp_level > 0 return self.otp_level > 0
def enabled_otp(self): @property
def otp_force_enabled(self):
return self.otp_level == 2
def enable_otp(self):
if not self.otp_force_enabled:
self.otp_level = 1 self.otp_level = 1
def force_enable_otp(self): def force_enable_otp(self):
self.otp_level = 2 self.otp_level = 2
@property def disable_otp(self):
def otp_force_enabled(self): self.otp_level = 0
return self.otp_level == 2 self.otp_secret_key = ''
def to_json(self): def to_json(self):
return OrderedDict({ return OrderedDict({
@ -233,6 +238,7 @@ class User(AbstractUser):
'groups': [group.name for group in self.groups.all()], 'groups': [group.name for group in self.groups.all()],
'wechat': self.wechat, 'wechat': self.wechat,
'phone': self.phone, 'phone': self.phone,
'otp_level': self.otp_level,
'comment': self.comment, 'comment': self.comment,
'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None
}) })

View File

@ -19,7 +19,10 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
exclude = ['first_name', 'last_name', 'password', '_private_key', '_public_key'] exclude = [
'first_name', 'last_name', 'password', '_private_key',
'_public_key', 'otp_secret_key', 'user_permissions'
]
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super(UserSerializer, self).get_field_names(declared_fields, info) fields = super(UserSerializer, self).get_field_names(declared_fields, info)

View File

@ -0,0 +1,87 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title> Jumpserver </title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'fonts/font_otp/iconfont.css' %}" />
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
</head>
<body>
<!--头部-->
<header>
<div class="logo">
<a href="{% url 'index' %}">
<img src="{% static 'img/logo.png' %}" alt="" width="50px" height="50px"/>
</a>
<a href="{% url 'index' %}">Jumpserver</a>
</div>
<div>
<a href="{% url 'index' %}">首页</a>
<b></b>
<a href="#">帮助中心</a>
<b></b>
<a href="https://www.github.com/jumpserver/">GitHub</a>
</div>
</header>
<!--内容-->
<article>
<div class="clearfix">
<ul class="change-color">
<li>
<div>
<i class="iconfont icon-step active"></i>
<span></span>
</div>
<div class="back">验证身份</div>
</li>
<li>
<div>
<i class="iconfont icon-step2"></i>
<span></span>
</div>
<div class="back">安装应用</div>
</li>
<li>
<div>
<i class="iconfont icon-step1"></i>
<span></span>
</div>
<div class="back">绑定TOTP</div>
</li>
<li>
<div>
<i class="iconfont icon-duigou"></i>
</div>
<div>完成</div>
</li>
</ul>
</div>
<div >
<div class="verify">安全令牌验证&nbsp;&nbsp;账户&nbsp;<span>{{ user.username }}</span>&nbsp;&nbsp;请按照以下步骤完成绑定操作</div>
<div class="line"></div>
{% block content %}
{% endblock %}
</div>
</article>
<footer>
<div class="" style="margin-top: 100px;">
{% include '_copyright.html' %}
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,87 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Jumpserver </title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
<style>
.captcha {
float: right;
}
</style>
</head>
<body class="gray-bg">
<div class="loginColumns animated fadeInDown">
<div class="row">
<div class="col-md-6">
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
<p>
全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
</p>
<p>
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
</p>
<p>
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发访问限制。
</p>
<p>
改变世界,从一点点开始。
</p>
</div>
<div class="col-md-6">
<div class="ibox-content">
<div>
<img src="{% static 'img/logo.png' %}" width="60" height="60">
<span class="font-bold text-center" style="font-size: 24px; font-family: inherit; margin-left: 20px">{% trans '二次认证' %}</span>
</div>
<div class="m-t">
<div class="form-group">
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<div class="text-center">
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
</div>
<p style="margin: 30px auto">请在手机中打开Google Authenticator应用输入6位动态码</p>
</div>
<form class="m-t" role="form" method="post" action="">
{% csrf_token %}
{% if 'otp_code' in form.errors %}
<p class="red-fonts">{{ form.otp_code.errors.as_text }}</p>
{% endif %}
<div class="form-group">
<input type="text" class="form-control" name="otp_code" placeholder="{% trans 'Six figures' %}" required="">
</div>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<a href="#">
<small>{% trans "Can't provide security? Please contact the administrator" %}</small>
</a>
</form>
</div>
<p class="m-t">
</p>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
{% include '_copyright.html' %}
</div>
</div>
</div>
</body>
</html>

View File

@ -87,10 +87,18 @@
<td>{% trans 'Role' %}:</td> <td>{% trans 'Role' %}:</td>
<td><b>{{ user_object.get_role_display }}</b></td> <td><b>{{ user_object.get_role_display }}</b></td>
</tr> </tr>
{# <tr>#} <tr>
{# <td>{% trans 'Enable OTP' %}:</td>#} <td>{% trans 'Enable OTP' %}:</td>
{# <td><b>{{ user_object.enable_otp|yesno:"Yes,No,Unknown"}}</b></td>#} <td><b>
{# </tr>#} {% if user_object.otp_force_enabled %}
{% trans 'Force enabled' %}
{% elif user_object.otp_enabled%}
{% trans 'Enabled' %}
{% else %}
{% trans 'Disabled' %}
{% endif %}
</b></td>
</tr>
<tr> <tr>
<td>{% trans 'Date expired' %}:</td> <td>{% trans 'Date expired' %}:</td>
<td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td> <td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td>
@ -137,22 +145,23 @@
</div> </div>
</div> </div>
</span></td> </span></td>
</tr>
<tr>
<td>{% trans 'Force enabled OTP' %}:</td>
<td><span class="pull-right">
<div class="switch">
<div class="onoffswitch">
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.otp_force_enabled%} checked {% endif %}{% if request.user == user_object %} disabled {% endif %}
id="force_enable_otp">
<label class="onoffswitch-label" for="force_enable_otp">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</div>
</span></td>
</tr> </tr>
{# <tr>#}
{# <td>{% trans 'Enable OTP' %}:</td>#}
{# <td><span class="pull-right">#}
{# <div class="switch">#}
{# <div class="onoffswitch">#}
{# <input type="checkbox" class="onoffswitch-checkbox" {% if user_object.enable_otp %} checked {% endif %}#}
{# id="enable_otp">#}
{# <label class="onoffswitch-label" for="enable_otp">#}
{# <span class="onoffswitch-inner"></span>#}
{# <span class="onoffswitch-switch"></span>#}
{# </label>#}
{# </div>#}
{# </div>#}
{# </span></td>#}
{# </tr>#}
<tr> <tr>
<td>{% trans 'Send reset password mail' %}:</td> <td>{% trans 'Send reset password mail' %}:</td>
<td> <td>
@ -277,19 +286,28 @@ $(document).ready(function() {
success_message: success success_message: success
}); });
}) })
{#.on('click', '#enable_otp', function() {#} .on('click', '#force_enable_otp', function() {
{# var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";#} var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
{# var checked = $(this).prop('checked');#} var checked = $(this).prop('checked');
{# var body = {#} var otp_level;
{# 'enable_otp': checked#} var otp_secret_key;
{# };#} if(checked){
{# var success = '{% trans "Update successfully!" %}';#} otp_level = 2
{# APIUpdateAttr({#} }else{
{# url: the_url,#} otp_level = 0;
{# body: JSON.stringify(body),#} otp_secret_key = '';
{# success_message: success#} }
{# });#} var body = {
{# });#} 'otp_level': otp_level,
'otp_secret_key': otp_secret_key
};
var success = '{% trans "Update successfully!" %}';
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success_message: success
});
})
.on('click', '#btn_join_group', function() { .on('click', '#btn_join_group', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) { if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false; return false;

View File

@ -0,0 +1,37 @@
{% extends 'users/_base_otp.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">账号保护已开启,请根据提示完成以下操作</strong></p>
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
<p style="margin: 20px auto;">请在手机中打开Google Authenticator应用输入6为动态码</p>
</div>
<form class="" role="form" method="post" action="">
{% csrf_token %}
{% if 'otp_code' in form.errors %}
<p class="red-fonts">{{ form.otp_code.errors.as_text }}</p>
{% endif %}
<div class="form-input">
<input type="text" class="" name="otp_code" placeholder="{% trans 'Six figures' %}" required="">
</div>
<button type="submit" class="next">{% trans 'Next' %}</button>
</form>
<script>
$(function(){
$('.change-color li').eq(2).remove();
$('.change-color li:eq(1) div').eq(1).html('解绑MFA')
})
</script>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends 'users/_base_otp.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="verify">
<p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码获取6位验证码</strong></p>
<div id="qr_code"></div>
<form class="" role="form" method="post" action="">
{% csrf_token %}
<div class="form-input">
<input type="text" class="" name="otp_code" placeholder="{% trans 'Six figures' %}" required="">
</div>
{% if 'otp_code' in form.errors %}
<p style="color: #ed5565">{{ form.otp_code.errors.as_text }}</p>
{% endif %}
<button type="submit" class="next">{% trans 'Next' %}</button>
</form>
</div>
<script>
$('.change-color li:eq(1) i').css('color', '#1ab394');
$('.change-color li:eq(2) i').css('color', '#1ab394');
$(document).ready(function() {
// 生成用户绑定otp的二维码
var qrcode = new QRCode(document.getElementById('qr_code'), {
text: "{{ otp_uri|safe}}",
width: 180 ,
height: 180,
colorDark: '#000000',
colorLight: '#ffffff',
correctlevel: QRCode.CorrectLevel.H
});
document.getElementById('qr_code').removeAttribute("title");
})
</script>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'users/_base_otp.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<div class="verify">
<p style="margin: 20px auto;"><strong style="color: #000000">请在手机端下载并安装 Google Authenticator 应用</strong></p>
<div>
<img src="{% static 'img/authenticator_android.png' %}" width="128" height="128" alt="">
<p>Android手机下载</p>
</div>
<div>
<img src="{% static 'img/authenticator_iphone.png' %}" width="128" height="128" alt="">
<p>iPhone手机下载</p>
</div>
<p style="margin: 20px auto;"></p>
<p style="margin: 20px auto;"><strong style="color: #000000">安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)</strong></p>
</div>
<a href="{% url 'users:user-otp-enable-bind' %}" class="next">{% trans 'Next' %}</a>
<script>
$(function(){
$('.change-color li:eq(1) i').css('color', '#1ab394')
})
</script>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends 'users/_base_otp.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<form class="" role="form" method="post" action="">
{% csrf_token %}
<div class="form-input">
<input type="text" class="" name="{{ form.username.html_name }}" value="{{ form.username.value }}" readonly="readonly" required="">
</div>
<div class="form-input">
<input type="password" class="" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div>
{% if 'password' in form.errors %}
<p class="red-fonts">{{ form.password.errors.as_text }}</p>
{% endif %}
<button type="submit" class="next">{% trans 'Next' %}</button>
</form>
{% endblock %}

View File

@ -65,7 +65,15 @@
</tr> </tr>
<tr> <tr>
<td class="text-navy">{% trans 'OTP' %}</td> <td class="text-navy">{% trans 'OTP' %}</td>
<td>{{ user.otp_enabled|yesno:"Yes,No,Unkown" }}</td> <td>
{% if user.otp_force_enabled %}
{% trans 'Force enable' %}
{% elif user.otp_enabled%}
{% trans 'Enable' %}
{% else %}
{% trans 'Disable' %}
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<td class="text-navy">{% trans 'Public key' %}</td> <td class="text-navy">{% trans 'Public key' %}</td>
@ -136,6 +144,28 @@
</span> </span>
</td> </td>
</tr> </tr>
<tr class="no-borders-tr">
<td>{% trans 'Update otp' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
href="
{% if request.user.otp_enabled and request.user.otp_secret_key %}
{% if request.user.otp_force_enabled %}
javascript:void(0)
"><span style="color:#ed5565">{% trans 'Disable' %}</span>
{% else %}
{% url 'users:user-otp-disable-authentication' %}
">{% trans 'Disable' %}
{% endif %}
{% else %}
{% url 'users:user-otp-enable-authentication' %}
">{% trans 'Enable' %}
{% endif %}
</a>
</span>
</td>
</tr>
<tr> <tr>
<td>{% trans 'Update SSH public key' %}:</td> <td>{% trans 'Update SSH public key' %}:</td>
<td> <td>

View File

@ -10,6 +10,7 @@ urlpatterns = [
# Login view # Login view
url(r'^login$', views.UserLoginView.as_view(), name='login'), url(r'^login$', views.UserLoginView.as_view(), name='login'),
url(r'^logout$', views.UserLogoutView.as_view(), name='logout'), url(r'^logout$', views.UserLogoutView.as_view(), name='logout'),
url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'),
url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'), url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'),
url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'), url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'),
@ -21,6 +22,11 @@ urlpatterns = [
url(r'^profile/password/update/$', views.UserPasswordUpdateView.as_view(), name='user-password-update'), url(r'^profile/password/update/$', views.UserPasswordUpdateView.as_view(), name='user-password-update'),
url(r'^profile/pubkey/update/$', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), url(r'^profile/pubkey/update/$', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'),
url(r'^profile/pubkey/generate/$', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), url(r'^profile/pubkey/generate/$', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
url(r'^profile/otp/enable/authentication/$', views.UserOtpEnableAuthenticationView.as_view(), name='user-otp-enable-authentication'),
url(r'^profile/otp/enable/install-app/$', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'),
url(r'^profile/otp/enable/bind/$', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
url(r'^profile/otp/disable/authentication/$', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'),
url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
# User view # User view
url(r'^user$', views.UserListView.as_view(), name='user-list'), url(r'^user$', views.UserListView.as_view(), name='user-list'),
@ -34,7 +40,6 @@ urlpatterns = [
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'), url(r'^user/(?P<pk>[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'),
# User group view # User group view
url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'),
url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'), url(r'^user-group/(?P<pk>[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'),

View File

@ -1,6 +1,8 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
# #
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import pyotp
import base64 import base64
import logging import logging
import uuid import uuid
@ -17,7 +19,6 @@ from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none from common.utils import reverse, get_object_or_none
from .models import User, LoginLog from .models import User, LoginLog
logger = logging.getLogger('jumpserver') logger = logging.getLogger('jumpserver')
@ -163,7 +164,7 @@ def generate_token(request, user):
remote_addr = request.META.get('REMOTE_ADDR', '') remote_addr = request.META.get('REMOTE_ADDR', '')
if not isinstance(remote_addr, bytes): if not isinstance(remote_addr, bytes):
remote_addr = remote_addr.encode("utf-8") remote_addr = remote_addr.encode("utf-8")
remote_addr = base64.b16encode(remote_addr) #.replace(b'=', '') remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '')
token = cache.get('%s_%s' % (user.id, remote_addr)) token = cache.get('%s_%s' % (user.id, remote_addr))
if not token: if not token:
token = uuid.uuid4().hex token = uuid.uuid4().hex
@ -181,6 +182,16 @@ def validate_ip(ip):
return False return False
def get_login_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = request.META.get('REMOTE_ADDR', '')
return login_ip
def write_login_log(username, type='', ip='', user_agent=''): def write_login_log(username, type='', ip='', user_agent=''):
if not (ip and validate_ip(ip)): if not (ip and validate_ip(ip)):
ip = ip[:15] ip = ip[:15]
@ -211,3 +222,35 @@ def get_ip_city(ip, timeout=10):
except ValueError: except ValueError:
pass pass
return city return city
def get_user(request):
if is_login(request):
user = request.user
else:
user = cache.get(request.session.session_key)
return user
def is_login(request):
return isinstance(request.user, User)
def redirect_user_first_login_or_index(request, redirect_field_name):
if request.user.is_first_login:
return reverse('users:user-first-login')
return request.POST.get(
redirect_field_name,
request.GET.get(redirect_field_name, reverse('index')))
def generate_otp_uri(user, issuer="Jumpserver"):
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8')
cache.set('otp_secret_key', otp_secret_key, 300)
totp = pyotp.TOTP(otp_secret_key)
return totp.provisioning_uri(name=user.username, issuer_name=issuer)
def check_otp_code(otp_secret_key, otp_code):
totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code)

View File

@ -19,17 +19,18 @@ from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView from formtools.wizard.views import SessionWizardView
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
from ..models import User, LoginLog from ..models import User, LoginLog
from ..utils import send_reset_password_mail from ..utils import send_reset_password_mail, check_otp_code , get_login_ip, redirect_user_first_login_or_index
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
from .. import forms from .. import forms
__all__ = [ __all__ = [
'UserLoginView', 'UserLogoutView', 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView',
'UserResetPasswordView', 'UserResetPasswordSuccessView', 'UserResetPasswordView', 'UserResetPasswordSuccessView',
'UserFirstLoginView', 'LoginLogListView' 'UserFirstLoginView', 'LoginLogListView'
@ -53,27 +54,23 @@ class UserLoginView(FormView):
def form_valid(self, form): def form_valid(self, form):
if not self.request.session.test_cookie_worked(): if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again.")) return HttpResponse(_("Please enable cookies and try again."))
auth_login(self.request, form.get_user()) cache.set(self.request.session.session_key, form.get_user(), 600)
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0]
else:
login_ip = self.request.META.get('REMOTE_ADDR', '')
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
write_login_log_async.delay(
self.request.user.username, type='W',
ip=login_ip, user_agent=user_agent
)
return redirect(self.get_success_url()) return redirect(self.get_success_url())
def get_success_url(self): def get_success_url(self):
if self.request.user.is_first_login: user = cache.get(self.request.session.session_key)
return reverse('users:user-first-login')
return self.request.POST.get( if user.otp_enabled and user.otp_secret_key:
self.redirect_field_name, # 1,2 & T
self.request.GET.get(self.redirect_field_name, reverse('index'))) return reverse('users:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2 & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
self.write_login_log()
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
@ -82,6 +79,44 @@ class UserLoginView(FormView):
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def write_login_log(self):
login_ip = get_login_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
write_login_log_async.delay(
self.request.user.username, type='W',
ip=login_ip, user_agent=user_agent
)
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = cache.get(self.request.session.session_key)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
auth_login(self.request, user)
self.write_login_log()
return redirect(self.get_success_url())
else:
form.add_error('otp_code', _('Otp code invalid'))
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def write_login_log(self):
login_ip = get_login_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
write_login_log_async.delay(
self.request.user.username, type='W',
ip=login_ip, user_agent=user_agent
)
@method_decorator(never_cache, name='dispatch') @method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView): class UserLogoutView(TemplateView):

View File

@ -11,6 +11,7 @@ from io import StringIO
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import authenticate, login as auth_login
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
@ -34,9 +35,9 @@ from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from .. import forms from .. import forms
from ..models import User, UserGroup from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user, is_login
from ..signals import post_user_create from ..signals import post_user_create
from ..tasks import write_login_log_async
__all__ = [ __all__ = [
'UserListView', 'UserCreateView', 'UserDetailView', 'UserListView', 'UserCreateView', 'UserDetailView',
@ -46,6 +47,9 @@ __all__ = [
'UserProfileUpdateView', 'UserPasswordUpdateView', 'UserProfileUpdateView', 'UserPasswordUpdateView',
'UserPublicKeyUpdateView', 'UserBulkUpdateView', 'UserPublicKeyUpdateView', 'UserBulkUpdateView',
'UserPublicKeyGenerateView', 'UserPublicKeyGenerateView',
'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView',
'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
'UserOtpDisableAuthenticationView',
] ]
logger = get_logger(__name__) logger = get_logger(__name__)
@ -380,6 +384,7 @@ class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView):
class UserPublicKeyGenerateView(LoginRequiredMixin, View): class UserPublicKeyGenerateView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver')
request.user.public_key = public request.user.public_key = public
@ -389,3 +394,125 @@ class UserPublicKeyGenerateView(LoginRequiredMixin, View):
response['Content-Disposition'] = 'attachment; filename={}'.format(filename) response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
return response return response
class UserOtpEnableAuthenticationView(FormView):
template_name = 'users/user_password_authentication.html'
form_class = forms.UserCheckPasswordForm
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
form['username'].initial = get_user(self.request).username
return form
def get_context_data(self, **kwargs):
context = {
'user': get_user(self.request)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def form_valid(self, form):
password = form.cleaned_data.get('password')
user = get_user(self.request)
user = authenticate(username=user.username, password=password)
if not user:
form.add_error("password", _("Password invalid"))
return self.form_invalid(form)
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('users:user-otp-enable-install-app')
class UserOtpEnableInstallAppView(TemplateView):
template_name = 'users/user_otp_enable_install_app.html'
def get_context_data(self, **kwargs):
context = {
'user': get_user(self.request)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserOtpEnableBindView(TemplateView, FormView):
template_name = 'users/user_otp_enable_bind.html'
form_class = forms.UserCheckOtpCodeForm
success_url = reverse_lazy('users:user-otp-settings-success')
def get_context_data(self, **kwargs):
context = {
'otp_uri': generate_otp_uri(user=get_user(self.request)),
'user': get_user(self.request)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def form_valid(self, form):
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = cache.get('otp_secret_key')
if check_otp_code(otp_secret_key, otp_code):
self.save_otp(otp_secret_key)
return super().form_valid(form)
else:
form.add_error("otp_code", _("Otp code invalid"))
return self.form_invalid(form)
def save_otp(self, otp_secret_key):
user = get_user(self.request)
user.enable_otp()
user.otp_secret_key = otp_secret_key
user.save()
class UserOtpDisableAuthenticationView(FormView):
template_name = 'users/user_otp_authentication.html'
form_class = forms.UserCheckOtpCodeForm
success_url = reverse_lazy('users:user-otp-settings-success')
def form_valid(self, form):
user = self.request.user
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
user.disable_otp()
user.save()
return super().form_valid(form)
else:
form.add_error('otp_code', _('Otp code invalid'))
return super().form_invalid(form)
class UserOtpSettingsSuccessView(TemplateView):
template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
if is_login(request):
auth_logout(request)
return response
def get_context_data(self, **kwargs):
title, describe = self.get_title_describe()
context = {
'title': title,
'messages': describe,
'interval': 1,
'redirect_url': reverse('users:login'),
'auto_redirect': True,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_title_describe(self):
user = get_user(self.request)
title = _('OTP enable success')
describe = _('OTP enable success, return login page')
if not user.otp_enabled:
title = _('OTP disable success')
describe = _('OTP disable success, return login page')
return title, describe