Compare commits
	
		
			27 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4439378fd4 | |||
| acf65eafdd | |||
| c2ebff55ef | |||
| 99c82676b6 | |||
| 4991e9b825 | |||
| 612f95c3ba | |||
| cd91d5ca15 | |||
| cbbbb5dc08 | |||
| c1640b9411 | |||
| a4842c1f95 | |||
| a4707ddc54 | |||
| fb82d56307 | |||
| 1a1005f80d | |||
| e86cae6cac | |||
| 0b282f45e0 | |||
| 791e88ffc1 | |||
| 7bd3c4bccf | |||
| 722e2e4050 | |||
| c7fc444c95 | |||
| 20ad062814 | |||
| fcb5d36e07 | |||
| 9b131b619f | |||
| 54427f7c68 | |||
| 35eef9c28d | |||
| e88a82553d | |||
| 01a9520140 | |||
| 46667615c3 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.1.1-beta
 | 
			
		||||
current_version = 0.1.2-beta
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
@ -9,6 +9,7 @@ tag_name = version/{new_version}
 | 
			
		||||
 | 
			
		||||
[bumpversion:part:release]
 | 
			
		||||
optional_value = stable
 | 
			
		||||
first_value = beta
 | 
			
		||||
values = 
 | 
			
		||||
	alpha
 | 
			
		||||
	beta
 | 
			
		||||
@ -36,6 +37,10 @@ values =
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:passbook/lib/__init__.py]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:passbook/hibp_policy/__init__.py]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:passbook/password_expiry_policy/__init__.py]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:passbook/saml_idp/__init__.py]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:passbook/audit/__init__.py]
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ package-docker:
 | 
			
		||||
  before_script:
 | 
			
		||||
    - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
 | 
			
		||||
  script:
 | 
			
		||||
    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.1-beta
 | 
			
		||||
    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.2-beta
 | 
			
		||||
  stage: build
 | 
			
		||||
  only:
 | 
			
		||||
    - tags
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
apiVersion: v1
 | 
			
		||||
appVersion: "0.1.1-beta"
 | 
			
		||||
appVersion: "0.1.2-beta"
 | 
			
		||||
description: A Helm chart for passbook.
 | 
			
		||||
name: passbook
 | 
			
		||||
version: "0.1.1-beta"
 | 
			
		||||
version: "0.1.2-beta"
 | 
			
		||||
icon: https://passbook.beryju.org/images/logo.png
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
replicaCount: 1
 | 
			
		||||
 | 
			
		||||
image:
 | 
			
		||||
  tag: 0.1.1-beta
 | 
			
		||||
  tag: 0.1.2-beta
 | 
			
		||||
 | 
			
		||||
nameOverride: ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook admin"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								passbook/admin/forms/users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								passbook/admin/forms/users.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
"""passbook administrative user forms"""
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserForm(forms.ModelForm):
 | 
			
		||||
    """Update User Details"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = User
 | 
			
		||||
        fields = ['username', 'name', 'email', 'is_staff', 'is_active']
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'name': forms.TextInput
 | 
			
		||||
        }
 | 
			
		||||
@ -76,9 +76,13 @@
 | 
			
		||||
            <div class="card-pf-body">
 | 
			
		||||
                <p class="card-pf-aggregate-status-notifications">
 | 
			
		||||
                    <span class="card-pf-aggregate-status-notification">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:factors' %}">
 | 
			
		||||
                        {% if factor_count < 1 %}
 | 
			
		||||
                        <span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
 | 
			
		||||
                            title="{% trans 'No Factors configured. No Users will be able to login.' %}"></span>
 | 
			
		||||
                        {{ factor_count }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <span class="pficon pficon-ok"></span>{{ factor_count }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -95,9 +99,13 @@
 | 
			
		||||
            <div class="card-pf-body">
 | 
			
		||||
                <p class="card-pf-aggregate-status-notifications">
 | 
			
		||||
                    <span class="card-pf-aggregate-status-notification">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:policies' %}">
 | 
			
		||||
                        {% if policies_without_attachment > 0 %}
 | 
			
		||||
                        <span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
 | 
			
		||||
                            title="{% trans 'Policies without attachment exist.' %}"></span>
 | 
			
		||||
                        {{ policy_count }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <span class="pficon pficon-ok"></span>{{ policy_count }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -174,7 +182,7 @@
 | 
			
		||||
                        <a href="#">
 | 
			
		||||
                            {% if worker_count < 1%}
 | 
			
		||||
                            <span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
 | 
			
		||||
                                title="{% trans 'No workers connected. Policies may not work.' %}"></span> {{ worker_count }}
 | 
			
		||||
                                title="{% trans 'No workers connected. Policies will not work and you may expect other issues.' %}"></span> {{ worker_count }}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <span class="pficon pficon-ok"></span>{{ worker_count }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@
 | 
			
		||||
    <table class="table table-striped table-bordered">
 | 
			
		||||
        <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th></th>
 | 
			
		||||
                <th>{% trans 'Name' %}</th>
 | 
			
		||||
                <th>{% trans 'Type' %}</th>
 | 
			
		||||
                <th></th>
 | 
			
		||||
@ -35,7 +36,14 @@
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
            {% for policy in object_list %}
 | 
			
		||||
            <tr>
 | 
			
		||||
            <tr {% if not policy.policymodel_set.exists %} class="warning" {% endif %}>
 | 
			
		||||
                <th>
 | 
			
		||||
                    {% if not policy.policymodel_set.exists %}
 | 
			
		||||
                    <span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Policy is not assigned.' %}"></span>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <span class="pficon-ok" data-toggle="tooltip" data-placement="right" title="{% blocktrans with objects=policy.policymodel_set.all|join:', ' %}Assigned to objects {{ objects }}{% endblocktrans %}"></span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </th>
 | 
			
		||||
                <td>{{ policy.name }}</td>
 | 
			
		||||
                <td>{{ policy|verbose_name }}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
 | 
			
		||||
@ -24,4 +24,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
        kwargs['version'] = __version__
 | 
			
		||||
        kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
 | 
			
		||||
        kwargs['providers_without_application'] = Provider.objects.filter(application=None)
 | 
			
		||||
        kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
@ -7,8 +7,8 @@ from django.utils.translation import ugettext as _
 | 
			
		||||
from django.views import View
 | 
			
		||||
from django.views.generic import DeleteView, ListView, UpdateView
 | 
			
		||||
 | 
			
		||||
from passbook.admin.forms.users import UserForm
 | 
			
		||||
from passbook.admin.mixins import AdminRequiredMixin
 | 
			
		||||
from passbook.core.forms.users import UserDetailForm
 | 
			
		||||
from passbook.core.models import Nonce, User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
 | 
			
		||||
    """Update user"""
 | 
			
		||||
 | 
			
		||||
    model = User
 | 
			
		||||
    form_class = UserDetailForm
 | 
			
		||||
    form_class = UserForm
 | 
			
		||||
 | 
			
		||||
    template_name = 'generic/update.html'
 | 
			
		||||
    success_url = reverse_lazy('passbook_admin:users')
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook api"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook audit Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
"""passbook audit models"""
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@ -7,11 +6,10 @@ from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
from django.contrib.postgres.fields import JSONField
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
 | 
			
		||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
 | 
			
		||||
from passbook.lib.models import UUIDModel
 | 
			
		||||
 | 
			
		||||
LOGGER = getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@ -75,43 +73,3 @@ class AuditEntry(UUIDModel):
 | 
			
		||||
 | 
			
		||||
        verbose_name = _('Audit Entry')
 | 
			
		||||
        verbose_name_plural = _('Audit Entries')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginAttempt(CreatedUpdatedModel):
 | 
			
		||||
    """Track failed login-attempts"""
 | 
			
		||||
 | 
			
		||||
    target_uid = models.CharField(max_length=254)
 | 
			
		||||
    request_ip = models.GenericIPAddressField()
 | 
			
		||||
    attempts = models.IntegerField(default=1)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def attempt(target_uid, request):
 | 
			
		||||
        """Helper function to create attempt or count up existing one"""
 | 
			
		||||
        if not target_uid:
 | 
			
		||||
            return
 | 
			
		||||
        client_ip, _ = get_client_ip(request)
 | 
			
		||||
        # Since we can only use 254 chars for target_uid, truncate target_uid.
 | 
			
		||||
        target_uid = target_uid[:254]
 | 
			
		||||
        time_threshold = timezone.now() - timedelta(minutes=10)
 | 
			
		||||
        existing_attempts = LoginAttempt.objects.filter(
 | 
			
		||||
            target_uid=target_uid,
 | 
			
		||||
            request_ip=client_ip,
 | 
			
		||||
            last_updated__gt=time_threshold).order_by('created')
 | 
			
		||||
        if existing_attempts.exists():
 | 
			
		||||
            attempt = existing_attempts.first()
 | 
			
		||||
            attempt.attempts += 1
 | 
			
		||||
            attempt.save()
 | 
			
		||||
            LOGGER.debug("Increased attempts on %s", attempt)
 | 
			
		||||
        else:
 | 
			
		||||
            attempt = LoginAttempt.objects.create(
 | 
			
		||||
                target_uid=target_uid,
 | 
			
		||||
                request_ip=client_ip)
 | 
			
		||||
            LOGGER.debug("Created new attempt %s", attempt)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "LoginAttempt to %s from %s (x%d)" % (self.target_uid,
 | 
			
		||||
                                                     self.request_ip, self.attempts)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        unique_together = (('target_uid', 'request_ip', 'created'),)
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
django-ipware
 | 
			
		||||
@ -1,9 +1,8 @@
 | 
			
		||||
"""passbook audit signal listener"""
 | 
			
		||||
from django.contrib.auth.signals import (user_logged_in, user_logged_out,
 | 
			
		||||
                                         user_login_failed)
 | 
			
		||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
 | 
			
		||||
from passbook.audit.models import AuditEntry, LoginAttempt
 | 
			
		||||
from passbook.audit.models import AuditEntry
 | 
			
		||||
from passbook.core.signals import (invitation_created, invitation_used,
 | 
			
		||||
                                   user_signed_up)
 | 
			
		||||
 | 
			
		||||
@ -34,8 +33,3 @@ def on_invitation_used(sender, request, invitation, **kwargs):
 | 
			
		||||
    """Log Invitation usage"""
 | 
			
		||||
    AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request,
 | 
			
		||||
                      invitation_uuid=invitation.uuid.hex)
 | 
			
		||||
 | 
			
		||||
@receiver(user_login_failed)
 | 
			
		||||
def on_user_login_failed(sender, request, credentials, **kwargs):
 | 
			
		||||
    """Log failed login attempt"""
 | 
			
		||||
    LoginAttempt.attempt(target_uid=credentials.get('username'), request=request)
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook captcha_factor Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook core"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ class AuthenticationView(UserPassesTestMixin, View):
 | 
			
		||||
            self.pending_factors = []
 | 
			
		||||
            for factor in _all_factors:
 | 
			
		||||
                policy_engine = PolicyEngine(factor.policies.all())
 | 
			
		||||
                policy_engine.for_user(self.pending_user)
 | 
			
		||||
                policy_engine.for_user(self.pending_user).with_request(request).build()
 | 
			
		||||
                if policy_engine.result[0]:
 | 
			
		||||
                    self.pending_factors.append((factor.uuid.hex, factor.type))
 | 
			
		||||
        # Read and instantiate factor from session
 | 
			
		||||
 | 
			
		||||
@ -5,9 +5,8 @@ import os
 | 
			
		||||
 | 
			
		||||
import celery
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
# from raven import Client
 | 
			
		||||
# from raven.contrib.celery import register_logger_signal, register_signal
 | 
			
		||||
from raven import Client
 | 
			
		||||
from raven.contrib.celery import register_logger_signal, register_signal
 | 
			
		||||
 | 
			
		||||
# set the default Django settings module for the 'celery' program.
 | 
			
		||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")
 | 
			
		||||
@ -18,16 +17,17 @@ LOGGER = logging.getLogger(__name__)
 | 
			
		||||
class Celery(celery.Celery):
 | 
			
		||||
    """Custom Celery class with Raven configured"""
 | 
			
		||||
 | 
			
		||||
    # def on_configure(self):
 | 
			
		||||
    #     """Update raven client"""
 | 
			
		||||
    #     try:
 | 
			
		||||
    #         client = Client(settings.RAVEN_CONFIG.get('dsn'))
 | 
			
		||||
    #         # register a custom filter to filter out duplicate logs
 | 
			
		||||
    #         register_logger_signal(client)
 | 
			
		||||
    #         # hook into the Celery error handler
 | 
			
		||||
    #         register_signal(client)
 | 
			
		||||
    #     except RecursionError:  # This error happens when pdoc is running
 | 
			
		||||
    #         pass
 | 
			
		||||
    # pylint: disable=method-hidden
 | 
			
		||||
    def on_configure(self):
 | 
			
		||||
        """Update raven client"""
 | 
			
		||||
        try:
 | 
			
		||||
            client = Client(settings.RAVEN_CONFIG.get('dsn'))
 | 
			
		||||
            # register a custom filter to filter out duplicate logs
 | 
			
		||||
            register_logger_signal(client)
 | 
			
		||||
            # hook into the Celery error handler
 | 
			
		||||
            register_signal(client)
 | 
			
		||||
        except RecursionError:  # This error happens when pdoc is running
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
 | 
			
		||||
@ -81,8 +81,6 @@ class SignUpForm(forms.Form):
 | 
			
		||||
        password_repeat = self.cleaned_data.get('password_repeat')
 | 
			
		||||
        if password != password_repeat:
 | 
			
		||||
            raise ValidationError(_("Passwords don't match"))
 | 
			
		||||
        # TODO: Password policy? Via Plugin? via Policy?
 | 
			
		||||
        # return check_password(self)
 | 
			
		||||
        return self.cleaned_data.get('password_repeat')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -91,5 +89,6 @@ class PasswordFactorForm(forms.Form):
 | 
			
		||||
 | 
			
		||||
    password = forms.CharField(widget=forms.PasswordInput(attrs={
 | 
			
		||||
        'placeholder': _('Password'),
 | 
			
		||||
        'autofocus': 'autofocus'
 | 
			
		||||
        'autofocus': 'autofocus',
 | 
			
		||||
        'autocomplete': 'current-password'
 | 
			
		||||
        }))
 | 
			
		||||
 | 
			
		||||
@ -22,10 +22,14 @@ class PasswordChangeForm(forms.Form):
 | 
			
		||||
    """Form to update password"""
 | 
			
		||||
 | 
			
		||||
    password = forms.CharField(label=_('Password'),
 | 
			
		||||
                               widget=forms.PasswordInput(attrs={'placeholder': _('New Password')}))
 | 
			
		||||
                               widget=forms.PasswordInput(attrs={
 | 
			
		||||
                                   'placeholder': _('New Password'),
 | 
			
		||||
                                   'autocomplete': 'new-password'
 | 
			
		||||
                                   }))
 | 
			
		||||
    password_repeat = forms.CharField(label=_('Repeat Password'),
 | 
			
		||||
                                      widget=forms.PasswordInput(attrs={
 | 
			
		||||
                                          'placeholder': _('Repeat Password')
 | 
			
		||||
                                          'placeholder': _('Repeat Password'),
 | 
			
		||||
                                          'autocomplete': 'new-password'
 | 
			
		||||
                                      }))
 | 
			
		||||
 | 
			
		||||
    def clean_password_repeat(self):
 | 
			
		||||
@ -34,5 +38,4 @@ class PasswordChangeForm(forms.Form):
 | 
			
		||||
        password_repeat = self.cleaned_data.get('password_repeat')
 | 
			
		||||
        if password != password_repeat:
 | 
			
		||||
            raise ValidationError(_("Passwords don't match"))
 | 
			
		||||
        # TODO: Password policy check
 | 
			
		||||
        return self.cleaned_data.get('password_repeat')
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								passbook/core/management/commands/import_users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								passbook/core/management/commands/import_users.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
"""passbook import_users management command"""
 | 
			
		||||
from csv import DictReader
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.core.validators import EmailValidator, ValidationError
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
 | 
			
		||||
LOGGER = getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    """Import users from CSV file"""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # Positional arguments
 | 
			
		||||
        parser.add_argument('file', nargs='+', type=str)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        """Create Users from CSV file"""
 | 
			
		||||
        for file in options.get('file'):
 | 
			
		||||
            with open(file, 'r') as _file:
 | 
			
		||||
                reader = DictReader(_file)
 | 
			
		||||
                for user in reader:
 | 
			
		||||
                    LOGGER.debug('User %s', user.get('username'))
 | 
			
		||||
                    try:
 | 
			
		||||
                        # only import users with valid email addresses
 | 
			
		||||
                        if user.get('email'):
 | 
			
		||||
                            validator = EmailValidator()
 | 
			
		||||
                            validator(user.get('email'))
 | 
			
		||||
                        # use combination of username and email to check for existing user
 | 
			
		||||
                        if User.objects.filter(
 | 
			
		||||
                                username=user.get('username'),
 | 
			
		||||
                                email=user.get('email')).exists():
 | 
			
		||||
                            LOGGER.debug('User %s exists already, skipping', user.get('username'))
 | 
			
		||||
                        # Create user
 | 
			
		||||
                        User.objects.create(
 | 
			
		||||
                            username=user.get('username'),
 | 
			
		||||
                            email=user.get('email'),
 | 
			
		||||
                            name=user.get('name'))
 | 
			
		||||
                        LOGGER.debug('Created User %s', user.get('username'))
 | 
			
		||||
                    except ValidationError as exc:
 | 
			
		||||
                        LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
 | 
			
		||||
                        continue
 | 
			
		||||
@ -153,10 +153,12 @@ class Application(PolicyModel):
 | 
			
		||||
    def user_is_authorized(self, user: User) -> bool:
 | 
			
		||||
        """Check if user is authorized to use this application"""
 | 
			
		||||
        from passbook.core.policies import PolicyEngine
 | 
			
		||||
        return PolicyEngine(self.policies.all()).for_user(user).result
 | 
			
		||||
        return PolicyEngine(self.policies.all()).for_user(user).build().result
 | 
			
		||||
 | 
			
		||||
    def get_provider(self):
 | 
			
		||||
        """Get casted provider instance"""
 | 
			
		||||
        if not self.provider:
 | 
			
		||||
            return None
 | 
			
		||||
        return Provider.objects.get_subclass(pk=self.provider.pk)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
@ -284,8 +286,7 @@ class FieldMatcherPolicy(Policy):
 | 
			
		||||
        if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
 | 
			
		||||
            pattern = re.compile(self.value)
 | 
			
		||||
            passes = bool(pattern.match(user_field_value))
 | 
			
		||||
        if self.negate:
 | 
			
		||||
            passes = not passes
 | 
			
		||||
 | 
			
		||||
        LOGGER.debug("User got '%r'", passes)
 | 
			
		||||
        return passes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
 | 
			
		||||
from celery import group
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
 | 
			
		||||
from passbook.core.celery import CELERY_APP
 | 
			
		||||
from passbook.core.models import Policy, User
 | 
			
		||||
@ -17,25 +18,52 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
 | 
			
		||||
        setattr(user_obj, key, value)
 | 
			
		||||
    LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
 | 
			
		||||
                 policy_obj.pk.hex, user_obj)
 | 
			
		||||
    return policy_obj.passes(user_obj)
 | 
			
		||||
    policy_result = policy_obj.passes(user_obj)
 | 
			
		||||
    # Handle policy result correctly if result, message or just result
 | 
			
		||||
    message = None
 | 
			
		||||
    if isinstance(policy_result, (tuple, list)):
 | 
			
		||||
        policy_result, message = policy_result
 | 
			
		||||
    # Invert result if policy.negate is set
 | 
			
		||||
    if policy_obj.negate:
 | 
			
		||||
        policy_result = not policy_result
 | 
			
		||||
    LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
 | 
			
		||||
    return policy_obj.action, policy_result, message
 | 
			
		||||
 | 
			
		||||
class PolicyEngine:
 | 
			
		||||
    """Orchestrate policy checking, launch tasks and return result"""
 | 
			
		||||
 | 
			
		||||
    policies = None
 | 
			
		||||
    _group = None
 | 
			
		||||
    _request = None
 | 
			
		||||
    _user = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, policies):
 | 
			
		||||
        self.policies = policies
 | 
			
		||||
        self._request = None
 | 
			
		||||
        self._user = None
 | 
			
		||||
 | 
			
		||||
    def for_user(self, user):
 | 
			
		||||
        """Check policies for user"""
 | 
			
		||||
        self._user = user
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def with_request(self, request):
 | 
			
		||||
        """Set request"""
 | 
			
		||||
        self._request = request
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def build(self):
 | 
			
		||||
        """Build task group"""
 | 
			
		||||
        signatures = []
 | 
			
		||||
        kwargs = {
 | 
			
		||||
            '__password__': getattr(user, '__password__', None)
 | 
			
		||||
            '__password__': getattr(self._user, '__password__', None),
 | 
			
		||||
        }
 | 
			
		||||
        if self._request:
 | 
			
		||||
            kwargs['remote_ip'], _ = get_client_ip(self._request)
 | 
			
		||||
            if not kwargs['remote_ip']:
 | 
			
		||||
                kwargs['remote_ip'] = '255.255.255.255'
 | 
			
		||||
        for policy in self.policies:
 | 
			
		||||
            signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs))
 | 
			
		||||
            signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs))
 | 
			
		||||
        self._group = group(signatures)()
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
@ -43,10 +71,11 @@ class PolicyEngine:
 | 
			
		||||
    def result(self):
 | 
			
		||||
        """Get policy-checking result"""
 | 
			
		||||
        messages = []
 | 
			
		||||
        for policy_result in self._group.get():
 | 
			
		||||
            if isinstance(policy_result, (tuple, list)):
 | 
			
		||||
                policy_result, policy_message = policy_result
 | 
			
		||||
        for policy_action, policy_result, policy_message in self._group.get():
 | 
			
		||||
            passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
 | 
			
		||||
                      (policy_action == Policy.ACTION_DENY and not policy_result)
 | 
			
		||||
            if policy_message:
 | 
			
		||||
                messages.append(policy_message)
 | 
			
		||||
            if policy_result is False:
 | 
			
		||||
            if not passing:
 | 
			
		||||
                return False, messages
 | 
			
		||||
        return True, messages
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
django>=2.0
 | 
			
		||||
django-model-utils
 | 
			
		||||
django-ipware
 | 
			
		||||
djangorestframework
 | 
			
		||||
PyYAML
 | 
			
		||||
raven
 | 
			
		||||
 | 
			
		||||
@ -62,6 +62,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    'django.contrib.staticfiles',
 | 
			
		||||
    'rest_framework',
 | 
			
		||||
    'drf_yasg',
 | 
			
		||||
    'raven.contrib.django.raven_compat',
 | 
			
		||||
    'passbook.core.apps.PassbookCoreConfig',
 | 
			
		||||
    'passbook.admin.apps.PassbookAdminConfig',
 | 
			
		||||
    'passbook.api.apps.PassbookAPIConfig',
 | 
			
		||||
@ -75,6 +76,8 @@ INSTALLED_APPS = [
 | 
			
		||||
    'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
 | 
			
		||||
    'passbook.hibp_policy.apps.PassbookHIBPConfig',
 | 
			
		||||
    'passbook.pretend.apps.PassbookPretendConfig',
 | 
			
		||||
    'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig',
 | 
			
		||||
    'passbook.suspicious_policy.apps.PassbookSuspiciousPolicyConfig',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# Message Tag fix for bootstrap CSS Classes
 | 
			
		||||
@ -103,6 +106,7 @@ MIDDLEWARE = [
 | 
			
		||||
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
			
		||||
    'django.contrib.messages.middleware.MessageMiddleware',
 | 
			
		||||
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
			
		||||
    'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ROOT_URLCONF = 'passbook.core.urls'
 | 
			
		||||
@ -183,6 +187,14 @@ CELERY_TASK_DEFAULT_QUEUE = 'passbook'
 | 
			
		||||
CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis')
 | 
			
		||||
CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis')
 | 
			
		||||
 | 
			
		||||
# Raven settings
 | 
			
		||||
RAVEN_CONFIG = {
 | 
			
		||||
    'dsn': ('https://55b5dd780bc14f4c96bba69b7a9abbcc:449af483bd0745'
 | 
			
		||||
            '0d83be640d834e5458@sentry.services.beryju.org/8'),
 | 
			
		||||
    'release': VERSION,
 | 
			
		||||
    'environment': 'dev' if DEBUG else 'production',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# CherryPY settings
 | 
			
		||||
with CONFIG.cd('web'):
 | 
			
		||||
    CHERRYPY_SERVER = {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ def password_policy_checker(sender, password, **kwargs):
 | 
			
		||||
    _all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
 | 
			
		||||
    for factor in _all_factors:
 | 
			
		||||
        policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses())
 | 
			
		||||
        policy_engine.for_user(sender)
 | 
			
		||||
        policy_engine.for_user(sender).build()
 | 
			
		||||
        passing, messages = policy_engine.result
 | 
			
		||||
        if not passing:
 | 
			
		||||
            raise PasswordPolicyInvalid(*messages)
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
<div class="login-pf-page">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <div class="col-sm-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">
 | 
			
		||||
            <div class="col-sm-12 col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3">
 | 
			
		||||
                <header class="login-pf-page-header">
 | 
			
		||||
                    <img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
 | 
			
		||||
                        alt="passbook logo" />
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ def user_factors(context):
 | 
			
		||||
    for factor in _all_factors:
 | 
			
		||||
        _link = factor.has_user_settings()
 | 
			
		||||
        policy_engine = PolicyEngine(factor.policies.all())
 | 
			
		||||
        policy_engine.for_user(user)
 | 
			
		||||
        policy_engine.for_user(user).with_request(context.get('request')).build()
 | 
			
		||||
        if policy_engine.result[0] and _link:
 | 
			
		||||
            matching_factors.append(_link)
 | 
			
		||||
    return matching_factors
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ class UserChangePasswordView(FormView):
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form: PasswordChangeForm):
 | 
			
		||||
        try:
 | 
			
		||||
            # user.set_password checks against Policies so we don't need to manually do it here
 | 
			
		||||
            self.request.user.set_password(form.cleaned_data.get('password'))
 | 
			
		||||
            self.request.user.save()
 | 
			
		||||
            update_session_auth_hash(self.request, self.request.user)
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,8 @@ https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.core.wsgi import get_wsgi_application
 | 
			
		||||
from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
 | 
			
		||||
 | 
			
		||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.settings')
 | 
			
		||||
 | 
			
		||||
application = get_wsgi_application()
 | 
			
		||||
application = Sentry(get_wsgi_application())
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook hibp_policy"""
 | 
			
		||||
