code is now clean but still not working
This commit is contained in:
parent
c1276e9695
commit
b5bc371a04
|
@ -1 +1,2 @@
|
||||||
|
"""passbook"""
|
||||||
__version__ = '0.0.1-alpha'
|
__version__ = '0.0.1-alpha'
|
||||||
|
|
|
@ -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__'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
|
"""passbook admin mixins"""
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue