code is now clean but still not working

This commit is contained in:
Jens Langhammer 2018-11-16 13:08:37 +01:00
parent c1276e9695
commit b5bc371a04
23 changed files with 279 additions and 217 deletions

View File

@ -1 +1,2 @@
"""passbook"""
__version__ = '0.0.1-alpha' __version__ = '0.0.1-alpha'

View File

@ -1,17 +1,18 @@
from django.db.models import Model """passbook admin api utils"""
from rest_framework.serializers import ModelSerializer # from django.db.models import Model
# from rest_framework.serializers import ModelSerializer
class LookupSerializer(ModelSerializer): # class LookupSerializer(ModelSerializer):
mapping = {} # mapping = {}
def to_representation(self, instance): # def to_representation(self, instance):
for __model, __serializer in self.mapping.items(): # for __model, __serializer in self.mapping.items():
if isinstance(instance, __model): # if isinstance(instance, __model):
return __serializer(instance=instance).to_representation(instance) # return __serializer(instance=instance).to_representation(instance)
raise KeyError(instance.__class__.__name__) # raise KeyError(instance.__class__.__name__)
class Meta: # class Meta:
model = Model # model = Model
fields = '__all__' # fields = '__all__'

View File

@ -1,4 +1,4 @@
"""passbook admin mixins"""
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin

View File

@ -1,12 +1,12 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from django.urls import include, path from django.urls import path
from passbook.admin.views import applications, overview, sources from passbook.admin.views import applications, overview, sources
urlpatterns = [ urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'), path('', overview.AdministrationOverviewView.as_view(), name='overview'),
path('applications/', applications.ApplicationListView.as_view(), path('applications/', applications.ApplicationListView.as_view(),
name='applications'), name='applications'),
path('applications/create/', applications.ApplicationCreateView.as_view(), path('applications/create/', applications.ApplicationCreateView.as_view(),
name='application-create'), name='application-create'),
path('sources/', sources.SourceListView.as_view(), path('sources/', sources.SourceListView.as_view(),

View File

@ -1,17 +1,20 @@
"""passbook application administration""" """passbook application administration"""
from django.views.generic import CreateView, DeleteView, ListView, UpdateView from django.views.generic import CreateView, ListView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Application from passbook.core.models import Application
class ApplicationListView(AdminRequiredMixin, ListView): class ApplicationListView(AdminRequiredMixin, ListView):
"""List all applications"""
model = Application model = Application
template_name = 'administration/application/list.html' template_name = 'administration/application/list.html'
class ApplicationCreateView(AdminRequiredMixin, CreateView): class ApplicationCreateView(AdminRequiredMixin, CreateView):
"""Create new application"""
model = Application model = Application
template_name = 'administration/application/create.html' template_name = 'administration/application/create.html'

View File

@ -1,11 +1,12 @@
from django.contrib.auth.mixins import LoginRequiredMixin """passbook administration overview"""
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Application, Rule, User, Provider from passbook.core.models import Application, Provider, Rule, User
class AdministrationOverviewView(AdminRequiredMixin, TemplateView): class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
template_name = 'administration/overview.html' template_name = 'administration/overview.html'

View File

@ -2,7 +2,7 @@
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView from django.views.generic import CreateView, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Source from passbook.core.models import Source
@ -10,6 +10,7 @@ from passbook.lib.utils.reflection import path_to_class
class SourceListView(AdminRequiredMixin, ListView): class SourceListView(AdminRequiredMixin, ListView):
"""Show list of all sources"""
model = Source model = Source
template_name = 'administration/source/list.html' template_name = 'administration/source/list.html'
@ -21,6 +22,7 @@ class SourceListView(AdminRequiredMixin, ListView):
class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Source"""
template_name = 'generic/create.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy('passbook_admin:sources')
@ -33,6 +35,7 @@ class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class SourceUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): class SourceUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update source"""
model = Source model = Source
template_name = 'generic/update.html' template_name = 'generic/update.html'

View File

@ -1,12 +1,13 @@
# Generated by Django 2.1.3 on 2018-11-16 10:21 # Generated by Django 2.1.3 on 2018-11-16 10:21
from django.conf import settings import uuid
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import uuid from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -6,7 +6,7 @@ from django.contrib import messages
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render, reverse from django.shortcuts import redirect, reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import FormView from django.views.generic import FormView
@ -89,14 +89,14 @@ class LoginView(UserPassesTestMixin, FormView):
@staticmethod @staticmethod
def invalid_login(request: HttpRequest, disabled_user: User = None) -> HttpResponse: def invalid_login(request: HttpRequest, disabled_user: User = None) -> HttpResponse:
"""Handle login for disabled users/invalid login attempts""" """Handle login for disabled users/invalid login attempts"""
if disabled_user: # if disabled_user:
context = { # context = {
'reason': 'disabled', # 'reason': 'disabled',
'user': disabled_user # 'user': disabled_user
} # }
else: # else:
context = { # context = {
'reason': 'invalid', # 'reason': 'invalid',
} # }
raise NotImplementedError() raise NotImplementedError()
return render(request, 'login/invalid.html', context) # return render(request, 'login/invalid.html', context)

View File

@ -1,6 +1,6 @@
"""passbook lib config loader""" """passbook lib config loader"""
import os import os
from collections import Mapping from collections.abc import Mapping
from contextlib import contextmanager from contextlib import contextmanager
from glob import glob from glob import glob
from logging import getLogger from logging import getLogger

View File

@ -0,0 +1,46 @@
"""passbook decorators"""
from time import time as timestamp
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.functional import wraps
from django.utils.http import urlencode
RE_AUTH_KEY = getattr(settings, 'RE_AUTH_KEY', 'passbook_require_re_auth_done')
RE_AUTH_MARGAIN = getattr(settings, 'RE_AUTH_MARGAIN', 300)
def reauth_required(view_function):
"""Decorator to force a re-authentication before continuing"""
@wraps(view_function)
def wrap(*args, **kwargs):
"""check if user just authenticated or not"""
request = args[0] if args else None
# Check if user is authenticated at all
if not request or not request.user or not request.user.is_authenticated:
return redirect(reverse('account-login'))
now = timestamp()
if RE_AUTH_KEY in request.session and \
request.session[RE_AUTH_KEY] < (now - RE_AUTH_MARGAIN):
# Timestamp in session but expired
del request.session[RE_AUTH_KEY]
if RE_AUTH_KEY not in request.session:
# Timestamp not in session, force user to reauth
return redirect(reverse('account-reauth') + '?' +
urlencode({'next': request.path}))
if RE_AUTH_KEY in request.session and \
request.session[RE_AUTH_KEY] >= (now - RE_AUTH_MARGAIN) and \
request.session[RE_AUTH_KEY] <= now:
# Timestamp in session and valid
return view_function(*args, **kwargs)
# This should never be reached, just return False
return False # pragma: no cover
return wrap

View File

@ -1,7 +1,7 @@
# Generated by Django 2.1.3 on 2018-11-16 10:21 # Generated by Django 2.1.3 on 2018-11-16 10:21
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,8 +1,8 @@
# Generated by Django 2.1.3 on 2018-11-16 10:21 # Generated by Django 2.1.3 on 2018-11-16 10:21
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -2,7 +2,7 @@
from django.urls import include, path from django.urls import include, path
from passbook.oauth_provider.views import oauth2 # from passbook.oauth_provider.views import oauth2
urlpatterns = [ urlpatterns = [
# Custom OAuth 2 Authorize View # Custom OAuth 2 Authorize View

View File

@ -1,58 +1,58 @@
"""passbook OAuth2 Views""" """passbook OAuth2 Views"""
from logging import getLogger # from logging import getLogger
from django.contrib import messages # from django.contrib import messages
from django.http import Http404, HttpResponseRedirect # from django.http import Http404, HttpResponseRedirect
from django.utils.translation import ugettext as _ # from django.utils.translation import ugettext as _
from oauth2_provider.models import get_application_model # from oauth2_provider.models import get_application_model
from oauth2_provider.views.base import AuthorizationView # from oauth2_provider.views.base import AuthorizationView
# from passbook.core.models import Event, UserAcquirableRelationship # # from passbook.core.models import Event, UserAcquirableRelationship
LOGGER = getLogger(__name__) # LOGGER = getLogger(__name__)
class PassbookAuthorizationView(AuthorizationView): # class PassbookAuthorizationView(AuthorizationView):
"""Custom OAuth2 Authorization View which checks for invite_only products""" # """Custom OAuth2 Authorization View which checks for invite_only products"""
def get(self, request, *args, **kwargs): # def get(self, request, *args, **kwargs):
"""Check if request.user has a relationship with product""" # """Check if request.user has a relationship with product"""
full_res = super().get(request, *args, **kwargs) # full_res = super().get(request, *args, **kwargs)
# If application cannot be found, oauth2_data is {} # # If application cannot be found, oauth2_data is {}
if self.oauth2_data == {}: # if self.oauth2_data == {}:
return full_res # return full_res
# self.oauth2_data['application'] should be set, if not an error occured # # self.oauth2_data['application'] should be set, if not an error occured
# if 'application' in self.oauth2_data: # # if 'application' in self.oauth2_data:
# app = self.oauth2_data['application'] # # app = self.oauth2_data['application']
# if app.productextensionoauth2_set.exists() and \ # # if app.productextensionoauth2_set.exists() and \
# app.productextensionoauth2_set.first().product_set.exists(): # # app.productextensionoauth2_set.first().product_set.exists():
# # Only check if there is a connection from OAuth2 Application to product # # # Only check if there is a connection from OAuth2 Application to product
# product = app.productextensionoauth2_set.first().product_set.first() # # product = app.productextensionoauth2_set.first().product_set.first()
# relationship = UserAcquirableRelationship.objects.filter(user=request.user, # # relationship = UserAcquirableRelationship.objects.filter(user=request.user,
# model=product) # # model=product)
# # Product is invite_only = True and no relation with user exists # # # Product is invite_only = True and no relation with user exists
# if product.invite_only and not relationship.exists(): # # if product.invite_only and not relationship.exists():
# LOGGER.warning("User '%s' has no invitation to '%s'", request.user, product) # # LOGGER.warning("User '%s' has no invitation to '%s'", request.user, product)
# messages.error(request, "You have no access to '%s'" % product.name) # # messages.error(request, "You have no access to '%s'" % product.name)
# raise Http404 # # raise Http404
# if isinstance(full_res, HttpResponseRedirect): # # if isinstance(full_res, HttpResponseRedirect):
# # Application has skip authorization on # # # Application has skip authorization on
# Event.create( # # Event.create(
# user=request.user, # # user=request.user,
# message=_('You authenticated %s (via OAuth) (skipped Authz)' % app.name), # # message=_('You authenticated %s (via OAuth) (skipped Authz)' % app.name),
# request=request, # # request=request,
# current=False, # # current=False,
# hidden=True) # # hidden=True)
return full_res # return full_res
def post(self, request, *args, **kwargs): # def post(self, request, *args, **kwargs):
"""Add event on confirmation""" # """Add event on confirmation"""
app = get_application_model().objects.get(client_id=request.GET["client_id"]) # app = get_application_model().objects.get(client_id=request.GET["client_id"])
# Event.create( # # Event.create(
# user=request.user, # # user=request.user,
# message=_('You authenticated %s (via OAuth)' % app.name), # # message=_('You authenticated %s (via OAuth)' % app.name),
# request=request, # # request=request,
# current=False, # # current=False,
# hidden=True) # # hidden=True)
return super().post(request, *args, **kwargs) # return super().post(request, *args, **kwargs)

View File

@ -84,7 +84,8 @@ class Processor:
'AUTH_INSTANT': get_time_string(), 'AUTH_INSTANT': get_time_string(),
'ISSUE_INSTANT': get_time_string(), 'ISSUE_INSTANT': get_time_string(),
'NOT_BEFORE': get_time_string(-1 * HOURS), # TODO: Make these settings. 'NOT_BEFORE': get_time_string(-1 * HOURS), # TODO: Make these settings.
'NOT_ON_OR_AFTER': get_time_string(int(CONFIG.y('saml_idp.assertion_valid_for')) * MINUTES), 'NOT_ON_OR_AFTER': get_time_string(int(CONFIG.y('saml_idp.assertion_valid_for'))
* MINUTES),
'SESSION_INDEX': self._session_index, 'SESSION_INDEX': self._session_index,
'SESSION_NOT_ON_OR_AFTER': get_time_string(8 * HOURS), 'SESSION_NOT_ON_OR_AFTER': get_time_string(8 * HOURS),
'SP_NAME_QUALIFIER': self._audience, 'SP_NAME_QUALIFIER': self._audience,

View File

@ -1,7 +1,7 @@
# Generated by Django 2.1.3 on 2018-11-16 10:21 # Generated by Django 2.1.3 on 2018-11-16 10:21
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -21,3 +21,6 @@ class SAMLApplication(Application):
def __str__(self): def __str__(self):
return "SAMLApplication %s (processor=%s)" % (self.name, self.processor_path) return "SAMLApplication %s (processor=%s)" % (self.name, self.processor_path)
def user_is_authorized(self):
raise NotImplementedError()

View File

@ -1,6 +1,6 @@
"""Shibboleth Processor""" """Shibboleth Processor"""
from supervisr.mod.auth.saml.idp.base import Processor from passbook.saml_idp.base import Processor
class ShibbolethProcessor(Processor): class ShibbolethProcessor(Processor):

View File

@ -1,28 +1,31 @@
"""passbook SAML IDP Views""" """passbook SAML IDP Views"""
from logging import getLogger from logging import getLogger
from django.contrib import auth, messages from django.contrib import auth
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.http import (Http404, HttpResponse, HttpResponseBadRequest, from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseRedirect) HttpResponseRedirect)
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.utils.html import escape # from django.utils.html import escape
from django.utils.translation import ugettext as _ # from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from OpenSSL.crypto import FILETYPE_PEM
from OpenSSL.crypto import Error as CryptoError
from OpenSSL.crypto import load_certificate
from passbook.lib.config import CONFIG
# from passbook.core.models import Event, Setting, UserAcquirableRelationship # from passbook.core.models import Event, Setting, UserAcquirableRelationship
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
# from passbook.core.views.common import ErrorResponseView # from passbook.core.views.common import ErrorResponseView
# from passbook.core.views.settings import GenericSettingView # from passbook.core.views.settings import GenericSettingView
from passbook.saml_idp import exceptions, registry, xml_signing from passbook.saml_idp import exceptions, registry, xml_signing
# from OpenSSL.crypto import FILETYPE_PEM
# from OpenSSL.crypto import Error as CryptoError
# from OpenSSL.crypto import load_certificate
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
URL_VALIDATOR = URLValidator(schemes=('http', 'https')) URL_VALIDATOR = URLValidator(schemes=('http', 'https'))
@ -82,25 +85,25 @@ def login_process(request):
proc, remote = registry.find_processor(request) proc, remote = registry.find_processor(request)
# Check if user has access # Check if user has access
access = True access = True
if remote.productextensionsaml2_set.exists() and \ # if remote.productextensionsaml2_set.exists() and \
remote.productextensionsaml2_set.first().product_set.exists(): # remote.productextensionsaml2_set.first().product_set.exists():
# Only check if there is a connection from OAuth2 Application to product # # Only check if there is a connection from OAuth2 Application to product
product = remote.productextensionsaml2_set.first().product_set.first() # product = remote.productextensionsaml2_set.first().product_set.first()
relationship = UserAcquirableRelationship.objects.filter(user=request.user, model=product) # relationship = UserAcquirableRelationship.objects.filter(user=request.user, model=product)
# Product is invite_only = True and no relation with user exists # # Product is invite_only = True and no relation with user exists
if product.invite_only and not relationship.exists(): # if product.invite_only and not relationship.exists():
access = False # access = False
# Check if we should just autosubmit # Check if we should just autosubmit
if remote.skip_authorization and access: if remote.skip_authorization and access:
# full_res = _generate_response(request, proc, remote) # full_res = _generate_response(request, proc, remote)
ctx = proc.generate_response() ctx = proc.generate_response()
# User accepted request # User accepted request
Event.create( # Event.create(
user=request.user, # user=request.user,
message=_('You authenticated %s (via SAML) (skipped Authz)' % remote.name), # message=_('You authenticated %s (via SAML) (skipped Authz)' % remote.name),
request=request, # request=request,
current=False, # current=False,
hidden=True) # hidden=True)
return redirect_to_sp( return redirect_to_sp(
request=request, request=request,
acs_url=ctx['acs_url'], acs_url=ctx['acs_url'],
@ -108,12 +111,12 @@ def login_process(request):
relay_state=ctx['relay_state']) relay_state=ctx['relay_state'])
if request.method == 'POST' and request.POST.get('ACSUrl', None) and access: if request.method == 'POST' and request.POST.get('ACSUrl', None) and access:
# User accepted request # User accepted request
Event.create( # Event.create(
user=request.user, # user=request.user,
message=_('You authenticated %s (via SAML)' % remote.name), # message=_('You authenticated %s (via SAML)' % remote.name),
request=request, # request=request,
current=False, # current=False,
hidden=True) # hidden=True)
return redirect_to_sp( return redirect_to_sp(
request=request, request=request,
acs_url=request.POST.get('ACSUrl'), acs_url=request.POST.get('ACSUrl'),
@ -121,13 +124,14 @@ def login_process(request):
relay_state=request.POST.get('RelayState')) relay_state=request.POST.get('RelayState'))
try: try:
full_res = _generate_response(request, proc, remote) full_res = _generate_response(request, proc, remote)
if not access: # if not access:
LOGGER.warning("User '%s' has no invitation to '%s'", request.user, product) # LOGGER.warning("User '%s' has no invitation to '%s'", request.user, product)
messages.error(request, "You have no access to '%s'" % product.name) # messages.error(request, "You have no access to '%s'" % product.name)
raise Http404 # raise Http404
return full_res return full_res
except exceptions.CannotHandleAssertion as exc: except exceptions.CannotHandleAssertion as exc:
return ErrorResponseView.as_view()(request, str(exc)) LOGGER.debug(exc)
# return ErrorResponseView.as_view()(request, str(exc))
@csrf_exempt @csrf_exempt

View File

@ -6,7 +6,7 @@ from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from passbook.core.views import common from passbook.core.views import overview
from passbook.tfa.middleware import tfa_force_verify from passbook.tfa.middleware import tfa_force_verify
@ -19,7 +19,7 @@ class TestMiddleware(TestCase):
def test_tfa_force_verify_anon(self): def test_tfa_force_verify_anon(self):
"""Test Anonymous TFA Force""" """Test Anonymous TFA Force"""
request = self.factory.get(reverse('common-index')) request = self.factory.get(reverse('passbook_core:overview'))
request.user = AnonymousUser() request.user = AnonymousUser()
response = tfa_force_verify(common.IndexView.as_view())(request) response = tfa_force_verify(overview.OverviewView.as_view())(request)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)

View File

@ -8,7 +8,7 @@ urlpatterns = [
url(r'^$', views.index, name='tfa-index'), url(r'^$', views.index, name='tfa-index'),
url(r'qr/$', views.qr_code, name='tfa-qr'), url(r'qr/$', views.qr_code, name='tfa-qr'),
url(r'verify/$', views.verify, name='tfa-verify'), url(r'verify/$', views.verify, name='tfa-verify'),
url(r'enable/$', views.TFASetupView.as_view(), name='tfa-enable'), # url(r'enable/$', views.TFASetupView.as_view(), name='tfa-enable'),
url(r'disable/$', views.disable, name='tfa-disable'), url(r'disable/$', views.disable, name='tfa-disable'),
url(r'user_settings/$', views.user_settings, name='tfa-user_settings'), url(r'user_settings/$', views.user_settings, name='tfa-user_settings'),
] ]

View File

@ -1,13 +1,12 @@
"""passbook 2FA Views""" """passbook 2FA Views"""
from base64 import b32encode # from base64 import b32encode
from binascii import unhexlify # from binascii import unhexlify
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django_otp import login, match_token, user_has_device from django_otp import login, match_token, user_has_device
@ -17,14 +16,13 @@ from django_otp.plugins.otp_totp.models import TOTPDevice
from qrcode import make as qr_make from qrcode import make as qr_make
from qrcode.image.svg import SvgPathImage from qrcode.image.svg import SvgPathImage
from passbook.core.decorators import reauth_required from passbook.lib.decorators import reauth_required
from passbook.core.models import Event # from passbook.core.models import Event
from passbook.core.views.wizards import BaseWizardView # from passbook.core.views.wizards import BaseWizardView
from passbook.mod.tfa.forms import (TFASetupInitForm, TFASetupStaticForm, from passbook.tfa.forms import TFAVerifyForm
TFAVerifyForm) from passbook.tfa.utils import otpauth_url
from passbook.mod.tfa.utils import otpauth_url
TFA_SESSION_KEY = 'passbook_mod_2fa_key' TFA_SESSION_KEY = 'passbook_2fa_key'
@login_required @login_required
@ -96,99 +94,99 @@ def disable(request: HttpRequest) -> HttpResponse:
token.delete() token.delete()
messages.success(request, 'Successfully disabled 2FA') messages.success(request, 'Successfully disabled 2FA')
# Create event with email notification # Create event with email notification
Event.create( # Event.create(
user=request.user, # user=request.user,
message=_('You disabled 2FA.'), # message=_('You disabled 2FA.'),
current=True, # current=True,
request=request, # request=request,
send_notification=True) # send_notification=True)
return redirect(reverse('common-index')) return redirect(reverse('common-index'))
# pylint: disable=too-many-ancestors # # pylint: disable=too-many-ancestors
@method_decorator([login_required, reauth_required], name="dispatch") # @method_decorator([login_required, reauth_required], name="dispatch")
class TFASetupView(BaseWizardView): # class TFASetupView(BaseWizardView):
"""Wizard to create a Mail Account""" # """Wizard to create a Mail Account"""
title = _('Set up 2FA') # title = _('Set up 2FA')
form_list = [TFASetupInitForm, TFASetupStaticForm] # form_list = [TFASetupInitForm, TFASetupStaticForm]
totp_device = None # totp_device = None
static_device = None # static_device = None
confirmed = False # confirmed = False
def get_template_names(self): # def get_template_names(self):
if self.steps.current == '1': # if self.steps.current == '1':
return 'tfa/wizard_setup_static.html' # return 'tfa/wizard_setup_static.html'
return self.template_name # return self.template_name
def handle_request(self, request: HttpRequest): # def handle_request(self, request: HttpRequest):
# Check if user has 2FA setup already # # Check if user has 2FA setup already
finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True) # finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True) # finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
if finished_totp_devices.exists() or finished_static_devices.exists(): # if finished_totp_devices.exists() or finished_static_devices.exists():
messages.error(request, _('You already have 2FA enabled!')) # messages.error(request, _('You already have 2FA enabled!'))
return redirect(reverse('common-index')) # return redirect(reverse('common-index'))
# Check if there's an unconfirmed device left to set up # # Check if there's an unconfirmed device left to set up
totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False) # totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
if not totp_devices.exists(): # if not totp_devices.exists():
# Create new TOTPDevice and save it, but not confirm it # # Create new TOTPDevice and save it, but not confirm it
self.totp_device = TOTPDevice(user=request.user, confirmed=False) # self.totp_device = TOTPDevice(user=request.user, confirmed=False)
self.totp_device.save() # self.totp_device.save()
else: # else:
self.totp_device = totp_devices.first() # self.totp_device = totp_devices.first()
# Check if we have a static device already # # Check if we have a static device already
static_devices = StaticDevice.objects.filter(user=request.user, confirmed=False) # static_devices = StaticDevice.objects.filter(user=request.user, confirmed=False)
if not static_devices.exists(): # if not static_devices.exists():
# Create new static device and some codes # # Create new static device and some codes
self.static_device = StaticDevice(user=request.user, confirmed=False) # self.static_device = StaticDevice(user=request.user, confirmed=False)
self.static_device.save() # self.static_device.save()
# Create 9 tokens and save them # # Create 9 tokens and save them
# pylint: disable=unused-variable # # pylint: disable=unused-variable
for counter in range(0, 9): # for counter in range(0, 9):
token = StaticToken(device=self.static_device, token=StaticToken.random_token()) # token = StaticToken(device=self.static_device, token=StaticToken.random_token())
token.save() # token.save()
else: # else:
self.static_device = static_devices.first() # self.static_device = static_devices.first()
# Somehow convert the generated key to base32 for the QR code # # Somehow convert the generated key to base32 for the QR code
rawkey = unhexlify(self.totp_device.key.encode('ascii')) # rawkey = unhexlify(self.totp_device.key.encode('ascii'))
request.session[TFA_SESSION_KEY] = b32encode(rawkey).decode("utf-8") # request.session[TFA_SESSION_KEY] = b32encode(rawkey).decode("utf-8")
return True # return True
def get_form(self, step=None, data=None, files=None): # def get_form(self, step=None, data=None, files=None):
form = super(TFASetupView, self).get_form(step, data, files) # form = super(TFASetupView, self).get_form(step, data, files)
if step is None: # if step is None:
step = self.steps.current # step = self.steps.current
if step == '0': # if step == '0':
form.confirmed = self.confirmed # form.confirmed = self.confirmed
form.device = self.totp_device # form.device = self.totp_device
form.fields['qr_code'].initial = reverse('passbook_mod_tfa:tfa-qr') # form.fields['qr_code'].initial = reverse('passbook_tfa:tfa-qr')
elif step == '1': # elif step == '1':
# This is a bit of a hack, but the 2fa token from step 1 has been checked here # # This is a bit of a hack, but the 2fa token from step 1 has been checked here
# And we need to save it, otherwise it's going to fail in render_done # # And we need to save it, otherwise it's going to fail in render_done
# and we're going to be redirected to step0 # # and we're going to be redirected to step0
self.confirmed = True # self.confirmed = True
tokens = [(x.token, x.token) for x in self.static_device.token_set.all()] # tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
form.fields['tokens'].choices = tokens # form.fields['tokens'].choices = tokens
return form # return form
def finish(self, *forms): # def finish(self, *forms):
# Save device as confirmed # # Save device as confirmed
self.totp_device.confirmed = True # self.totp_device.confirmed = True
self.totp_device.save() # self.totp_device.save()
self.static_device.confirmed = True # self.static_device.confirmed = True
self.static_device.save() # self.static_device.save()
# Create event with email notification # # Create event with email notification
Event.create( # Event.create(
user=self.request.user, # user=self.request.user,
message=_('You activated 2FA.'), # message=_('You activated 2FA.'),
current=True, # current=True,
request=self.request, # request=self.request,
send_notification=True) # send_notification=True)
return redirect(reverse('passbook_mod_tfa:tfa-index')) # return redirect(reverse('passbook_tfa:tfa-index'))
@never_cache @never_cache
@ -199,7 +197,7 @@ def qr_code(request: HttpRequest) -> HttpResponse:
try: try:
key = request.session[TFA_SESSION_KEY] key = request.session[TFA_SESSION_KEY]
except KeyError: except KeyError:
raise Http404() raise Http404
url = otpauth_url(accountname=request.user.username, secret=key) url = otpauth_url(accountname=request.user.username, secret=key)
# Make and return QR code # Make and return QR code