__version__ = '0.0.7-alpha'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""Passbook ldap app Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook lib"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -212,10 +212,14 @@ def gravatar(email, size=None, rating=None):
 | 
			
		||||
@register.filter
 | 
			
		||||
def verbose_name(obj):
 | 
			
		||||
    """Return Object's Verbose Name"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return ''
 | 
			
		||||
    return obj._meta.verbose_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def form_verbose_name(obj):
 | 
			
		||||
    """Return ModelForm's Object's Verbose Name"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return ''
 | 
			
		||||
    return obj._meta.model._meta.verbose_name
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook oauth_client Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook oauth_provider Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ class OAuth2Provider(Provider, AbstractApplication):
 | 
			
		||||
    form = 'passbook.oauth_provider.forms.OAuth2ProviderForm'
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
        return "OAuth2 Provider %s" % self.name
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook otp Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								passbook/password_expiry_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								passbook/password_expiry_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
"""passbook password_expiry"""
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
							
								
								
									
										5
									
								
								passbook/password_expiry_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/password_expiry_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
"""Passbook password_expiry_policy Admin"""
 | 
			
		||||
 | 
			
		||||
from passbook.lib.admin import admin_autoregister
 | 
			
		||||
 | 
			
		||||
admin_autoregister('passbook_password_expiry_policy')
 | 
			
		||||
							
								
								
									
										11
									
								
								passbook/password_expiry_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/password_expiry_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
