mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 07:01:12 +00:00
292 lines
10 KiB
Python
292 lines
10 KiB
Python
# Copyright (c) 2012-2016 Seafile Ltd.
|
|
import logging
|
|
from binascii import unhexlify
|
|
from base64 import b32encode
|
|
|
|
from constance import config
|
|
from django.conf import settings
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from django.urls import reverse
|
|
from django.forms import Form
|
|
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
|
from django.shortcuts import redirect
|
|
from django.utils.http import is_safe_url
|
|
from django.utils.module_loading import import_string
|
|
from django.views.decorators.cache import never_cache
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
|
from django.views.generic import FormView, DeleteView, TemplateView
|
|
from django.views.generic.base import View
|
|
|
|
import qrcode
|
|
import qrcode.image.svg
|
|
|
|
from seahub.auth import login as login, REDIRECT_FIELD_NAME
|
|
from seahub.auth.decorators import login_required
|
|
from seahub.auth.forms import AuthenticationForm
|
|
|
|
from seahub.two_factor import login as two_factor_login
|
|
from seahub.two_factor.decorators import otp_required
|
|
from seahub.two_factor.models import (StaticDevice, PhoneDevice,
|
|
get_available_methods, default_device)
|
|
from seahub.two_factor.utils import random_hex, totp_digits, get_otpauth_url
|
|
|
|
from seahub.two_factor.forms import (MethodForm, TOTPDeviceForm,
|
|
PhoneNumberForm, DeviceValidationForm)
|
|
from seahub.two_factor.views.utils import (class_view_decorator,
|
|
CheckTwoFactorEnabledMixin,
|
|
IdempotentSessionWizardView)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
QR_SESSION_KEY = 'django_two_factor-qr_secret_key'
|
|
|
|
@class_view_decorator(never_cache)
|
|
@class_view_decorator(login_required)
|
|
class SetupView(CheckTwoFactorEnabledMixin, IdempotentSessionWizardView):
|
|
|
|
redirect_url = 'two_factor:backup_tokens'
|
|
qrcode_url = 'two_factor:qr'
|
|
template_name = 'two_factor/core/setup.html'
|
|
initial_dict = {}
|
|
form_list = (
|
|
# ('welcome', Form),
|
|
('method', MethodForm),
|
|
('generator', TOTPDeviceForm),
|
|
('sms', PhoneNumberForm),
|
|
('call', PhoneNumberForm),
|
|
('validation', DeviceValidationForm),
|
|
)
|
|
condition_dict = {
|
|
'generator': lambda self: self.get_method() == 'generator',
|
|
'call': lambda self: self.get_method() == 'call',
|
|
'sms': lambda self: self.get_method() == 'sms',
|
|
'validation': lambda self: self.get_method() in ('sms', 'call'),
|
|
}
|
|
|
|
def get_method(self):
|
|
method_data = self.storage.validated_step_data.get('method', {})
|
|
return method_data.get('method', None)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""
|
|
Start the setup wizard. Redirect if already enabled.
|
|
"""
|
|
if default_device(self.request.user):
|
|
return redirect(self.redirect_url)
|
|
return super(SetupView, self).get(request, *args, **kwargs)
|
|
|
|
def get_form_list(self):
|
|
"""
|
|
Check if there is only one method, then skip the MethodForm from form_list
|
|
"""
|
|
form_list = super(SetupView, self).get_form_list()
|
|
available_methods = get_available_methods()
|
|
if len(available_methods) == 1:
|
|
form_list.pop('method', None)
|
|
|
|
# XXX: since we comment out first welcome step, `form_list` will
|
|
# be empty after pop 'method', which will cause index error in
|
|
# `WizardView::get` when reset to the first step, so we have to
|
|
# add our default method to the form list.
|
|
if len(form_list) == 0:
|
|
form_list['generator'] = TOTPDeviceForm
|
|
|
|
method_key, _ = available_methods[0]
|
|
self.storage.validated_step_data['method'] = {'method': method_key}
|
|
return form_list
|
|
|
|
def render_next_step(self, form, **kwargs):
|
|
"""
|
|
In the validation step, ask the device to generate a challenge.
|
|
"""
|
|
next_step = self.steps.__next__
|
|
if next_step == 'validation':
|
|
try:
|
|
self.get_device().generate_challenge()
|
|
kwargs["challenge_succeeded"] = True
|
|
except:
|
|
logger.exception("Could not generate challenge")
|
|
kwargs["challenge_succeeded"] = False
|
|
return super(SetupView, self).render_next_step(form, **kwargs)
|
|
|
|
def done(self, form_list, **kwargs):
|
|
"""
|
|
Finish the wizard. Save all forms and redirect.
|
|
"""
|
|
# TOTPDeviceForm
|
|
if self.get_method() == 'generator':
|
|
form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0]
|
|
device = form.save()
|
|
|
|
# PhoneNumberForm / YubiKeyDeviceForm
|
|
elif self.get_method() in ('call', 'sms', 'yubikey'):
|
|
device = self.get_device()
|
|
device.save()
|
|
|
|
else:
|
|
raise NotImplementedError("Unknown method '%s'" % self.get_method())
|
|
|
|
two_factor_login(self.request, device)
|
|
|
|
device = StaticDevice.get_or_create(self.request.user.username)
|
|
if device.token_set.count() == 0:
|
|
device.generate_tokens()
|
|
|
|
return redirect(self.redirect_url)
|
|
|
|
def get_form_kwargs(self, step=None):
|
|
kwargs = {}
|
|
if step == 'generator':
|
|
kwargs.update({
|
|
'key': self.get_key(step),
|
|
'user': self.request.user,
|
|
})
|
|
if step in ('validation', 'yubikey'):
|
|
kwargs.update({
|
|
'device': self.get_device()
|
|
})
|
|
metadata = self.get_form_metadata(step)
|
|
if metadata:
|
|
kwargs.update({'metadata': metadata, })
|
|
return kwargs
|
|
|
|
def get_device(self, **kwargs):
|
|
"""
|
|
Uses the data from the setup step and generated key to recreate device.
|
|
|
|
Only used for call / sms -- generator uses other procedure.
|
|
"""
|
|
method = self.get_method()
|
|
kwargs = kwargs or {}
|
|
kwargs['name'] = 'default'
|
|
kwargs['user'] = self.request.user
|
|
|
|
if method in ('call', 'sms'):
|
|
kwargs['method'] = method
|
|
kwargs['number'] = self.storage.validated_step_data\
|
|
.get(method, {}).get('number')
|
|
return PhoneDevice(key=self.get_key(method), **kwargs)
|
|
|
|
def get_key(self, step):
|
|
self.storage.extra_data.setdefault('keys', {})
|
|
if step in self.storage.extra_data['keys']:
|
|
return self.storage.extra_data['keys'].get(step)
|
|
key = random_hex(20).decode('ascii')
|
|
self.storage.extra_data['keys'][step] = key
|
|
return key
|
|
|
|
def get_context_data(self, form, **kwargs):
|
|
context = super(SetupView, self).get_context_data(form, **kwargs)
|
|
if self.steps.current == 'generator':
|
|
key = self.get_key('generator')
|
|
rawkey = unhexlify(key.encode('ascii'))
|
|
b32key = b32encode(rawkey).decode('utf-8')
|
|
self.request.session[QR_SESSION_KEY] = b32key
|
|
context.update({'QR_URL': reverse(self.qrcode_url)})
|
|
elif self.steps.current == 'validation':
|
|
context['device'] = self.get_device()
|
|
context['cancel_url'] = reverse('edit_profile')
|
|
return context
|
|
|
|
def process_step(self, form):
|
|
if hasattr(form, 'metadata'):
|
|
self.storage.extra_data.setdefault('forms', {})
|
|
self.storage.extra_data['forms'][
|
|
self.steps.current] = form.metadata
|
|
return super(SetupView, self).process_step(form)
|
|
|
|
def get_form_metadata(self, step):
|
|
self.storage.extra_data.setdefault('forms', {})
|
|
return self.storage.extra_data['forms'].get(step, None)
|
|
|
|
|
|
@class_view_decorator(never_cache)
|
|
@class_view_decorator(otp_required)
|
|
class BackupTokensView(CheckTwoFactorEnabledMixin, FormView):
|
|
"""
|
|
View for listing and generating backup tokens.
|
|
|
|
A user can generate a number of static backup tokens. When the user loses
|
|
its phone, these backup tokens can be used for verification. These backup
|
|
tokens should be stored in a safe location; either in a safe or underneath
|
|
a pillow ;-).
|
|
"""
|
|
form_class = Form
|
|
redirect_url = 'two_factor:backup_tokens'
|
|
template_name = 'two_factor/core/backup_tokens.html'
|
|
number_of_tokens = 10
|
|
|
|
def get_device(self):
|
|
return StaticDevice.get_or_create(self.request.user.username)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(BackupTokensView, self).get_context_data(**kwargs)
|
|
context['device'] = self.get_device()
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
"""
|
|
Delete existing backup codes and generate new ones.
|
|
"""
|
|
device = self.get_device()
|
|
device.token_set.all().delete()
|
|
device.generate_tokens()
|
|
|
|
return redirect(self.redirect_url)
|
|
|
|
|
|
@class_view_decorator(never_cache)
|
|
@class_view_decorator(otp_required)
|
|
class SetupCompleteView(CheckTwoFactorEnabledMixin, TemplateView):
|
|
"""
|
|
View congratulation the user when OTP setup has completed.
|
|
"""
|
|
template_name = 'two_factor/core/setup_complete.html'
|
|
|
|
def get_context_data(self):
|
|
return {'phone_methods': [], }
|
|
|
|
|
|
@class_view_decorator(never_cache)
|
|
@class_view_decorator(login_required)
|
|
class QRGeneratorView(View):
|
|
"""
|
|
View returns an SVG image with the OTP token information
|
|
"""
|
|
http_method_names = ['get']
|
|
default_qr_factory = 'qrcode.image.svg.SvgPathImage'
|
|
|
|
# The qrcode library only supports PNG and SVG for now
|
|
image_content_types = {
|
|
'PNG': 'image/png',
|
|
'SVG': 'image/svg+xml; charset=utf-8',
|
|
}
|
|
|
|
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
|
# Get the data from the session
|
|
if not config.ENABLE_TWO_FACTOR_AUTH:
|
|
raise Http404()
|
|
try:
|
|
key = self.request.session[QR_SESSION_KEY]
|
|
del self.request.session[QR_SESSION_KEY]
|
|
except KeyError:
|
|
raise Http404()
|
|
|
|
# Get data for qrcode
|
|
image_factory_string = getattr(settings, 'TWO_FACTOR_QR_FACTORY',
|
|
self.default_qr_factory)
|
|
image_factory = import_string(image_factory_string)
|
|
content_type = self.image_content_types[image_factory.kind]
|
|
|
|
otpauth_url = get_otpauth_url(
|
|
accountname=self.request.user.username,
|
|
issuer=config.SITE_NAME,
|
|
secret=key,
|
|
digits=totp_digits())
|
|
|
|
# Make and return QR code
|
|
img = qrcode.make(otpauth_url, image_factory=image_factory)
|
|
resp = HttpResponse(content_type=content_type)
|
|
img.save(resp)
|
|
return resp
|