From b9991465ee56a35cec9f3f90114e390160ff723a Mon Sep 17 00:00:00 2001 From: "Langhammer, Jens" Date: Thu, 10 Oct 2019 14:05:16 +0200 Subject: [PATCH] recovery(new): add recovery app to create recovery links --- passbook/recovery/__init__.py | 0 passbook/recovery/apps.py | 11 +++++ passbook/recovery/management/__init__.py | 0 .../recovery/management/commands/__init__.py | 0 .../commands/create_recovery_key.py | 46 +++++++++++++++++++ passbook/recovery/urls.py | 9 ++++ passbook/recovery/views.py | 24 ++++++++++ passbook/root/settings.py | 1 + 8 files changed, 91 insertions(+) create mode 100644 passbook/recovery/__init__.py create mode 100644 passbook/recovery/apps.py create mode 100644 passbook/recovery/management/__init__.py create mode 100644 passbook/recovery/management/commands/__init__.py create mode 100644 passbook/recovery/management/commands/create_recovery_key.py create mode 100644 passbook/recovery/urls.py create mode 100644 passbook/recovery/views.py diff --git a/passbook/recovery/__init__.py b/passbook/recovery/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/apps.py b/passbook/recovery/apps.py new file mode 100644 index 000000000..0a6db1a06 --- /dev/null +++ b/passbook/recovery/apps.py @@ -0,0 +1,11 @@ +"""passbook Recovery app config""" +from django.apps import AppConfig + + +class PassbookRecoveryConfig(AppConfig): + """passbook Recovery app config""" + + name = 'passbook.recovery' + label = 'passbook_recovery' + verbose_name = 'passbook Recovery' + mountpoint = 'recovery/' diff --git a/passbook/recovery/management/__init__.py b/passbook/recovery/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/management/commands/__init__.py b/passbook/recovery/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/management/commands/create_recovery_key.py b/passbook/recovery/management/commands/create_recovery_key.py new file mode 100644 index 000000000..49a3fc3ef --- /dev/null +++ b/passbook/recovery/management/commands/create_recovery_key.py @@ -0,0 +1,46 @@ +"""passbook recovery createkey command""" +from datetime import timedelta +from getpass import getuser + +from django.core.management.base import BaseCommand +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext as _ +from structlog import get_logger + +from passbook.core.models import Nonce, User +from passbook.lib.config import CONFIG + +LOGGER = get_logger() + + +class Command(BaseCommand): + """Create Nonce used to recover access""" + + help = _('Create a Key which can be used to restore access to passbook.') + + def add_arguments(self, parser): + parser.add_argument('duration', default=1, action='store', + help='How long the token is valid for (in years).') + parser.add_argument('user', action='store', + help='Which user the Token gives access to.') + + def get_url(self, nonce: Nonce) -> str: + """Get full recovery link""" + path = reverse('passbook_recovery:use-nonce', kwargs={'uuid': str(nonce.uuid)}) + return f"https://{CONFIG.y('domain')}{path}" + + def handle(self, *args, **options): + """Create Nonce used to recover access""" + duration = int(options.get('duration', 1)) + delta = timedelta(days=duration * 365.2425) + _now = now() + expiry = _now + delta + user = User.objects.get(username=options.get('user')) + nonce = Nonce.objects.create( + expires=expiry, + user=user, + description=f'Recovery Nonce generated by {getuser()} on {_now}') + self.stdout.write((f"Store this link safely, as it will allow" + f" anyone to access passbook as {user}.")) + self.stdout.write(self.get_url(nonce)) diff --git a/passbook/recovery/urls.py b/passbook/recovery/urls.py new file mode 100644 index 000000000..36a3f93f6 --- /dev/null +++ b/passbook/recovery/urls.py @@ -0,0 +1,9 @@ +"""recovery views""" + +from django.urls import path + +from passbook.recovery.views import UseNonceView + +urlpatterns = [ + path('use-nonce//', UseNonceView.as_view(), name='use-nonce'), +] diff --git a/passbook/recovery/views.py b/passbook/recovery/views.py new file mode 100644 index 000000000..fd870138e --- /dev/null +++ b/passbook/recovery/views.py @@ -0,0 +1,24 @@ +"""recovery views""" +from django.contrib import messages +from django.contrib.auth import login +from django.http import Http404, HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext as _ +from django.views import View + +from passbook.core.models import Nonce + + +class UseNonceView(View): + """Use nonce to login""" + + def get(self, request: HttpRequest, uuid: str) -> HttpResponse: + """Check if nonce exists, log user in and delete nonce.""" + nonce: Nonce = get_object_or_404(Nonce, pk=uuid) + if nonce.is_expired: + nonce.delete() + raise Http404 + login(request, nonce.user, backend='django.contrib.auth.backends.ModelBackend') + nonce.delete() + messages.warning(request, _("Used recovery-link to authenticate.")) + return redirect('passbook_core:overview') diff --git a/passbook/root/settings.py b/passbook/root/settings.py index e09dfe6eb..0c7041798 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -72,6 +72,7 @@ INSTALLED_APPS = [ 'passbook.api.apps.PassbookAPIConfig', 'passbook.lib.apps.PassbookLibConfig', 'passbook.audit.apps.PassbookAuditConfig', + 'passbook.recovery.apps.PassbookRecoveryConfig', 'passbook.sources.ldap.apps.PassbookSourceLDAPConfig', 'passbook.sources.oauth.apps.PassbookSourceOAuthConfig',