"""Passbook password_expiry_policy app config"""
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PassbookPasswordExpiryPolicyConfig(AppConfig):
 | 
			
		||||
    """Passbook password_expiry_policy app config"""
 | 
			
		||||
 | 
			
		||||
    name = 'passbook.password_expiry_policy'
 | 
			
		||||
    label = 'passbook_password_expiry_policy'
 | 
			
		||||
    verbose_name = 'passbook Password Expiry Policy'
 | 
			
		||||
							
								
								
									
										24
									
								
								passbook/password_expiry_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/password_expiry_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
"""passbook PasswordExpiry Policy forms"""
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.forms.policies import GENERAL_FIELDS
 | 
			
		||||
from passbook.password_expiry_policy.models import PasswordExpiryPolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordExpiryPolicyForm(forms.ModelForm):
 | 
			
		||||
    """Edit PasswordExpiryPolicy instances"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = PasswordExpiryPolicy
 | 
			
		||||
        fields = GENERAL_FIELDS + ['days', 'deny_only']
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'name': forms.TextInput(),
 | 
			
		||||
            'order': forms.NumberInput(),
 | 
			
		||||
            'days': forms.NumberInput(),
 | 
			
		||||
        }
 | 
			
		||||
        labels = {
 | 
			
		||||
            'deny_only': _("Only fail the policy, don't set user's password.")
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										29
									
								
								passbook/password_expiry_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/password_expiry_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
# Generated by Django 2.1.7 on 2019-03-03 13:46
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('passbook_core', '0016_auto_20190227_1355'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='PasswordExpiryPolicy',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
 | 
			
		||||
                ('deny_only', models.BooleanField(default=False)),
 | 
			
		||||
                ('days', models.IntegerField()),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Password Expiry Policy',
 | 
			
		||||
                'verbose_name_plural': 'Password Expiry Policies',
 | 
			
		||||
            },
 | 
			
		||||
            bases=('passbook_core.policy',),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										42
									
								
								passbook/password_expiry_policy/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								passbook/password_expiry_policy/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
"""passbook password_expiry_policy Models"""
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Policy, User
 | 
			
		||||
 | 
			
		||||
LOGGER = getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordExpiryPolicy(Policy):
 | 
			
		||||
    """If password change date is more than x days in the past, call set_unusable_password
 | 
			
		||||
    and show a notice"""
 | 
			
		||||
 | 
			
		||||
    deny_only = models.BooleanField(default=False)
 | 
			
		||||
    days = models.IntegerField()
 | 
			
		||||
 | 
			
		||||
    form = 'passbook.password_expiry_policy.forms.PasswordExpiryPolicyForm'
 | 
			
		||||
 | 
			
		||||
    def passes(self, user: User) -> bool:
 | 
			
		||||
        """If password change date is more than x days in the past, call set_unusable_password
 | 
			
		||||
        and show a notice"""
 | 
			
		||||
        actual_days = (now() - user.password_change_date).days
 | 
			
		||||
        days_since_expiry = (now() - (user.password_change_date + timedelta(days=self.days))).days
 | 
			
		||||
        if actual_days >= self.days:
 | 
			
		||||
            if not self.deny_only:
 | 
			
		||||
                user.set_unusable_password()
 | 
			
		||||
                user.save()
 | 
			
		||||
                return False, _(('Password expired %(days)d days ago. '
 | 
			
		||||
                                 'Please update your password.') % {
 | 
			
		||||
                                     'days': days_since_expiry
 | 
			
		||||
                                 })
 | 
			
		||||
            return False, _('Password has expired.')
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _('Password Expiry Policy')
 | 
			
		||||
        verbose_name_plural = _('Password Expiry Policies')
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook saml_idp Header"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
__version__ = '0.1.2-beta'
 | 
			
		||||
 | 
			
		||||
@ -36,13 +36,13 @@ class SAMLProvider(Provider):
 | 
			
		||||
        return self._processor
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "SAMLProvider %s (processor=%s)" % (self.name, self.processor_path)
 | 
			
		||||
        return "SAML Provider %s" % self.name
 | 
			
		||||
 | 
			
		||||
    def link_download_metadata(self):
 | 
			
		||||
        """Get link to download XML metadata for admin interface"""
 | 
			
		||||
        try:
 | 
			
		||||
            # pylint: disable=no-member
 | 
			
		||||
            return reverse('passbook_saml_idp:metadata_xml',
 | 
			
		||||
            return reverse('passbook_saml_idp:saml-metadata',
 | 
			
		||||
                           kwargs={'application': self.application.slug})
 | 
			
		||||
        except Provider.application.RelatedObjectDoesNotExist:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@
 | 
			
		||||
        </section>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-footer">
 | 
			
		||||
        <a href="{% url 'passbook_saml_idp:metadata_xml' %}" class="btn btn-primary"><clr-icon shape="download"></clr-icon>{% trans 'Download Metadata' %}</a>
 | 
			
		||||
        <a href="{% url 'passbook_saml_idp:saml-metadata' %}" class="btn btn-primary"><clr-icon shape="download"></clr-icon>{% trans 'Download Metadata' %}</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -4,13 +4,14 @@ from django.urls import path
 | 
			
		||||
from passbook.saml_idp import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login/<slug:application>/',
 | 
			
		||||
         views.LoginBeginView.as_view(), name="saml_login_begin"),
 | 
			
		||||
    path('login/<slug:application>/initiate/',
 | 
			
		||||
         views.InitiateLoginView.as_view(), name="saml_login_init"),
 | 
			
		||||
    path('login/<slug:application>/process/',
 | 
			
		||||
         views.LoginProcessView.as_view(), name='saml_login_process'),
 | 
			
		||||
    path('logout/', views.LogoutView.as_view(), name="saml_logout"),
 | 
			
		||||
    path('metadata/<slug:application>/',
 | 
			
		||||
         views.DescriptorDownloadView.as_view(), name='metadata_xml'),
 | 
			
		||||
    path('<slug:application>/login/',
 | 
			
		||||
         views.LoginBeginView.as_view(), name="saml-login"),
 | 
			
		||||
    path('<slug:application>/login/initiate/',
 | 
			
		||||
         views.InitiateLoginView.as_view(), name="saml-login-initiate"),
 | 
			
		||||
    path('<slug:application>/login/process/',
 | 
			
		||||
         views.LoginProcessView.as_view(), name='saml-login-process'),
 | 
			
		||||
    path('<slug:application>/logout/', views.LogoutView.as_view(), name="saml-logout"),
 | 
			
		||||
    path('<slug:application>/logout/slo/', views.SLOLogout.as_view(), name="saml-logout-slo"),
 | 
			
		||||
    path('<slug:application>/metadata/',
 | 
			
		||||
         views.DescriptorDownloadView.as_view(), name='saml-metadata'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,9 @@ from django.views import View
 | 
			
		||||
from django.views.decorators.csrf import csrf_exempt
 | 
			
		||||
from signxml.util import strip_pem_header
 | 
			
		||||
 | 
			
		||||
from passbook.audit.models import AuditEntry
 | 
			
		||||
from passbook.core.models import Application
 | 
			
		||||
from passbook.core.policies import PolicyEngine
 | 
			
		||||
from passbook.lib.config import CONFIG
 | 
			
		||||
from passbook.lib.mixins import CSRFExemptMixin
 | 
			
		||||
from passbook.lib.utils.template import render_to_string
 | 
			
		||||
@ -75,7 +77,7 @@ class LoginBeginView(LoginRequiredMixin, View):
 | 
			
		||||
            return HttpResponseBadRequest('the SAML request payload is missing')
 | 
			
		||||
 | 
			
		||||
        request.session['RelayState'] = source.get('RelayState', '')
 | 
			
		||||
        return redirect(reverse('passbook_saml_idp:saml_login_process', kwargs={
 | 
			
		||||
        return redirect(reverse('passbook_saml_idp:saml-login-process', kwargs={
 | 
			
		||||
            'application': application
 | 
			
		||||
        }))
 | 
			
		||||
 | 
			
		||||
@ -94,19 +96,29 @@ class RedirectToSPView(LoginRequiredMixin, View):
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginProcessView(ProviderMixin, LoginRequiredMixin, View):
 | 
			
		||||
    """Processor-based login continuation.
 | 
			
		||||
    Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
 | 
			
		||||
 | 
			
		||||
    def _has_access(self):
 | 
			
		||||
        """Check if user has access to application"""
 | 
			
		||||
        policy_engine = PolicyEngine(self.provider.application.policies.all())
 | 
			
		||||
        policy_engine.for_user(self.request.user).with_request(self.request).build()
 | 
			
		||||
        return policy_engine.result
 | 
			
		||||
 | 
			
		||||
    def get(self, request, application):
 | 
			
		||||
        """Handle get request, i.e. render form"""
 | 
			
		||||
        LOGGER.debug("Request: %s", request)
 | 
			
		||||
        # Check if user has access
 | 
			
		||||
        access = True
 | 
			
		||||
        # TODO: Check access here
 | 
			
		||||
        if self.provider.application.skip_authorization and access:
 | 
			
		||||
        if self.provider.application.skip_authorization and self._has_access():
 | 
			
		||||
            ctx = self.provider.processor.generate_response()
 | 
			
		||||
            # TODO: AuditLog Skipped Authz
 | 
			
		||||
            # Log Application Authorization
 | 
			
		||||
            AuditEntry.create(
 | 
			
		||||
                action=AuditEntry.ACTION_AUTHORIZE_APPLICATION,
 | 
			
		||||
                request=request,
 | 
			
		||||
                app=self.provider.application.name,
 | 
			
		||||
                skipped_authorization=True)
 | 
			
		||||
            return RedirectToSPView.as_view()(
 | 
			
		||||
                request=request,
 | 
			
		||||
                acs_url=ctx['acs_url'],
 | 
			
		||||
@ -122,11 +134,13 @@ class LoginProcessView(ProviderMixin, LoginRequiredMixin, View):
 | 
			
		||||
        """Handle post request, return back to ACS"""
 | 
			
		||||
        LOGGER.debug("Request: %s", request)
 | 
			
		||||
        # Check if user has access
 | 
			
		||||
        access = True
 | 
			
		||||
        # TODO: Check access here
 | 
			
		||||
        if request.POST.get('ACSUrl', None) and access:
 | 
			
		||||
        if request.POST.get('ACSUrl', None) and self._has_access():
 | 
			
		||||
            # User accepted request
 | 
			
		||||
            # TODO: AuditLog accepted
 | 
			
		||||
            AuditEntry.create(
 | 
			
		||||
                action=AuditEntry.ACTION_AUTHORIZE_APPLICATION,
 | 
			
		||||
                request=request,
 | 
			
		||||
                app=self.provider.application.name,
 | 
			
		||||
                skipped_authorization=False)
 | 
			
		||||
            return RedirectToSPView.as_view()(
 | 
			
		||||
                request=request,
 | 
			
		||||
                acs_url=request.POST.get('ACSUrl'),
 | 
			
		||||
@ -144,7 +158,7 @@ class LogoutView(CSRFExemptMixin, LoginRequiredMixin, View):
 | 
			
		||||
    returns a standard logged-out page. (SalesForce and others use this method,
 | 
			
		||||
    though it's technically not SAML 2.0)."""
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
    def get(self, request, application):
 | 
			
		||||
        """Perform logout"""
 | 
			
		||||
        logout(request)
 | 
			
		||||
 | 
			
		||||
@ -164,11 +178,10 @@ class SLOLogout(CSRFExemptMixin, LoginRequiredMixin, View):
 | 
			
		||||
    """Receives a SAML 2.0 LogoutRequest from a Service Provider,
 | 
			
		||||
    logs out the user and returns a standard logged-out page."""
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
    def post(self, request, application):
 | 
			
		||||
        """Perform logout"""
 | 
			
		||||
        request.session['SAMLRequest'] = request.POST['SAMLRequest']
 | 
			
		||||
        # TODO: Parse SAML LogoutRequest from POST data, similar to login_process().
 | 
			
		||||
        # TODO: Add a URL dispatch for this view.
 | 
			
		||||
        # TODO: Modify the base processor to handle logouts?
 | 
			
		||||
        # TODO: Combine this with login_process(), since they are so very similar?
 | 
			
		||||
        # TODO: Format a LogoutResponse and return it to the browser.
 | 
			
		||||
@ -183,8 +196,8 @@ class DescriptorDownloadView(ProviderMixin, View):
 | 
			
		||||
    def get(self, request, application):
 | 
			
		||||
        """Replies with the XML Metadata IDSSODescriptor."""
 | 
			
		||||
        entity_id = CONFIG.y('saml_idp.issuer')
 | 
			
		||||
        slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_logout'))
 | 
			
		||||
        sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin', kwargs={
 | 
			
		||||
        slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml-logout'))
 | 
			
		||||
        sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml-login', kwargs={
 | 
			
		||||
            'application': application
 | 
			
		||||
        }))
 | 
			
		||||
        pubkey = strip_pem_header(self.provider.signing_cert.replace('\r', '')).replace('\n', '')
 | 
			
		||||
