Commit 0accbbd3 by Kálmán Viktor

dashboard: basic 2fa profile settings

parent a8272957
......@@ -567,3 +567,5 @@ BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
SSHKEY_EMAIL_ADD_KEY = False
TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE")
......@@ -20,6 +20,8 @@ from __future__ import absolute_import
from datetime import timedelta
from urlparse import urlparse
import pyotp
from django.forms import ModelForm
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
......@@ -1298,21 +1300,29 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField(
label=_('Instance limit'),
min_value=0, widget=NumberInput)
two_factor_secret = forms.CharField(
label=_('Two-factor authentication secret'),
help_text=_("Remove the secret key to disable two-factor "
"authentication for this user."), required=False)
def __init__(self, *args, **kwargs):
super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit)
self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret)
class Meta:
model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active')
'is_active', "two_factor_secret", )
def save(self, commit=True):
user = super(UserEditForm, self).save()
user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None)
user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None)
user.profile.save()
return user
......@@ -1650,3 +1660,29 @@ class MessageForm(ModelForm):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
class TwoFactorForm(ModelForm):
class Meta:
model = Profile
fields = ["two_factor_secret", ]
class DisableTwoFactorForm(ModelForm):
confirmation_code = forms.CharField(
label=_('Confirmation code'),
help_text=_("Get the code from your authenticator to disable "
"two-factor authentication."))
def __init__(self, *args, **kwargs):
super(DisableTwoFactorForm, self).__init__(*args, **kwargs)
self.fields['two_factor_secret'].initial = None
class Meta:
model = Profile
fields = ('two_factor_secret', 'confirmation_code', )
def clean_confirmation_code(self):
totp = pyotp.TOTP(self.instance.two_factor_secret)
if not totp.verify(self.cleaned_data.get('confirmation_code')):
raise ValidationError(_("Invalid confirmation code."))
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0004_profile_desktop_notifications'),
]
operations = [
migrations.AddField(
model_name='profile',
name='two_factor_secret',
field=models.CharField(max_length=32, null=True, verbose_name='two factor secret key', blank=True),
),
]
......@@ -200,6 +200,10 @@ class Profile(Model):
verbose_name=_('disk quota'),
default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.'))
two_factor_secret = CharField(
verbose_name=_("two factor secret key"),
max_length=32, null=True, blank=True,
)
def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template."""
......
......@@ -1533,3 +1533,19 @@ textarea[name="new_members"] {
#manage-access-select-all {
cursor: pointer;
}
#two-factor-qr {
text-align: center;
span, small {
display: block;
}
}
#two-factor-confirm {
text-align: center;
button {
margin-left: 15px;
}
}
......@@ -22,6 +22,5 @@ $(function() {
}
});
});
});
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-unlock"></i>
{% trans "Disable two-factor authentication" %}
</h3>
</div>
<div class="panel-body">
<form action="" method="POST">
{% csrf_token %}
<input type="hidden" value="" name="{{ form.two_factor_secret.name }}"/>
{{ form.confirmation_code|as_crispy_field }}
<button type="submit" class="btn btn-warning">
<i class="fa fa-unlock"></i>
{% trans "Disable" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-lock"></i>
{% trans "Enable two-factor authentication" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with lang=LANGUAGE_CODE %}
To use two-factor authentication you need to download Google Authenticator
and use the following qr code, secret key or link to set it up.
If you need help with the download or setup check out the
<a href="https://support.google.com/accounts/answer/1066447?hl={{ lang }}">
official help page.
</a>
{% endblocktrans %}
<hr />
<div id="two-factor-qr">
<span>
{% blocktrans with secret=secret %}
Your secret key is: <strong>{{ secret }}</strong>
{% endblocktrans %}
</span>
<img src="//chart.googleapis.com/chart?chs=255x255&chld=L|0&cht=qr&chl={{ uri }}"/>
<small><a href="{{ uri }}">{{ uri }}</a></small>
</div>
<hr />
<div id="two-factor-confirm">
<form action="" method="POST">
{% csrf_token %}
<input type="hidden" value="{{ secret }}" name="{{ form.two_factor_secret.name }}"/>
{% blocktrans %}
If you managed to set up the authenticator click enable to finalize two-factor
authentication for this account.
{% endblocktrans %}
<button type="submit" class="btn btn-success">
<i class="fa fa-lock"></i>
{% trans "Enable" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -23,6 +23,28 @@
<legend>{% trans "Password change" %}</legend>
{% crispy forms.change_password %}
</fieldset>
<fieldset style="margin-top: 25px;">
<legend>{% trans "Two-factor authentication" %}</legend>
{% if profile.two_factor_secret %}
{% blocktrans %}
Two-factor authentication is currently enabled on your account. To disable it
click the button
{% endblocktrans %}
<a href="{% url "dashboard.views.profile-disable-two-factor" %}" class="btn btn-warning btn-xs">
<i class="fa fa-unlock"></i>
{% trans "Disable" %}
</a>
{% else %}
{% blocktrans %}
Two-factor authentication is currently disabled on your account. To enable it
click the button
{% endblocktrans %}
<a href="{% url "dashboard.views.profile-enable-two-factor" %}" class="btn btn-success btn-xs">
<i class="fa fa-lock"></i>
{% trans "Enable" %}
</a>
{% endif %}
</fieldset>
</div>
<div class="col-md-4" style="margin-bottom: 50px;">
<fieldset>
......
......@@ -55,6 +55,7 @@ from .views import (
UserList,
StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -179,6 +180,10 @@ urlpatterns = patterns(
url(r'^profile/(?P<username>[^/]+)/$', ProfileView.as_view(),
name="dashboard.views.profile"),
url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar),
url(r'^profile/two-factor/enable/$', EnableTwoFactorView.as_view(),
name="dashboard.views.profile-enable-two-factor"),
url(r'^profile/two-factor/disable/$', DisableTwoFactorView.as_view(),
name="dashboard.views.profile-disable-two-factor"),
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(),
......
......@@ -19,6 +19,8 @@ from __future__ import unicode_literals, absolute_import
import json
import logging
import pyotp
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
......@@ -26,9 +28,7 @@ from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login as login_view
from django.contrib.messages.views import SuccessMessageMixin
from django.core import signing
from django.core.exceptions import (
PermissionDenied, SuspiciousOperation,
)
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q
......@@ -51,7 +51,7 @@ from vm.models import Instance, InstanceTemplate
from ..forms import (
CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm,
UserListSearchForm, UserEditForm,
UserListSearchForm, UserEditForm, TwoFactorForm, DisableTwoFactorForm
)
from ..models import Profile, GroupProfile, ConnectCommand
from ..tables import (
......@@ -555,3 +555,37 @@ class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
qs = qs.filter(filters)
return qs.select_related("profile")
class EnableTwoFactorView(LoginRequiredMixin, UpdateView):
model = Profile
form_class = TwoFactorForm
template_name = "dashboard/enable-two-factor.html"
success_url = reverse_lazy("dashboard.views.profile-preferences")
def get_object(self, queryset=None):
if self.request.user.is_anonymous():
raise PermissionDenied
return self.request.user.profile
def get_context_data(self, **kwargs):
ctx = super(EnableTwoFactorView, self).get_context_data(**kwargs)
random_base32 = pyotp.random_base32()
ctx['uri'] = pyotp.TOTP(random_base32).provisioning_uri(
self.request.user.username, issuer_name=settings.TWO_FACTOR_ISSUER)
ctx['secret'] = random_base32
return ctx
class DisableTwoFactorView(LoginRequiredMixin, UpdateView):
model = Profile
form_class = DisableTwoFactorForm
template_name = "dashboard/disable-two-factor.html"
success_url = reverse_lazy("dashboard.views.profile-preferences")
def get_object(self, queryset=None):
if self.request.user.is_anonymous():
raise PermissionDenied
return self.request.user.profile
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment