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", "") ...@@ -567,3 +567,5 @@ BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "") REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
SSHKEY_EMAIL_ADD_KEY = False SSHKEY_EMAIL_ADD_KEY = False
TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE")
...@@ -20,6 +20,8 @@ from __future__ import absolute_import ...@@ -20,6 +20,8 @@ from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from urlparse import urlparse from urlparse import urlparse
import pyotp
from django.forms import ModelForm from django.forms import ModelForm
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm, AuthenticationForm, PasswordResetForm, SetPasswordForm,
...@@ -1298,21 +1300,29 @@ class UserEditForm(forms.ModelForm): ...@@ -1298,21 +1300,29 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField( instance_limit = forms.IntegerField(
label=_('Instance limit'), label=_('Instance limit'),
min_value=0, widget=NumberInput) 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): def __init__(self, *args, **kwargs):
super(UserEditForm, self).__init__(*args, **kwargs) super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = ( self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit) self.instance.profile.instance_limit)
self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret)
class Meta: class Meta:
model = User model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit', fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active') 'is_active', "two_factor_secret", )
def save(self, commit=True): def save(self, commit=True):
user = super(UserEditForm, self).save() user = super(UserEditForm, self).save()
user.profile.instance_limit = ( user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None) self.cleaned_data['instance_limit'] or None)
user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None)
user.profile.save() user.profile.save()
return user return user
...@@ -1650,3 +1660,29 @@ class MessageForm(ModelForm): ...@@ -1650,3 +1660,29 @@ class MessageForm(ModelForm):
helper = FormHelper() helper = FormHelper()
helper.add_input(Submit("submit", _("Save"))) helper.add_input(Submit("submit", _("Save")))
return helper 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): ...@@ -200,6 +200,10 @@ class Profile(Model):
verbose_name=_('disk quota'), verbose_name=_('disk quota'),
default=2048 * 1024 * 1024, default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.')) 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): def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template.""" """ Generate connection command based on template."""
......
...@@ -1533,3 +1533,19 @@ textarea[name="new_members"] { ...@@ -1533,3 +1533,19 @@ textarea[name="new_members"] {
#manage-access-select-all { #manage-access-select-all {
cursor: pointer; 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() { ...@@ -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 @@ ...@@ -23,6 +23,28 @@
<legend>{% trans "Password change" %}</legend> <legend>{% trans "Password change" %}</legend>
{% crispy forms.change_password %} {% crispy forms.change_password %}
</fieldset> </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>
<div class="col-md-4" style="margin-bottom: 50px;"> <div class="col-md-4" style="margin-bottom: 50px;">
<fieldset> <fieldset>
......
...@@ -55,6 +55,7 @@ from .views import ( ...@@ -55,6 +55,7 @@ from .views import (
UserList, UserList,
StorageDetail, DiskDetail, StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete, MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -179,6 +180,10 @@ urlpatterns = patterns( ...@@ -179,6 +180,10 @@ urlpatterns = patterns(
url(r'^profile/(?P<username>[^/]+)/$', ProfileView.as_view(), url(r'^profile/(?P<username>[^/]+)/$', ProfileView.as_view(),
name="dashboard.views.profile"), name="dashboard.views.profile"),
url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar), 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+)/$', url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(), GroupRemoveUserView.as_view(),
......
...@@ -19,6 +19,8 @@ from __future__ import unicode_literals, absolute_import ...@@ -19,6 +19,8 @@ from __future__ import unicode_literals, absolute_import
import json import json
import logging import logging
import pyotp
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login from django.contrib.auth import login
...@@ -26,9 +28,7 @@ from django.contrib.auth.models import User, Group ...@@ -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.auth.views import login as login_view
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core import signing from django.core import signing
from django.core.exceptions import ( from django.core.exceptions import PermissionDenied, SuspiciousOperation
PermissionDenied, SuspiciousOperation,
)
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.core.paginator import Paginator, InvalidPage from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q from django.db.models import Q
...@@ -51,7 +51,7 @@ from vm.models import Instance, InstanceTemplate ...@@ -51,7 +51,7 @@ from vm.models import Instance, InstanceTemplate
from ..forms import ( from ..forms import (
CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm, CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm, UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm,
UserListSearchForm, UserEditForm, UserListSearchForm, UserEditForm, TwoFactorForm, DisableTwoFactorForm
) )
from ..models import Profile, GroupProfile, ConnectCommand from ..models import Profile, GroupProfile, ConnectCommand
from ..tables import ( from ..tables import (
...@@ -555,3 +555,37 @@ class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView): ...@@ -555,3 +555,37 @@ class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
qs = qs.filter(filters) qs = qs.filter(filters)
return qs.select_related("profile") 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