@ -206,6 +219,5 @@ class InitiateLoginView(ProviderMixin, LoginRequiredMixin, View):
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, application):
 | 
			
		||||
        """Initiates an IdP-initiated link to a simple SP resource/target URL."""
 | 
			
		||||
        super().dispatch(request, application)
 | 
			
		||||
        self.provider.processor.init_deep_link(request, '')
 | 
			
		||||
        return _generate_response(request, self.provider)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								passbook/suspicious_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								passbook/suspicious_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
"""passbook suspicious_policy"""
 | 
			
		||||
__version__ = '0.1.1-beta'
 | 
			
		||||
							
								
								
									
										5
									
								
								passbook/suspicious_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/suspicious_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
"""Passbook suspicious_policy Admin"""
 | 
			
		||||
 | 
			
		||||
from passbook.lib.admin import admin_autoregister
 | 
			
		||||
 | 
			
		||||
admin_autoregister('passbook_suspicious_policy')
 | 
			
		||||
							
								
								
									
										15
									
								
								passbook/suspicious_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								passbook/suspicious_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
"""Passbook suspicious_policy app config"""
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PassbookSuspiciousPolicyConfig(AppConfig):
 | 
			
		||||
    """Passbook suspicious_policy app config"""
 | 
			
		||||
 | 
			
		||||
    name = 'passbook.suspicious_policy'
 | 
			
		||||
    label = 'passbook_suspicious_policy'
 | 
			
		||||
    verbose_name = 'passbook Suspicious Request Detector'
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        import_module('passbook.suspicious_policy.signals')
 | 
			
		||||
							
								
								
									
										18
									
								
								passbook/suspicious_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/suspicious_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
