1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-11-01 03:54:15 +00:00

Merge pull request #378 from Real-Gecko/master

OTP improvements #341
This commit is contained in:
catborise 2020-10-20 13:46:08 +03:00 committed by GitHub
commit 83c8eccde3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 185 additions and 149 deletions

View file

@ -1,6 +1,6 @@
from appsettings.settings import app_settings
from django.contrib.auth import get_user_model
from django.forms import ModelForm, ValidationError
from django.forms import EmailField, Form, ModelForm, ValidationError
from django.utils.translation import gettext_lazy as _
from .models import UserInstance, UserSSHKey
@ -69,3 +69,7 @@ class UserSSHKeyForm(ModelForm):
class Meta:
model = UserSSHKey
fields = ('keyname', 'keypublic')
class EmailOTPForm(Form):
email = EmailField(label=_('Email'))

View file

@ -8,7 +8,15 @@
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}
{% block page_heading_extra %}
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success">
{% if otp_enabled %}
<a href="{% url 'accounts:admin_email_otp' user.id %}" class="btn btn-secondary" title="{% trans "Email OTP QR code" %}">
{% icon 'qrcode' %}
</a>
{% endif %}
<a href="{% url 'admin:user_update' user.id %}?next={% url 'accounts:account' user.id %}" class="btn btn-primary" title="{% trans "Edit user" %}">
{% icon 'pencil' %}
</a>
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success" title="{% trans "Create user instance" %}">
{% icon 'plus' %}
</a>
{% endblock page_heading_extra %}
@ -21,11 +29,6 @@
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
</li>
{% if totp_url %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#otp">{% trans "OTP QR Code" %}</a>
</li>
{% endif %}
</ul>
<div class="tab-content">
@ -82,12 +85,5 @@
</tbody>
</table>
</div>
{% if totp_url %}
<div class="tab-pane fade" id="otp">
<div class="text-center">
{% qr_from_text totp_url image_format="png" %}
</div>
</div>
{% endif %}
</div>
{% endblock content %}

View file

@ -0,0 +1,7 @@
{% load i18n %}
{% load qr_code %}
{% blocktrans %}
Scan this QR code to get OTP for account '{{ user }}'
{% endblocktrans %}
<br>
{% qr_from_text totp_url %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block content %}
<div class="alert alert-info">
{% blocktrans %}
Enter email address OTP QR code will be sent to.
{% endblocktrans %}
</div>
<div class="card">
<div class="card-body">
<form id="create-update" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
</div>
<div class="card-footer">
<div class="form-group mb-0 float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'arrow-left' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% icon 'envelope-o' %} {% trans "Send" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,62 +1,30 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap4 %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block title %}WebVirtCloud{% endblock title %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
<meta name="author" content="anatoliy.guskov@gmail.com">
{% block page_heading %}WebVirtCloud{% endblock page_heading %}
<title>{% trans "WebVirtCloud" %} - {% trans "Sign In" %}</title>
<!-- Bootstrap Core CSS -->
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div>
<div class="page-header text-center">
<a class="" href="/"><h1>WebVirtCloud</h1></a>
</div>
<div class="row">
<div class="col-6 offset-3" role="main">
<div class="card">
<div class="card-body">
{% if form.errors %}
{% bootstrap_form_errors form %}
{% endif %}
<form class="form-signin" method="post" role="form" aria-label="Login form">
{% csrf_token %}
{% bootstrap_field form.username layout='horizontal' %}
{% bootstrap_field form.password layout='horizontal' %}
{% bootstrap_field form.otp_token layout='horizontal' %}
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
</div>
{% block content %}
<div class="row">
<div class="col-6 offset-3" role="main">
<div class="card">
<div class="card-body">
{% if form.errors %}
{% bootstrap_form_errors form %}
{% endif %}
<form class="form-signin" method="post" role="form" aria-label="Login form">
{% csrf_token %}
{% bootstrap_field form.username layout='horizontal' %}
{% bootstrap_field form.password layout='horizontal' %}
{% bootstrap_field form.otp_token layout='horizontal' %}
<a href="{% url 'accounts:email_otp' %}" class="float-right mb-2">{% trans "I do not have/lost my OTP!" %}</a>
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
</div>
<!-- jQuery -->
<script src="{% static "js/jquery.js" %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
</body>
</html>
</div>
{% endblock content %}

View file

@ -1,41 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
<meta name="author" content="anatoliy.guskov@gmail.com">
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap Core CSS -->
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
<!-- SB admin CSS -->
<link href="{% static "css/signin.css" %}" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- jQuery -->
<script src="{% static "js/jquery.js" %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
</body>
</html>

View file

@ -1,25 +1,30 @@
{% extends "base_auth.html" %}
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock %}
{% block style %}
<link href="{% static "css/signin.css" %}" rel="stylesheet">
{% endblock style %}
{% block content %}
<div>
<div class="page-header">
<a class="" href="/"><h1>WebVirtCloud</h1></a>
<div class="page-header">
<a class="" href="/"><h1>WebVirtCloud</h1></a>
</div>
<div class="col-12" role="main">
{% if form.errors %}
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% trans "Incorrect username or password." %}
</div>
<div class="col-12" role="main">
{% if form.errors %}
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% trans "Incorrect username or password." %}
</div>
{% endif %}
<form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
<input type="text" class="form-control" name="username" placeholder="{% trans "Login" %}" autocapitalize="none" autocorrect="off" autofocus>
<input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
{% endif %}
<form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
<input type="text" class="form-control" name="username" placeholder="{% trans "Login" %}" autocapitalize="none" autocorrect="off" autofocus>
<input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
{% endblock %}

View file

@ -20,10 +20,14 @@ urlpatterns = [
]
if settings.OTP_ENABLED:
urlpatterns += path(
'login/',
LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
name='login',
),
urlpatterns += [
path(
'login/',
LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
name='login',
),
path('email_otp/', views.email_otp, name='email_otp'),
path('admin_email_otp/<int:user_id>/', views.admin_email_otp, name='admin_email_otp'),
]
else:
urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),

View file

@ -2,6 +2,9 @@ import base64
import binascii
import struct
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
@ -12,6 +15,9 @@ def get_user_totp_device(user):
if isinstance(device, TOTPDevice):
return device
device = user.totpdevice_set.create()
return device
def validate_ssh_key(key):
array = key.encode().split()
@ -37,3 +43,20 @@ def validate_ssh_key(key):
return True
else:
return False
def send_email_with_otp(user, device):
send_mail(
_('OTP QR Code'),
_('Please view HTML version of this message.'),
None,
[user.email],
html_message=render_to_string(
'accounts/email/otp.html',
{
'totp_url': device.config_url,
'user': user,
},
),
fail_silently=False,
)

View file

@ -1,7 +1,7 @@
from admin.decorators import superuser_only
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import get_object_or_404, redirect, render
@ -9,11 +9,11 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from instances.models import Instance
from accounts.forms import ProfileForm, UserSSHKeyForm
from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
from accounts.models import *
from . import forms
from .utils import get_user_totp_device
from .utils import get_user_totp_device, send_email_with_otp
def profile(request):
@ -65,13 +65,15 @@ def account(request, user_id):
user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by("name")
publickeys = UserSSHKey.objects.filter(user_id=user_id)
if settings.OTP_ENABLED:
device = get_user_totp_device(user)
if not device:
device = user.totpdevice_set.create()
totp_url = device.config_url
return render(request, "account.html", locals())
return render(
request, "account.html", {
'user': user,
'user_insts': user_insts,
'instances': instances,
'publickeys': publickeys,
'otp_enabled': settings.OTP_ENABLED,
})
@permission_required("accounts.change_password", raise_exception=True)
@ -141,3 +143,36 @@ def user_instance_delete(request, pk):
"common/confirm_delete.html",
{"object": user_instance},
)
def email_otp(request):
form = EmailOTPForm(request.POST or None)
if form.is_valid():
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=form.cleaned_data['email'])
except UserModel.DoesNotExist:
pass
else:
device = get_user_totp_device(user)
send_email_with_otp(user, device)
messages.success(request, _('OTP Sent to %s') % form.cleaned_data['email'])
return redirect('accounts:login')
return render(request, 'accounts/email_otp_form.html', {
'form': form,
'title': _('Email OTP'),
})
@superuser_only
def admin_email_otp(request, user_id):
user = get_object_or_404(get_user_model(), pk=user_id)
device = get_user_totp_device(user)
if user.email != '':
send_email_with_otp(user, device)
messages.success(request, _('OTP QR code was emailed to user %s') % user)
else:
messages.error(request, _('User email not set, failed to send QR code'))
return redirect('accounts:account', user.id)

View file

@ -120,12 +120,12 @@ def user_update(request, pk):
user = get_object_or_404(User, pk=pk)
attributes = UserAttributes.objects.get(user=user)
user_form = forms.UserForm(request.POST or None, instance=user)
attributes_form = forms.UserAttributesForm(
request.POST or None, instance=attributes)
attributes_form = forms.UserAttributesForm(request.POST or None, instance=attributes)
if user_form.is_valid() and attributes_form.is_valid():
user_form.save()
attributes_form.save()
return redirect("admin:user_list")
next = request.GET.get('next')
return redirect(next or "admin:user_list")
return render(
request,
@ -146,8 +146,7 @@ def user_update_password(request, pk):
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
messages.success(request, _(
"User password changed: {}".format(user.username)))
messages.success(request, _("User password changed: {}".format(user.username)))
return redirect("admin:user_list")
else:
messages.error(request, _("Wrong Data Provided"))
@ -159,8 +158,8 @@ def user_update_password(request, pk):
"accounts/change_password_form.html",
{
"form": form,
"user": user.username
}
"user": user.username,
},
)

View file

@ -34,7 +34,9 @@
<body>
{% if request.user.is_authenticated %}
{% include 'navbar.html' %}
{% endif %}
<div role="main" class="container">
<div class"row">

View file

@ -183,3 +183,5 @@ SHOW_PROFILE_EDIT_PASSWORD = True
OTP_ENABLED = False
LOGIN_REQUIRED_IGNORE_VIEW_NAMES = ['accounts:email_otp']