Merge branch '19-lockout-prevention' into 'master'
add lockout prevention See merge request BeryJu.org/passbook!27
This commit is contained in:
		
							
								
								
									
										18
									
								
								passbook/core/migrations/0002_nonce_description.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/core/migrations/0002_nonce_description.py
									
									
									
									
									
										Normal 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=''),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -57,6 +57,7 @@ class User(AbstractUser):
 | 
			
		||||
        self.password_change_date = now()
 | 
			
		||||
        return super().set_password(password)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Provider(models.Model):
 | 
			
		||||
    """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
 | 
			
		||||
 | 
			
		||||
@ -70,6 +71,7 @@ class Provider(models.Model):
 | 
			
		||||
            return getattr(self, 'name')
 | 
			
		||||
        return super().__str__()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyModel(UUIDModel, CreatedUpdatedModel):
 | 
			
		||||
    """Base model which can have policies applied to it"""
 | 
			
		||||
 | 
			
		||||
@ -255,21 +257,29 @@ class Invitation(UUIDModel):
 | 
			
		||||
        verbose_name = _('Invitation')
 | 
			
		||||
        verbose_name_plural = _('Invitations')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Nonce(UUIDModel):
 | 
			
		||||
    """One-time link for password resets/sign-up-confirmations"""
 | 
			
		||||
 | 
			
		||||
    expires = models.DateTimeField(default=default_nonce_duration)
 | 
			
		||||
    user = models.ForeignKey('User', on_delete=models.CASCADE)
 | 
			
		||||
    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):
 | 
			
		||||
        return f"Nonce f{self.uuid.hex} (expires={self.expires})"
 | 
			
		||||
        return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _('Nonce')
 | 
			
		||||
        verbose_name_plural = _('Nonces')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PropertyMapping(UUIDModel):
 | 
			
		||||
    """User-defined key -> x mapping which can be used by providers to expose extra data."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/recovery/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/recovery/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										11
									
								
								passbook/recovery/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/recovery/apps.py
									
									
									
									
									
										Normal 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/'
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/recovery/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/recovery/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								passbook/recovery/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/recovery/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										46
									
								
								passbook/recovery/management/commands/create_recovery_key.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								passbook/recovery/management/commands/create_recovery_key.py
									
									
									
									
									
										Normal 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))
 | 
			
		||||
							
								
								
									
										9
									
								
								passbook/recovery/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								passbook/recovery/urls.py
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										24
									
								
								passbook/recovery/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/recovery/views.py
									
									
									
									
									
										Normal 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')
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user