"""passbook suspicious request forms"""
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
from passbook.core.forms.policies import GENERAL_FIELDS
 | 
			
		||||
from passbook.suspicious_policy.models import SuspiciousRequestPolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SuspiciousRequestPolicyForm(forms.ModelForm):
 | 
			
		||||
    """Form to edit SuspiciousRequestPolicy"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = SuspiciousRequestPolicy
 | 
			
		||||
        fields = GENERAL_FIELDS + ['check_ip', 'check_username', 'threshold']
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'name': forms.TextInput(),
 | 
			
		||||
            'value': forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										49
									
								
								passbook/suspicious_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								passbook/suspicious_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
# Generated by Django 2.1.7 on 2019-03-03 18:17
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('passbook_core', '0016_auto_20190227_1355'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='IPScore',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('ip', models.GenericIPAddressField()),
 | 
			
		||||
                ('score', models.IntegerField(default=0)),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='SuspiciousRequestPolicy',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
 | 
			
		||||
                ('check_ip', models.BooleanField(default=True)),
 | 
			
		||||
                ('check_username', models.BooleanField(default=True)),
 | 
			
		||||
                ('threshold', models.IntegerField(default=-5)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
            bases=('passbook_core.policy',),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='UserScore',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('score', models.IntegerField(default=0)),
 | 
			
		||||
                ('updated', models.DateTimeField(auto_now=True)),
 | 
			
		||||
                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 2.1.7 on 2019-03-03 18:20
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('passbook_suspicious_policy', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name='suspiciousrequestpolicy',
 | 
			
		||||
            options={'verbose_name': 'Suspicious Request Policy', 'verbose_name_plural': 'Suspicious Request Policies'},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 2.1.7 on 2019-03-03 18:33
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('passbook_suspicious_policy', '0002_auto_20190303_1820'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='ipscore',
 | 
			
		||||
            name='ip',
 | 
			
		||||
            field=models.GenericIPAddressField(unique=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='userscore',
 | 
			
		||||
            name='user',
 | 
			
		||||
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/suspicious_policy/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/suspicious_policy/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										51
									
								
								passbook/suspicious_policy/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								passbook/suspicious_policy/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
"""passbook suspicious request policy"""
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Policy, User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SuspiciousRequestPolicy(Policy):
 | 
			
		||||
    """Return true if request IP/target username's score is below a certain threshold"""
 | 
			
		||||
 | 
			
		||||
    check_ip = models.BooleanField(default=True)
 | 
			
		||||
    check_username = models.BooleanField(default=True)
 | 
			
		||||
    threshold = models.IntegerField(default=-5)
 | 
			
		||||
 | 
			
		||||
    form = 'passbook.suspicious_policy.forms.SuspiciousRequestPolicyForm'
 | 
			
		||||
 | 
			
		||||
    def passes(self, user: User):
 | 
			
		||||
        remote_ip = user.remote_ip
 | 
			
		||||
        passing = True
 | 
			
		||||
        if self.check_ip:
 | 
			
		||||
            ip_scores = IPScore.objects.filter(ip=remote_ip, score__lte=self.threshold)
 | 
			
		||||
            passing = passing and ip_scores.exists()
 | 
			
		||||
        if self.check_username:
 | 
			
		||||
            user_scores = UserScore.objects.filter(user=user, score__lte=self.threshold)
 | 
			
		||||
            passing = passing and user_scores.exists()
 | 
			
		||||
        return passing
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _('Suspicious Request Policy')
 | 
			
		||||
        verbose_name_plural = _('Suspicious Request Policies')
 | 
			
		||||
 | 
			
		||||
