Merge branch '19-lockout-prevention' into 'master'

add lockout prevention

See merge request BeryJu.org/passbook!27
This commit is contained in:
Jens Langhammer 2019-10-10 12:37:14 +00:00
commit 6446ca8bb2
10 changed files with 120 additions and 1 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2019-10-10 11:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='nonce',
name='description',
field=models.TextField(blank=True, default=''),
),
]

View File

@ -57,6 +57,7 @@ class User(AbstractUser):
self.password_change_date = now() self.password_change_date = now()
return super().set_password(password) return super().set_password(password)
class Provider(models.Model): class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -70,6 +71,7 @@ class Provider(models.Model):
return getattr(self, 'name') return getattr(self, 'name')
return super().__str__() return super().__str__()
class PolicyModel(UUIDModel, CreatedUpdatedModel): class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have policies applied to it""" """Base model which can have policies applied to it"""
@ -255,21 +257,29 @@ class Invitation(UUIDModel):
verbose_name = _('Invitation') verbose_name = _('Invitation')
verbose_name_plural = _('Invitations') verbose_name_plural = _('Invitations')
class Nonce(UUIDModel): class Nonce(UUIDModel):
"""One-time link for password resets/sign-up-confirmations""" """One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration) expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE) user = models.ForeignKey('User', on_delete=models.CASCADE)
expiring = models.BooleanField(default=True) expiring = models.BooleanField(default=True)
description = models.TextField(default='', blank=True)
@property
def is_expired(self) -> bool:
"""Check if nonce is expired yet."""
return now() > self.expires
def __str__(self): def __str__(self):
return f"Nonce f{self.uuid.hex} (expires={self.expires})" return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
class Meta: class Meta:
verbose_name = _('Nonce') verbose_name = _('Nonce')
verbose_name_plural = _('Nonces') verbose_name_plural = _('Nonces')
class PropertyMapping(UUIDModel): class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data.""" """User-defined key -> x mapping which can be used by providers to expose extra data."""

View File

11
passbook/recovery/apps.py Normal file
View File

@ -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/'

View File

View File

@ -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))

View File

@ -0,0 +1,9 @@
"""recovery views"""
from django.urls import path
from passbook.recovery.views import UseNonceView
urlpatterns = [
path('use-nonce/<uuid:uuid>/', UseNonceView.as_view(), name='use-nonce'),
]

View File

@ -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')

View File

@ -72,6 +72,7 @@ INSTALLED_APPS = [
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
'passbook.lib.apps.PassbookLibConfig', 'passbook.lib.apps.PassbookLibConfig',
'passbook.audit.apps.PassbookAuditConfig', 'passbook.audit.apps.PassbookAuditConfig',
'passbook.recovery.apps.PassbookRecoveryConfig',
'passbook.sources.ldap.apps.PassbookSourceLDAPConfig', 'passbook.sources.ldap.apps.PassbookSourceLDAPConfig',
'passbook.sources.oauth.apps.PassbookSourceOAuthConfig', 'passbook.sources.oauth.apps.PassbookSourceOAuthConfig',