class IPScore(models.Model):
 | 
			
		||||
    """Store score coming from the same IP"""
 | 
			
		||||
 | 
			
		||||
    ip = models.GenericIPAddressField(unique=True)
 | 
			
		||||
    score = models.IntegerField(default=0)
 | 
			
		||||
    updated = models.DateTimeField(auto_now=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "IPScore for %s @ %d" % (self.ip, self.score)
 | 
			
		||||
 | 
			
		||||
class UserScore(models.Model):
 | 
			
		||||
    """Store score attempting to log in as the same username"""
 | 
			
		||||
 | 
			
		||||
    user = models.OneToOneField(User, on_delete=models.CASCADE)
 | 
			
		||||
    score = models.IntegerField(default=0)
 | 
			
		||||
    updated = models.DateTimeField(auto_now=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "UserScore for %s @ %d" % (self.user, self.score)
 | 
			
		||||
							
								
								
									
										39
									
								
								passbook/suspicious_policy/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								passbook/suspicious_policy/signals.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
"""passbook suspicious request signals"""
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.signals import user_logged_in, user_login_failed
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.suspicious_policy.models import IPScore, UserScore
 | 
			
		||||
 | 
			
		||||
LOGGER = getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_score(request, username, amount):
 | 
			
		||||
    """Update score for IP and User"""
 | 
			
		||||
    remote_ip, _ = get_client_ip(request)
 | 
			
		||||
    if not remote_ip:
 | 
			
		||||
        remote_ip = '255.255.255.255'
 | 
			
		||||
    ip_score, _ = IPScore.objects.update_or_create(ip=remote_ip)
 | 
			
		||||
    ip_score.score += amount
 | 
			
		||||
    ip_score.save()
 | 
			
		||||
    LOGGER.debug("Added %s to score of IP %s", amount, remote_ip)
 | 
			
		||||
    user = User.objects.filter(username=username)
 | 
			
		||||
    if not user.exists():
 | 
			
		||||
        return
 | 
			
		||||
    user_score, _ = UserScore.objects.update_or_create(user=user.first())
 | 
			
		||||
    user_score.score += amount
 | 
			
		||||
    user_score.save()
 | 
			
		||||
    LOGGER.debug("Added %s to score of User %s", amount, username)
 | 
			
		||||
 | 
			
		||||
@receiver(user_login_failed)
 | 
			
		||||
def handle_failed_login(sender, request, credentials, **kwargs):
 | 
			
		||||
    """Lower Score for failed loging attempts"""
 | 
			
		||||
    update_score(request, credentials.get('username'), -1)
 | 
			
		||||
 | 
			
		||||
@receiver(user_logged_in)
 | 
			
		||||
def handle_successful_login(sender, request, user, **kwargs):
 | 
			
		||||
    """Raise score for successful attempts"""
 | 
			
		||||
    update_score(request, user.username, 1)
 | 
			
		||||
@ -4,7 +4,6 @@
 | 
			
		||||
-r passbook/saml_idp/requirements.txt
 | 
			
		||||
-r passbook/otp/requirements.txt
 | 
			
		||||
-r passbook/oauth_provider/requirements.txt
 | 
			
		||||
-r passbook/audit/requirements.txt
 | 
			
		||||
-r passbook/captcha_factor/requirements.txt
 | 
			
		||||
-r passbook/admin/requirements.txt
 | 
			
		||||
-r passbook/api/requirements.txt
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user