Compare commits

...

21 Commits

Author SHA1 Message Date
f54520b5cf bump version: 0.0.12-alpha -> 0.0.13-alpha 2019-02-27 16:06:28 +01:00
d7c4697625 Only use one create template, get title from Form's Model 2019-02-27 16:06:20 +01:00
5584f5bda8 switch to PolicyEngine everywhere 2019-02-27 15:49:20 +01:00
2ce6f5a714 improve error display on forms 2019-02-27 15:49:05 +01:00
c66945623a Improve admin interface more (back links, better headlines) 2019-02-27 15:48:33 +01:00
cbae05c74c show more useful information on admin overview 2019-02-27 15:45:42 +01:00
5b771da972 switch from first_name and last_name to name 2019-02-27 15:09:05 +01:00
2db1738e4a make Admin UI more consistent, better show when provider has no application assigned 2019-02-27 14:47:11 +01:00
95de6a14fd bump version: 0.0.11-alpha -> 0.0.12-alpha 2019-02-27 13:18:28 +01:00
17132ebc19 Verify OAuth Username vuln and fix closes #9 2019-02-27 13:18:16 +01:00
289be46388 fix SAML Views not having LoginRequiredMixin 2019-02-27 12:36:18 +01:00
6c300b7b31 autofocus password field 2019-02-27 12:35:57 +01:00
b726583084 Keep GET parameters throughout entire login process 2019-02-27 12:35:48 +01:00
48055d1cfd fix CSRF Bug in SAML 2019-02-27 11:20:52 +01:00
436070f5bd fix redis connection issues in k8s 2019-02-27 09:59:01 +01:00
3ee79818db explicit version in helm values 2019-02-27 09:33:26 +01:00
e7a02104db fix display on mobile 2019-02-27 09:33:12 +01:00
556740d7bc add PasswordPolicyForm back in 2019-02-26 15:41:11 +01:00
421f51770c implement password policy checking on signup and password change closes #8 2019-02-26 15:40:58 +01:00
96f7e70f9e enable always_eager when unittesting 2019-02-26 14:24:50 +01:00
ad96f7dbb8 add E-Mail support via celery task, untested, closes #17 2019-02-26 14:10:53 +01:00
84 changed files with 829 additions and 240 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.11-alpha current_version = 0.0.13-alpha
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -14,6 +14,8 @@ values =
beta beta
stable stable
[bumpversion:file:helm/passbook/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml] [bumpversion:file:helm/passbook/Chart.yaml]
[bumpversion:file:.gitlab-ci.yml] [bumpversion:file:.gitlab-ci.yml]

View File

@ -16,7 +16,6 @@ variables:
POSTGRES_DB: passbook POSTGRES_DB: passbook
POSTGRES_USER: passbook POSTGRES_USER: passbook
POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77' POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77'
SUPERVISR_ENV: ci
include: include:
- /allauth/.gitlab-ci.yml - /allauth/.gitlab-ci.yml
@ -54,7 +53,7 @@ package-docker:
before_script: before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.0.11-alpha - /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.0.13-alpha
stage: build stage: build
only: only:
- tags - tags

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.0.11-alpha" appVersion: "0.0.13-alpha"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: "0.0.11-alpha" version: "0.0.13-alpha"
icon: https://passbook.beryju.org/images/logo.png icon: https://passbook.beryju.org/images/logo.png

View File

@ -36,7 +36,7 @@ data:
debug: false debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
redis: {{ .Release.Name }}-redis redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master"
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }} error_report_enabled: {{ .Values.config.error_reporting }}
@ -105,10 +105,9 @@ data:
email: mail # or userPrincipalName email: mail # or userPrincipalName
user_attribute_map: user_attribute_map:
active_directory: active_directory:
sAMAccountName: username username: "%(sAMAccountName)s"
mail: email email: "%(mail)s"
given_name: first_name name: "%(displayName)"
name: last_name
# # Create new users in LDAP upon sign-up # # Create new users in LDAP upon sign-up
# create_users: true # create_users: true
# # Reset LDAP password when user reset their password # # Reset LDAP password when user reset their password

View File

@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
tag: latest tag: 0.0.13-alpha
nameOverride: "" nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -11,7 +11,7 @@ class UserSerializer(ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['is_superuser', 'username', 'first_name', 'last_name', 'email', 'date_joined', fields = ['is_superuser', 'username', 'name', 'email', 'date_joined',
'uuid'] 'uuid']

View File

@ -12,7 +12,7 @@
<h1><span class="pficon-applications"></span> {% trans "Applications" %}</h1> <h1><span class="pficon-applications"></span> {% trans "Applications" %}</h1>
<span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span> <span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span>
<hr> <hr>
<a href="{% url 'passbook_admin:application-create' %}" class="btn btn-primary"> <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
<hr> <hr>
@ -21,6 +21,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Provider' %}</th> <th>{% trans 'Provider' %}</th>
<th>{% trans 'Provider Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -28,7 +29,8 @@
{% for application in object_list %} {% for application in object_list %}
<tr> <tr>
<td>{{ application.name }}</td> <td>{{ application.name }}</td>
<td>{{ application.provider }}</td> <td>{{ application.get_provider }}</td>
<td>{{ application.get_provider|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -21,7 +21,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:factor-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -40,7 +40,7 @@
{% for factor in object_list %} {% for factor in object_list %}
<tr> <tr>
<td>{{ factor.name }} ({{ factor.slug }})</td> <td>{{ factor.name }} ({{ factor.slug }})</td>
<td>{{ factor.type }}</td> <td>{{ factor|verbose_name }}</td>
<td>{{ factor.order }}</td> <td>{{ factor.order }}</td>
<td>{{ factor.enabled }}</td> <td>{{ factor.enabled }}</td>
<td> <td>

View File

@ -12,7 +12,7 @@
<h1><span class="pficon-migration"></span> {% trans "Invitations" %}</h1> <h1><span class="pficon-migration"></span> {% trans "Invitations" %}</h1>
<span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span> <span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span>
<hr> <hr>
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary"> <a href="{% url 'passbook_admin:invitation-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
<hr> <hr>

View File

@ -54,7 +54,11 @@
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:providers' %}"> <a href="{% url 'passbook_admin:providers' %}">
<span class="pficon pficon-ok"></span>{{ provider_count }} {% if providers_without_application.exists %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: At least one Provider has no application assigned.' %}"></span> {{ provider_count }}
{% else %}
<span class="pficon pficon-ok"></span> {{ provider_count }}
{% endif %}
</a> </a>
</span> </span>
</p> </p>
@ -168,7 +172,12 @@
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="#"> <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 }}
{% else %}
<span class="pficon pficon-ok"></span>{{ worker_count }} <span class="pficon pficon-ok"></span>{{ worker_count }}
{% endif %}
</a> </a>
</span> </span>
</p> </p>

View File

@ -20,7 +20,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -29,7 +29,7 @@
<thead> <thead>
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -37,7 +37,7 @@
{% for policy in object_list %} {% for policy in object_list %}
<tr> <tr>
<td>{{ policy.name }}</td> <td>{{ policy.name }}</td>
<td>{{ policy|fieldtype }}</td> <td>{{ policy|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -21,7 +21,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -29,16 +29,24 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th></th>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for provider in object_list %} {% for provider in object_list %}
<tr> <tr {% if not provider.application %} class="warning" {% endif %}>
<th>
{% if not provider.application %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Provider has no application assigned.' %}"></span>
{% else %}
<span class="pficon-ok" data-toggle="tooltip" data-placement="right" title="{% blocktrans with app=provider.application %}Assigned to Application {{ app }}{% endblocktrans %}"></span>
{% endif %}
</th>
<td>{{ provider.name }}</td> <td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td> <td>{{ provider|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -17,7 +17,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -11,8 +11,7 @@
<thead> <thead>
<tr> <tr>
<th>{% trans 'Username' %}</th> <th>{% trans 'Username' %}</th>
<th>{% trans 'First Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Last Name' %}</th>
<th>{% trans 'Active' %}</th> <th>{% trans 'Active' %}</th>
<th>{% trans 'Last Login' %}</th> <th>{% trans 'Last Login' %}</th>
<th></th> <th></th>
@ -22,8 +21,7 @@
{% for user in object_list %} {% for user in object_list %}
<tr> <tr>
<td>{{ user.username }}</td> <td>{{ user.username }}</td>
<td>{{ user.first_name|default:'-' }}</td> <td>{{ user.name|default:'-' }}</td>
<td>{{ user.last_name|default:'-' }}</td>
<td>{{ user.is_active }}</td> <td>{{ user.is_active }}</td>
<td>{{ user.last_login }}</td> <td>{{ user.last_login }}</td>
<td> <td>

View File

@ -1,11 +1,12 @@
{% extends "generic/form.html" %} {% extends "generic/form.html" %}
{% load utils %}
{% load i18n %} {% load i18n %}
{% block above_form %} {% block above_form %}
<h1>{% trans 'Create' %}</h1> <h1>{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %} {% block action %}
{% trans 'Create' %} {% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}
{% endblock %} {% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "generic/create.html" %}
{% load i18n %}
{% block title %}
{% blocktrans with type=request.GET.type %}Create {{ type }}{% endblocktrans %}
{% endblock %}
{% block above_form %}
<h1>{% blocktrans with type=request.GET.type %}Create {{ type }}{% endblocktrans %}</h1>
{% endblock %}

View File

@ -1,11 +1,12 @@
{% extends "generic/form.html" %} {% extends "generic/form.html" %}
{% load utils %}
{% load i18n %} {% load i18n %}
{% block above_form %} {% block above_form %}
<h1>{% trans 'Update' %}</h1> <h1>{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %} {% block action %}
{% trans 'Update' %} {% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}
{% endblock %} {% endblock %}

View File

@ -14,6 +14,7 @@ class ApplicationListView(AdminRequiredMixin, ListView):
"""Show list of all applications""" """Show list of all applications"""
model = Application model = Application
ordering = 'name'
template_name = 'administration/application/list.html' template_name = 'administration/application/list.html'
def get_queryset(self): def get_queryset(self):
@ -29,6 +30,10 @@ class ApplicationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView)
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully created Application') success_message = _('Successfully created Application')
def get_context_data(self, **kwargs):
kwargs['type'] = 'Application'
return super().get_context_data(**kwargs)
class ApplicationUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): class ApplicationUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update application""" """Update application"""

View File

@ -34,7 +34,7 @@ class FactorListView(AdminRequiredMixin, ListView):
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Factor""" """Create new Factor"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully created Factor') success_message = _('Successfully created Factor')

View File

@ -27,6 +27,10 @@ class InvitationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
success_message = _('Successfully created Invitation') success_message = _('Successfully created Invitation')
form_class = InvitationForm form_class = InvitationForm
def get_context_data(self, **kwargs):
kwargs['type'] = 'Invitation'
return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
obj = form.save(commit=False) obj = form.save(commit=False)
obj.created_by = self.request.user obj.created_by = self.request.user

View File

@ -23,4 +23,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs['invitation_count'] = len(Invitation.objects.all()) kwargs['invitation_count'] = len(Invitation.objects.all())
kwargs['version'] = __version__ kwargs['version'] = __version__
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5)) kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -32,7 +32,7 @@ class PolicyListView(AdminRequiredMixin, ListView):
class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Policy""" """Create new Policy"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully created Policy') success_message = _('Successfully created Policy')

View File

@ -29,7 +29,7 @@ class ProviderListView(AdminRequiredMixin, ListView):
class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Provider""" """Create new Provider"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully created Provider') success_message = _('Successfully created Provider')

View File

@ -34,7 +34,7 @@ class SourceListView(AdminRequiredMixin, ListView):
class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Source""" """Create new Source"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully created Source') success_message = _('Successfully created Source')

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -14,8 +14,8 @@ class OpenIDUserInfoView(ScopedResourceMixin, View):
payload = { payload = {
'sub': request.user.uuid.int, 'sub': request.user.uuid.int,
'name': request.user.get_full_name(), 'name': request.user.get_full_name(),
'given_name': request.user.first_name, 'given_name': request.user.name,
'family_name': request.user.last_name, 'family_name': '',
'preferred_username': request.user.username, 'preferred_username': request.user.username,
'email': request.user.email, 'email': request.user.email,
} }

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -5,7 +5,7 @@ from django.contrib import messages
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.shortcuts import redirect from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
@ -13,6 +13,7 @@ from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.view import AuthenticationView from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import PasswordFactorForm from passbook.core.forms.authentication import PasswordFactorForm
from passbook.core.models import Nonce from passbook.core.models import Nonce
from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -32,7 +33,16 @@ class PasswordFactor(FormView, AuthenticationFactor):
if 'password-forgotten' in request.GET: if 'password-forgotten' in request.GET:
nonce = Nonce.objects.create(user=self.pending_user) nonce = Nonce.objects.create(user=self.pending_user)
LOGGER.debug("DEBUG %s", str(nonce.uuid)) LOGGER.debug("DEBUG %s", str(nonce.uuid))
# TODO: Send email to user # Send mail to user
send_email.delay(self.pending_user.email, _('Forgotten password'),
'email/account_password_reset.html', {
'url': self.request.build_absolute_uri(
reverse('passbook_core:passbook_core:auth-password-reset',
kwargs={
'nonce': nonce.uuid
})
)
})
self.authenticator.cleanup() self.authenticator.cleanup()
messages.success(request, _('Check your E-Mails for a password reset link.')) messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login') return redirect('passbook_core:auth-login')

View File

@ -4,15 +4,23 @@ from logging import getLogger
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from passbook.core.models import Factor, User from passbook.core.models import Factor, User
from passbook.core.policies import PolicyEngine
from passbook.core.views.utils import PermissionDeniedView from passbook.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute from passbook.lib.utils.urls import is_url_absolute
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
def _redirect_with_qs(view, get_query_set=None):
"""Wrapper to redirect whilst keeping GET Parameters"""
target = reverse(view)
if get_query_set:
target += '?' + urlencode({key: value for key, value in get_query_set.items()})
return redirect(target)
class AuthenticationView(UserPassesTestMixin, View): class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator""" """Wizard-like Multi-factor authenticator"""
@ -37,7 +45,7 @@ class AuthenticationView(UserPassesTestMixin, View):
# Function from UserPassesTestMixin # Function from UserPassesTestMixin
if 'next' in self.request.GET: if 'next' in self.request.GET:
return redirect(self.request.GET.get('next')) return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview')) return _redirect_with_qs('passbook_core:overview', self.request.GET)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# Extract pending user from session (only remember uid) # Extract pending user from session (only remember uid)
@ -46,7 +54,7 @@ class AuthenticationView(UserPassesTestMixin, View):
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER]) User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
else: else:
# No Pending user, redirect to login screen # No Pending user, redirect to login screen
return redirect(reverse('passbook_core:auth-login')) return _redirect_with_qs('passbook_core:auth-login', request.GET)
# Write pending factors to session # Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in request.session: if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS] self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
@ -56,7 +64,9 @@ class AuthenticationView(UserPassesTestMixin, View):
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
self.pending_factors = [] self.pending_factors = []
for factor in _all_factors: for factor in _all_factors:
if factor.passes(self.pending_user): policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(self.pending_user)
if policy_engine.result[0]:
self.pending_factors.append((factor.uuid.hex, factor.type)) self.pending_factors.append((factor.uuid.hex, factor.type))
# Read and instantiate factor from session # Read and instantiate factor from session
factor_uuid, factor_class = None, None factor_uuid, factor_class = None, None
@ -101,8 +111,8 @@ class AuthenticationView(UserPassesTestMixin, View):
self.pending_factors self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor is %s", next_factor) LOGGER.debug("Rendering Factor is %s", next_factor)
# return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor})) # return _redirect_with_qs('passbook_core:auth-process', kwargs={'factor': next_factor})
return redirect(reverse('passbook_core:auth-process')) return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
# User passed all factors # User passed all factors
LOGGER.debug("User passed all factors, logging in") LOGGER.debug("User passed all factors, logging in")
return self._user_passed() return self._user_passed()
@ -112,7 +122,7 @@ class AuthenticationView(UserPassesTestMixin, View):
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
self.cleanup() self.cleanup()
return redirect(reverse('passbook_core:auth-denied')) return _redirect_with_qs('passbook_core:auth-denied', self.request.GET)
def _user_passed(self): def _user_passed(self):
"""User Successfully passed all factors""" """User Successfully passed all factors"""
@ -123,9 +133,9 @@ class AuthenticationView(UserPassesTestMixin, View):
# Cleanup # Cleanup
self.cleanup() self.cleanup()
next_param = self.request.GET.get('next', None) next_param = self.request.GET.get('next', None)
if next_param and is_url_absolute(next_param): if next_param and not is_url_absolute(next_param):
return redirect(next_param) return redirect(next_param)
return redirect(reverse('passbook_core:overview')) return _redirect_with_qs('passbook_core:overview')
def cleanup(self): def cleanup(self):
"""Remove temporary data from session""" """Remove temporary data from session"""

View File

@ -0,0 +1,10 @@
"""passbook core exceptions"""
class PasswordPolicyInvalid(Exception):
"""Exception raised when a Password Policy fails"""
messages = []
def __init__(self, *messages):
super().__init__()
self.messages = messages

View File

@ -38,10 +38,8 @@ class SignUpForm(forms.Form):
"""SignUp Form""" """SignUp Form"""
title = _('Sign Up') title = _('Sign Up')
first_name = forms.CharField(label=_('First Name'), name = forms.CharField(label=_('Name'),
widget=forms.TextInput(attrs={'placeholder': _('First Name')})) widget=forms.TextInput(attrs={'placeholder': _('Name')}))
last_name = forms.CharField(label=_('Last Name'),
widget=forms.TextInput(attrs={'placeholder': _('Last Name')}))
username = forms.CharField(label=_('Username'), username = forms.CharField(label=_('Username'),
widget=forms.TextInput(attrs={'placeholder': _('Username')})) widget=forms.TextInput(attrs={'placeholder': _('Username')}))
email = forms.EmailField(label=_('E-Mail'), email = forms.EmailField(label=_('E-Mail'),
@ -91,4 +89,7 @@ class SignUpForm(forms.Form):
class PasswordFactorForm(forms.Form): class PasswordFactorForm(forms.Form):
"""Password authentication form""" """Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')})) password = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': _('Password'),
'autofocus': 'autofocus'
}))

View File

@ -3,7 +3,8 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import DebugPolicy, FieldMatcherPolicy, WebhookPolicy from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
PasswordPolicy, WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ] GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
@ -50,3 +51,25 @@ class DebugPolicyForm(forms.ModelForm):
labels = { labels = {
'result': _('Allow user') 'result': _('Allow user')
} }
class PasswordPolicyForm(forms.ModelForm):
"""PasswordPolicy Form"""
class Meta:
model = PasswordPolicy
fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase',
'amount_symbols', 'length_min', 'symbol_charset',
'error_message']
widgets = {
'name': forms.TextInput(),
'symbol_charset': forms.TextInput(),
'error_message': forms.TextInput(),
}
labels = {
'amount_uppercase': _('Minimum amount of Uppercase Characters'),
'amount_lowercase': _('Minimum amount of Lowercase Characters'),
'amount_symbols': _('Minimum amount of Symbols Characters'),
'length_min': _('Minimum Length'),
}

View File

@ -13,7 +13,10 @@ class UserDetailForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'first_name', 'last_name', 'email'] fields = ['username', 'name', 'email']
widgets = {
'name': forms.TextInput
}
class PasswordChangeForm(forms.Form): class PasswordChangeForm(forms.Form):
"""Form to update password""" """Form to update password"""

View File

@ -0,0 +1,19 @@
# Generated by Django 2.1.7 on 2019-02-26 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0014_auto_20190226_0850'),
]
operations = [
migrations.AddField(
model_name='passwordpolicy',
name='error_message',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.1.7 on 2019-02-27 13:55
from django.db import migrations, models
def migrate_names(apps, schema_editor):
"""migrate first_name and last_name to name"""
User = apps.get_model("passbook_core", "User")
for user in User.objects.all():
user.name = '%s %s' % (user.first_name, user.last_name)
user.save()
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0015_passwordpolicy_error_message'),
]
operations = [
migrations.AddField(
model_name='user',
name='name',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.RunPython(migrate_names),
migrations.AlterField(
model_name='user',
name='name',
field=models.TextField(),
preserve_default=False,
),
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='user_field',
field=models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
),
]

View File

@ -4,6 +4,7 @@ from datetime import timedelta
from logging import getLogger from logging import getLogger
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Tuple, Union
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -43,12 +44,15 @@ class User(AbstractUser):
"""Custom User model to allow easier adding o f user-based settings""" """Custom User model to allow easier adding o f user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False) uuid = models.UUIDField(default=uuid4, editable=False)
name = models.TextField()
sources = models.ManyToManyField('Source', through='UserSourceConnection') sources = models.ManyToManyField('Source', through='UserSourceConnection')
applications = models.ManyToManyField('Application') applications = models.ManyToManyField('Application')
groups = models.ManyToManyField('Group') groups = models.ManyToManyField('Group')
password_change_date = models.DateTimeField(auto_now_add=True) password_change_date = models.DateTimeField(auto_now_add=True)
def set_password(self, password): def set_password(self, password):
if self.pk:
password_changed.send(sender=self, user=self, password=password) password_changed.send(sender=self, user=self, password=password)
self.password_change_date = now() self.password_change_date = now()
return super().set_password(password) return super().set_password(password)
@ -69,13 +73,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField('Policy', blank=True) policies = models.ManyToManyField('Policy', blank=True)
def passes(self, user: User) -> bool:
"""Return true if user passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
class Factor(PolicyModel): class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
@ -158,6 +155,10 @@ class Application(PolicyModel):
from passbook.core.policies import PolicyEngine from passbook.core.policies import PolicyEngine
return PolicyEngine(self.policies.all()).for_user(user).result return PolicyEngine(self.policies.all()).for_user(user).result
def get_provider(self):
"""Get casted provider instance"""
return Provider.objects.get_subclass(pk=self.provider.pk)
def __str__(self): def __str__(self):
return self.name return self.name
@ -222,7 +223,7 @@ class Policy(UUIDModel, CreatedUpdatedModel):
return self.name return self.name
return "%s action %s" % (self.name, self.action) return "%s action %s" % (self.name, self.action)
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
"""Check if user instance passes this policy""" """Check if user instance passes this policy"""
raise NotImplementedError() raise NotImplementedError()
@ -246,8 +247,7 @@ class FieldMatcherPolicy(Policy):
USER_FIELDS = ( USER_FIELDS = (
('username', _('Username'),), ('username', _('Username'),),
('first_name', _('First Name'),), ('name', _('Name'),),
('last_name', _('Last Name'),),
('email', _('E-Mail'),), ('email', _('E-Mail'),),
('is_staff', _('Is staff'),), ('is_staff', _('Is staff'),),
('is_active', _('Is active'),), ('is_active', _('Is active'),),
@ -267,7 +267,7 @@ class FieldMatcherPolicy(Policy):
description = "%s: %s" % (self.name, description) description = "%s: %s" % (self.name, description)
return description return description
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
"""Check if user instance passes this role""" """Check if user instance passes this role"""
if not hasattr(user, self.user_field): if not hasattr(user, self.user_field):
raise ValueError("Field does not exist") raise ValueError("Field does not exist")
@ -302,10 +302,11 @@ class PasswordPolicy(Policy):
amount_symbols = models.IntegerField(default=0) amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0) length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
form = 'passbook.core.forms.policies.PasswordPolicyForm' form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
# Only check if password is being set # Only check if password is being set
if not hasattr(user, '__password__'): if not hasattr(user, '__password__'):
return True return True
@ -320,6 +321,8 @@ class PasswordPolicy(Policy):
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols) filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password)) result = bool(re.compile(filter_regex).match(password))
LOGGER.debug("User got %r", result) LOGGER.debug("User got %r", result)
if not result:
return result, self.error_message
return result return result
class Meta: class Meta:
@ -378,7 +381,7 @@ class DebugPolicy(Policy):
wait = SystemRandom().randrange(self.wait_min, self.wait_max) wait = SystemRandom().randrange(self.wait_min, self.wait_max)
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait) LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
sleep(wait) sleep(wait)
return self.result return self.result, 'Debugging'
class Meta: class Meta:

View File

@ -42,7 +42,11 @@ class PolicyEngine:
@property @property
def result(self): def result(self):
"""Get policy-checking result""" """Get policy-checking result"""
messages = []
for policy_result in self._group.get(): for policy_result in self._group.get():
if isinstance(policy_result, (tuple, list)):
policy_result, policy_message = policy_result
messages.append(policy_message)
if policy_result is False: if policy_result is False:
return False return False, messages
return True return True, messages

View File

@ -291,6 +291,7 @@ TEST_OUTPUT_FILE_NAME = 'unittest.xml'
if any('test' in arg for arg in sys.argv): if any('test' in arg for arg in sys.argv):
LOGGING = None LOGGING = None
TEST = True TEST = True
CELERY_TASK_ALWAYS_EAGER = True
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS'] _DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
# Load subapps's INSTALLED_APPS # Load subapps's INSTALLED_APPS

View File

@ -1,12 +1,26 @@
"""passbook core signals""" """passbook core signals"""
from django.core.signals import Signal from django.core.signals import Signal
from django.dispatch import receiver
# from django.db.models.signals import post_save, pre_delete from passbook.core.exceptions import PasswordPolicyInvalid
# from django.dispatch import receiver
# from passbook.core.models import Invitation, User
user_signed_up = Signal(providing_args=['request', 'user']) user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation']) invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user']) invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password']) password_changed = Signal(providing_args=['user', 'password'])
@receiver(password_changed)
# pylint: disable=unused-argument
def password_policy_checker(sender, password, **kwargs):
"""Run password through all password policies which are applied to the user"""
from passbook.core.models import PasswordFactor
from passbook.core.policies import PolicyEngine
setattr(sender, '__password__', password)
_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)
passing, messages = policy_engine.result
if not passing:
raise PasswordPolicyInvalid(*messages)

17
passbook/core/tasks.py Normal file
View File

@ -0,0 +1,17 @@
"""passbook core tasks"""
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from passbook.core.celery import CELERY_APP
from passbook.lib.config import CONFIG
@CELERY_APP.task()
def send_email(to_address, subject, template, context):
"""Send Email to user(s)"""
html_content = render_to_string(template, context=context)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address])
msg.attach_alternative(html_content, "text/html")
msg.send()

View File

@ -6,6 +6,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> <title>
{% block title %} {% block title %}
{% title %} {% title %}
@ -19,6 +20,7 @@
.login-pf { .login-pf {
background-attachment: fixed; background-attachment: fixed;
scroll-behavior: smooth; scroll-behavior: smooth;
background-size: cover;
} }
</style> </style>
{% block head %} {% block head %}

View File

@ -0,0 +1,84 @@
{% extends 'email/base.html' %}
{% load inline %}
{% load i18n %}
{% block pre_header %}
{% trans "We're thrilled to have you here! Get ready to dive into your new account." %}
{% endblock %}
{% block content %}
<!-- HERO -->
<tr>
<td bgcolor="#3625b7" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<tr>
<td bgcolor="#566572" align="center" valign="top"
style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #8F9BA3; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 32px; font-weight: 400; margin: 0; color: #E9ECEF;">{% trans 'Welcome!' %}
</h1>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 20px 30px 40px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "We're excited to have you get started. First, you need to confirm your account. Just press the button below."%}
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#566572" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#566572" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3625b7"><a
href="{{ url }}" target="_blank"
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3625b7; display: inline-block;">{% trans 'Confirm Account' %}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 0px 30px 0px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "If that doesn't work, copy and paste the following link in your browser:" %}</p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 20px 30px 20px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;"><a href="{{ url }}" target="_blank" style="color: #3625b7;">{{ url }}</a></p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 0px 30px 20px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "If you have any questions, just reply to this email—we're always happy to help out." %}
</p>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends "email/base.html" %}
{% load utils %}
{% load i18n %}
{% block pre_header %}
{% trans "Looks like you tried signing in a few too many times. Let's see if we can get you back into your account." %}
{% endblock %}
{% block content %}
{% config 'passbook.branding' as branding %}
<!-- HERO -->
<tr>
<td bgcolor="#7c72dc" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">{% trans 'Trouble signing in?' %}</h1>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">{% trans "Resetting your password is easy. Just press the button below and follow the instructions. We'll have you up and running in no time." %}</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#ffffff" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#7c72dc"><a href="{{ url }}" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #7c72dc; display: inline-block;">{% trans 'Reset Password' %}</a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY CALLOUT -->
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
<!-- HEADLINE -->
<tr>
<td bgcolor="#111111" align="left" style="padding: 40px 30px 20px 30px; color: #ffffff; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<h2 style="font-size: 24px; font-weight: 400; margin: 0;">{% trans 'Want a more secure account?' %}</h2>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#111111" align="left" style="padding: 0px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">{% trans 'We support two-factor authentication to help keep your information private.' %}</p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#111111" align="left" style="padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;"><a href="http://litmus.com" target="_blank" style="color: #7c72dc;">{% trans 'See how easy it is to get started' %}</a></p>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}

View File

@ -0,0 +1,129 @@
{% load inline %}
{% load utils %}
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<title>{% config passbook.branding %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
/* CLIENT-SPECIFIC STYLES */
body, table, td, a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* RESET STYLES */
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
table {
border-collapse: collapse !important;
}
body {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
</style>
</head>
<body style="background-color: #1b2a32; margin: 0 !important; padding: 0 !important;">
<!-- HIDDEN PREHEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Metropolis', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
{% block pre_header %}
{% endblock %}
</div>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!-- LOGO -->
<tr>
<td bgcolor="#3625b7" align="center">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<tr>
<td align="center" valign="top" style="padding: 40px 10px 40px 10px;">
<a href="" target="_blank">
<img alt="Logo" src="{% inline_static 'assets/dark.svg' %}" width="64" height="64"
style="display: block; width: 64px; max-width: 64px; min-width: 64px; font-family: 'Metropolis', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;"
border="0">
</a>
</td>
</tr>
</table>
</td>
</tr>
{% block content %}
{% endblock %}
<!-- SUPPORT CALLOUT -->
<!-- <tr>
<td bgcolor="#1b2a32" align="center" style="padding: 30px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
HEADLINE
<tr>
<td bgcolor="#566572" align="center" style="padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<h2 style="font-size: 20px; font-weight: 400; color: ##E9ECEF; margin: 0;">Need more help?</h2>
<p style="margin: 0;"><a href="http://litmus.com" target="_blank" style="color: #3625b7;">We&rsquo;re
here, ready to talk</a></p>
</td>
</tr>
</table>
</td>
</tr> -->
<!-- FOOTER -->
<tr>
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<!-- NAVIGATION -->
<tr>
<td bgcolor="#1b2a32" align="left" style="padding: 30px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;">
</p>
</td>
</tr>
<!-- ADDRESS -->
<tr>
<td bgcolor="#1b2a32" align="left" style="padding: 0px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;"><a href="{% config 'passbook.branding' %}">{% config 'passbook.branding' %}</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,26 @@
{% extends "email/base.html" %}
{% block content %}
<tr>
<td bgcolor="#3625b7" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<tr>
<td bgcolor="#566572" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #8F9BA3; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 32px; font-weight: 400; margin: 0; color: #E9ECEF;">{{ title }}!</h1>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<tr>
<td bgcolor="#566572" align="left" style="padding: 20px 30px 40px 30px; color: #E9ECEF; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">{{ body }}</p>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
<div class="form-group"> <div class="form-group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %} {% if field.field.widget|fieldtype == 'RadioSelect' %}
<label class="col-sm-2 control-label" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}"> <label class="col-sm-2 control-label" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }} {{ field.label }}
@ -40,11 +40,9 @@
</span> </span>
{% endif %} {% endif %}
{% for error in field.errors %} {% for error in field.errors %}
<hr> <span class="help-block">
<div class="alert alert-danger"> {{ error }}
<span class="pficon pficon-error-circle-o"></span> </span>
<strong>{{ error }}</strong>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@ -3,19 +3,23 @@
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
<div class="form-group login-pf-settings"> <div class="form-group login-pf-settings {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %} {% if field.field.widget|fieldtype == 'RadioSelect' %}
<label class="col-sm-2 control-label" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}"> <label class="col-sm-2 control-label" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }} {{ field.label }}
</label> </label>
{% for c in field %} {% for c in field %}
<div class="radio col-sm-10"> <div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}" name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}" {% if c.data.selected %} checked {% endif %}> <input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="col-sm-2 control-label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label> <label class="col-sm-2 control-label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div> </div>
{% endfor %} {% endfor %}
{% elif field.field.widget|fieldtype == 'Select' %} {% elif field.field.widget|fieldtype == 'Select' %}
<label class="col-sm-2 control-label" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}"> <label class="col-sm-2 control-label" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }} {{ field.label }}
</label> </label>
<div class="select col-sm-10"> <div class="select col-sm-10">
@ -26,7 +30,8 @@
{{ field }} {{ field.label }} {{ field }} {{ field.label }}
</label> </label>
{% else %} {% else %}
<label class="col-sm-2 sr-only" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}"> <label class="col-sm-2 sr-only" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }} {{ field.label }}
</label> </label>
{{ field|css_class:'form-control input-lg' }} {{ field|css_class:'form-control input-lg' }}
@ -37,11 +42,9 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% for error in field.errors %} {% for error in field.errors %}
<hr> <span class="help-block">
<div class="alert alert-danger alert-block"> {{ error }}
<span class="pficon pficon-error-circle-o"></span> </span>
<strong>{{ error }}</strong>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}

View File

@ -3,6 +3,7 @@
from django import template from django import template
from passbook.core.models import Factor from passbook.core.models import Factor
from passbook.core.policies import PolicyEngine
register = template.Library() register = template.Library()
@ -14,6 +15,8 @@ def user_factors(context):
matching_factors = [] matching_factors = []
for factor in _all_factors: for factor in _all_factors:
_link = factor.has_user_settings() _link = factor.has_user_settings()
if factor.passes(user) and _link: policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user)
if policy_engine.result[0] and _link:
matching_factors.append(_link) matching_factors.append(_link)
return matching_factors return matching_factors

View File

@ -15,8 +15,7 @@ class TestAuthenticationViews(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.sign_up_data = { self.sign_up_data = {
'first_name': 'Test', 'name': 'Test',
'last_name': 'User',
'username': 'beryjuorg', 'username': 'beryjuorg',
'email': 'unittest@passbook.beryju.org', 'email': 'unittest@passbook.beryju.org',
'password': 'B3ryju0rg!', 'password': 'B3ryju0rg!',

View File

@ -1,24 +1,28 @@
"""Core views""" """passbook core authentication views"""
from logging import getLogger from logging import getLogger
from typing import Dict from typing import Dict
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login, logout from django.contrib.auth import login, logout
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.forms.utils import ErrorList
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import View from django.views import View
from django.views.generic import FormView from django.views.generic import FormView
from passbook.core.auth.view import AuthenticationView from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.authentication import LoginForm, SignUpForm from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up from passbook.core.signals import invitation_used, user_signed_up
from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
class LoginView(UserPassesTestMixin, FormView): class LoginView(UserPassesTestMixin, FormView):
"""Allow users to sign in""" """Allow users to sign in"""
@ -69,13 +73,14 @@ class LoginView(UserPassesTestMixin, FormView):
return self.invalid_login(self.request) return self.invalid_login(self.request)
self.request.session.flush() self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return redirect(reverse('passbook_core:auth-process')) return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse: def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
"""Handle login for disabled users/invalid login attempts""" """Handle login for disabled users/invalid login attempts"""
messages.error(request, _('Failed to authenticate.')) messages.error(request, _('Failed to authenticate.'))
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
class LogoutView(LoginRequiredMixin, View): class LogoutView(LoginRequiredMixin, View):
"""Log current user out""" """Log current user out"""
@ -138,14 +143,30 @@ class SignUpView(UserPassesTestMixin, FormView):
def form_valid(self, form: SignUpForm) -> HttpResponse: def form_valid(self, form: SignUpForm) -> HttpResponse:
"""Create user""" """Create user"""
try:
self._user = SignUpView.create_user(form.cleaned_data, self.request) self._user = SignUpView.create_user(form.cleaned_data, self.request)
except PasswordPolicyInvalid as exc:
# Manually inject error into form
# pylint: disable=protected-access
errors = form._errors.setdefault("password", ErrorList())
for error in exc.messages:
errors.append(error)
return self.form_invalid(form)
needs_confirmation = True needs_confirmation = True
if self._invitation and not self._invitation.needs_confirmation: if self._invitation and not self._invitation.needs_confirmation:
needs_confirmation = False needs_confirmation = False
if needs_confirmation: if needs_confirmation:
nonce = Nonce.objects.create(user=self._user) nonce = Nonce.objects.create(user=self._user)
LOGGER.debug(str(nonce.uuid)) LOGGER.debug(str(nonce.uuid))
# TODO: Send E-Mail to user # Send email to user
send_email.delay(self._user.email, _('Confirm your account.'),
'email/account_confirm.html', {
'url': self.request.build_absolute_uri(
reverse('passbook_core:auth-sign-up-confirm', kwargs={
'nonce': nonce.uuid
})
)
})
self._user.is_active = False self._user.is_active = False
self._user.save() self._user.save()
self.consume_invitation() self.consume_invitation()
@ -176,16 +197,17 @@ class SignUpView(UserPassesTestMixin, FormView):
The user created The user created
Raises: Raises:
SignalException: if any signals raise an exception. This also deletes the created user. PasswordPolicyInvalid: if any policy are not fulfilled.
This also deletes the created user.
""" """
# Create user # Create user
new_user = User.objects.create_user( new_user = User.objects.create(
username=data.get('username'), username=data.get('username'),
email=data.get('email'), email=data.get('email'),
first_name=data.get('first_name'), name=data.get('name'),
last_name=data.get('last_name'),
) )
new_user.is_active = True new_user.is_active = True
try:
new_user.set_password(data.get('password')) new_user.set_password(data.get('password'))
new_user.save() new_user.save()
request.user = new_user request.user = new_user
@ -195,6 +217,10 @@ class SignUpView(UserPassesTestMixin, FormView):
user=new_user, user=new_user,
request=request) request=request)
return new_user return new_user
except PasswordPolicyInvalid as exc:
new_user.delete()
raise exc
class SignUpConfirmView(View): class SignUpConfirmView(View):
"""Confirm registration from Nonce""" """Confirm registration from Nonce"""

View File

@ -1,20 +1,27 @@
"""passbook core user views""" """passbook core user views"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import logout, update_session_auth_hash from django.contrib.auth import logout, update_session_auth_hash
from django.contrib.messages.views import SuccessMessageMixin
from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import DeleteView, FormView, UpdateView from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
class UserSettingsView(UpdateView): class UserSettingsView(SuccessMessageMixin, UpdateView):
"""Update User settings""" """Update User settings"""
template_name = 'user/settings.html' template_name = 'user/settings.html'
form_class = UserDetailForm form_class = UserDetailForm
success_message = _('Successfully updated user.')
success_url = reverse_lazy('passbook_core:user-settings')
def get_object(self): def get_object(self):
return self.request.user return self.request.user
@ -38,10 +45,20 @@ class UserChangePasswordView(FormView):
template_name = 'login/form_with_user.html' template_name = 'login/form_with_user.html'
def form_valid(self, form: PasswordChangeForm): def form_valid(self, form: PasswordChangeForm):
try:
self.request.user.set_password(form.cleaned_data.get('password')) self.request.user.set_password(form.cleaned_data.get('password'))
self.request.user.save() self.request.user.save()
update_session_auth_hash(self.request, self.request.user) update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Successfully changed password')) messages.success(self.request, _('Successfully changed password'))
except PasswordPolicyInvalid as exc:
# Manually inject error into form
# pylint: disable=protected-access
errors = form._errors.setdefault("password_repeat", ErrorList(''))
# pylint: disable=protected-access
errors = form._errors.setdefault("password", ErrorList())
for error in exc.messages:
errors.append(error)
return self.form_invalid(form)
return redirect('passbook_core:overview') return redirect('passbook_core:overview')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2019-02-27 15:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_hibp_policy', '0002_auto_20190225_1912'),
]
operations = [
migrations.AlterModelOptions(
name='haveibeenpwendpolicy',
options={'verbose_name': 'Have I Been Pwned Policy', 'verbose_name_plural': 'Have I Been Pwned Policies'},
),
]

View File

@ -1,6 +1,6 @@
"""passbook HIBP Models""" """passbook HIBP Models"""
from hashlib import sha1 from hashlib import sha1
from logging import getLogger
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -8,6 +8,7 @@ from requests import get
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
LOGGER = getLogger(__name__)
class HaveIBeenPwendPolicy(Policy): class HaveIBeenPwendPolicy(Policy):
"""Check if password is on HaveIBeenPwned's list by upload the first """Check if password is on HaveIBeenPwned's list by upload the first
@ -33,11 +34,12 @@ class HaveIBeenPwendPolicy(Policy):
full_hash, count = line.split(':') full_hash, count = line.split(':')
if pw_hash[5:] == full_hash.lower(): if pw_hash[5:] == full_hash.lower():
final_count = int(count) final_count = int(count)
LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5])
if final_count > self.allowed_count: if final_count > self.allowed_count:
return False return False, _("Password exists on %(count)d online lists." % {'count': final_count})
return True return True
class Meta: class Meta:
verbose_name = _('have i been pwned Policy') verbose_name = _('Have I Been Pwned Policy')
verbose_name_plural = _('have i been pwned Policies') verbose_name_plural = _('Have I Been Pwned Policies')

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header""" """Passbook ldap app Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -39,7 +39,7 @@ class LDAPSourceForm(forms.ModelForm):
# (MODE_CREATE_USERS, _('Create Users')) # (MODE_CREATE_USERS, _('Create Users'))
# ) # )
# namespace = 'supervisr.mod.auth.ldap' # namespace = 'passbook.ldap'
# settings = ['enabled', 'mode'] # settings = ['enabled', 'mode']
# widgets = { # widgets = {
@ -51,7 +51,7 @@ class LDAPSourceForm(forms.ModelForm):
# class ConnectionSettings(SettingsForm): # class ConnectionSettings(SettingsForm):
# """Connection settings form""" # """Connection settings form"""
# namespace = 'supervisr.mod.auth.ldap' # namespace = 'passbook.ldap'
# settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain'] # settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain']
# attrs_map = { # attrs_map = {
@ -68,7 +68,7 @@ class LDAPSourceForm(forms.ModelForm):
# class AuthenticationBackendSettings(SettingsForm): # class AuthenticationBackendSettings(SettingsForm):
# """Authentication backend settings""" # """Authentication backend settings"""
# namespace = 'supervisr.mod.auth.ldap' # namespace = 'passbook.ldap'
# settings = ['base'] # settings = ['base']
# attrs_map = { # attrs_map = {
@ -79,7 +79,7 @@ class LDAPSourceForm(forms.ModelForm):
# class CreateUsersSettings(SettingsForm): # class CreateUsersSettings(SettingsForm):
# """Create users settings""" # """Create users settings"""
# namespace = 'supervisr.mod.auth.ldap' # namespace = 'passbook.ldap'
# settings = ['create_base'] # settings = ['create_base']
# attrs_map = { # attrs_map = {

View File

@ -129,7 +129,7 @@ class LDAPConnector:
# Create the user data. # Create the user data.
field_map = { field_map = {
'username': '%(' + USERNAME_FIELD + ')s', 'username': '%(' + USERNAME_FIELD + ')s',
'first_name': '%(givenName)s %(sn)s', 'name': '%(givenName)s %(sn)s',
'email': '%(mail)s', 'email': '%(mail)s',
} }
user_fields = {} user_fields = {}
@ -224,9 +224,9 @@ class LDAPConnector:
'cn': str(username), 'cn': str(username),
'description': str('t=' + time()), 'description': str('t=' + time()),
'sAMAccountName': str(username_trunk), 'sAMAccountName': str(username_trunk),
'givenName': str(user.first_name), 'givenName': str(user.name),
'displayName': str(user.username), 'displayName': str(user.username),
'name': str(user.first_name), 'name': str(user.name),
'mail': str(user.email), 'mail': str(user.email),
'userPrincipalName': str(username + '@' + self._source.domain), 'userPrincipalName': str(username + '@' + self._source.domain),
'objectClass': ['top', 'person', 'organizationalPerson', 'user'], 'objectClass': ['top', 'person', 'organizationalPerson', 'user'],

View File

@ -57,7 +57,7 @@ class LDAPSource(Source):
# class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel): # class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel):
# """Model to map an LDAP Group to a supervisr group""" # """Model to map an LDAP Group to a passbook group"""
# ldap_dn = models.TextField() # ldap_dn = models.TextField()
# group = models.ForeignKey(Group, on_delete=models.CASCADE) # group = models.ForeignKey(Group, on_delete=models.CASCADE)

View File

@ -2,7 +2,7 @@
# from django.conf.urls import url # from django.conf.urls import url
# from supervisr.mod.auth.ldap import views # from passbook.mod.auth.ldap import views
# urlpatterns = [ # urlpatterns = [
# url(r'^settings/$', views.admin_settings, name='admin_settings'), # url(r'^settings/$', views.admin_settings, name='admin_settings'),

View File

@ -1,4 +1,4 @@
# """Supervisr Mod LDAP Views""" # """passbook LDAP Views"""
# from django.contrib import messages # from django.contrib import messages
@ -8,7 +8,7 @@
# from django.urls import reverse # from django.urls import reverse
# from django.utils.translation import ugettext as _ # from django.utils.translation import ugettext as _
# from supervisr.mod.auth.ldap.forms import (AuthenticationBackendSettings, # from passbook.ldap.forms import (AuthenticationBackendSettings,
# ConnectionSettings, # ConnectionSettings,
# CreateUsersSettings, # CreateUsersSettings,
# GeneralSettingsForm) # GeneralSettingsForm)
@ -34,5 +34,5 @@
# if form.is_valid(): # if form.is_valid():
# update_count += form.save() # update_count += form.save()
# messages.success(request, _('Successfully updated %d settings.' % update_count)) # messages.success(request, _('Successfully updated %d settings.' % update_count))
# return redirect(reverse('supervisr_mod_auth_ldap:admin_settings')) # return redirect(reverse('passbook_ldap:admin_settings'))
# return render(request, 'ldap/settings.html', render_data) # return render(request, 'ldap/settings.html', render_data)

View File

@ -1,2 +1,2 @@
"""passbook lib""" """passbook lib"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -77,10 +77,9 @@ ldap:
email: mail # or userPrincipalName email: mail # or userPrincipalName
user_attribute_map: user_attribute_map:
active_directory: active_directory:
sAMAccountName: username username: "%(sAMAccountName)s"
mail: email email: "%(mail)s"
given_name: first_name name: "%(displayName)"
name: last_name
oauth_client: oauth_client:
# List of python packages with sources types to load. # List of python packages with sources types to load.
types: types:

View File

@ -207,3 +207,15 @@ def gravatar(email, size=None, rating=None):
gravatar_url += '?' + urlencode(parameters, doseq=True) gravatar_url += '?' + urlencode(parameters, doseq=True)
return escape(gravatar_url) return escape(gravatar_url)
@register.filter
def verbose_name(obj):
"""Return Object's Verbose Name"""
return obj._meta.verbose_name
@register.filter
def form_verbose_name(obj):
"""Return ModelForm's Object's Verbose Name"""
return obj._meta.model._meta.verbose_name

View File

@ -0,0 +1,20 @@
"""passbook core inlining template tags"""
import os
from django import template
from django.conf import settings
register = template.Library()
@register.simple_tag()
def inline_static(path):
"""Inline static asset. If file is binary, return b64 representation"""
prefix = 'data:image/svg+xml;utf8,'
data = ''
full_path = settings.STATIC_ROOT + '/' + path
if os.path.exists(full_path):
if full_path.endswith('.svg'):
with open(full_path) as _file:
data = _file.read()
return prefix + data

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header""" """passbook oauth_client Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -2,7 +2,6 @@
import json import json
from logging import getLogger from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
@ -50,12 +49,11 @@ class DiscordOAuth2Callback(OAuthCallback):
client_class = DiscordOAuth2Client client_class = DiscordOAuth2Client
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('username'), 'username': info.get('username'),
'email': info.get('email', 'None'), 'email': info.get('email', 'None'),
'first_name': info.get('username'), 'name': info.get('username'),
'password': None, 'password': None,
} }
discord_user = user_get_or_create(user_model=user, **user_data) discord_user = user_get_or_create(**user_data)
return discord_user return discord_user

View File

@ -1,7 +1,5 @@
"""Facebook OAuth Views""" """Facebook OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
@ -22,12 +20,11 @@ class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback""" """Facebook OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('name'), 'username': info.get('name'),
'email': info.get('email', ''), 'email': info.get('email', ''),
'first_name': info.get('name'), 'name': info.get('name'),
'password': None, 'password': None,
} }
fb_user = user_get_or_create(user_model=user, **user_data) fb_user = user_get_or_create(**user_data)
return fb_user return fb_user

View File

@ -1,7 +1,5 @@
"""GitHub OAuth Views""" """GitHub OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback from passbook.oauth_client.views.core import OAuthCallback
@ -12,12 +10,11 @@ class GitHubOAuth2Callback(OAuthCallback):
"""GitHub OAuth2 Callback""" """GitHub OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('login'), 'username': info.get('login'),
'email': info.get('email', ''), 'email': info.get('email', ''),
'first_name': info.get('name'), 'name': info.get('name'),
'password': None, 'password': None,
} }
gh_user = user_get_or_create(user_model=user, **user_data) gh_user = user_get_or_create(**user_data)
return gh_user return gh_user

View File

@ -1,6 +1,4 @@
"""Google OAuth Views""" """Google OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
@ -21,12 +19,11 @@ class GoogleOAuth2Callback(OAuthCallback):
"""Google OAuth2 Callback""" """Google OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('email'), 'username': info.get('email'),
'email': info.get('email', ''), 'email': info.get('email', ''),
'first_name': info.get('name'), 'name': info.get('name'),
'password': None, 'password': None,
} }
google_user = user_get_or_create(user_model=user, **user_data) google_user = user_get_or_create(**user_data)
return google_user return google_user

View File

@ -2,7 +2,6 @@
import json import json
from logging import getLogger from logging import getLogger
from django.contrib.auth import get_user_model
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from requests.exceptions import RequestException from requests.exceptions import RequestException
@ -59,12 +58,11 @@ class RedditOAuth2Callback(OAuthCallback):
client_class = RedditOAuth2Client client_class = RedditOAuth2Client
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('name'), 'username': info.get('name'),
'email': None, 'email': None,
'first_name': info.get('name'), 'name': info.get('name'),
'password': None, 'password': None,
} }
reddit_user = user_get_or_create(user_model=user, **user_data) reddit_user = user_get_or_create(**user_data)
return reddit_user return reddit_user

View File

@ -3,7 +3,6 @@
import json import json
from logging import getLogger from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
@ -44,12 +43,11 @@ class SupervisrOAuthCallback(OAuthCallback):
return info['pk'] return info['pk']
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('username'), 'username': info.get('username'),
'email': info.get('email', ''), 'email': info.get('email', ''),
'first_name': info.get('first_name'), 'name': info.get('first_name'),
'password': None, 'password': None,
} }
sv_user = user_get_or_create(user_model=user, **user_data) sv_user = user_get_or_create(**user_data)
return sv_user return sv_user

View File

@ -2,7 +2,6 @@
from logging import getLogger from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuthClient from passbook.oauth_client.clients import OAuthClient
@ -36,12 +35,11 @@ class TwitterOAuthCallback(OAuthCallback):
client_class = TwitterOAuthClient client_class = TwitterOAuthClient
def get_or_create_user(self, source, access, info): def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = { user_data = {
user.USERNAME_FIELD: info.get('screen_name'), 'username': info.get('screen_name'),
'email': info.get('email', ''), 'email': info.get('email', ''),
'first_name': info.get('name'), 'name': info.get('name'),
'password': None, 'password': None,
} }
tw_user = user_get_or_create(user_model=user, **user_data) tw_user = user_get_or_create(**user_data)
return tw_user return tw_user

View File

@ -1,16 +1,17 @@
"""OAuth Client User Creation Utils""" """OAuth Client User Creation Utils"""
from django.contrib.auth import get_user_model
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from passbook.core.models import User
def user_get_or_create(user_model=None, **kwargs):
def user_get_or_create(**kwargs):
"""Create user or return existing user""" """Create user or return existing user"""
if user_model is None:
user_model = get_user_model()
try: try:
new_user = user_model.objects.create_user(**kwargs) new_user = User.objects.create_user(**kwargs)
except IntegrityError: except IntegrityError:
# TODO: Fix potential username change vuln # At this point we've already checked that there is no existing connection
new_user = user_model.objects.get(username=kwargs['username']) # to any user. Hence if we can't create the user,
kwargs['username'] = '%s_1' % kwargs['username']
new_user = User.objects.create_user(**kwargs)
return new_user return new_user

View File

@ -113,7 +113,9 @@ class OAuthCallback(OAuthClientMixin, View):
) )
user = authenticate(source=self.source, identifier=identifier, request=request) user = authenticate(source=self.source, identifier=identifier, request=request)
if user is None: if user is None:
LOGGER.debug("Handling new user")
return self.handle_new_user(self.source, connection, info) return self.handle_new_user(self.source, connection, info)
LOGGER.debug("Handling existing user")
return self.handle_existing_user(self.source, user, connection, info) return self.handle_existing_user(self.source, user, connection, info)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header""" """passbook oauth_provider Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -1,2 +1,2 @@
"""passbook otp Header""" """passbook otp Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -27,7 +27,7 @@ class GitHubUserView(View):
"received_events_url": "", "received_events_url": "",
"type": "User", "type": "User",
"site_admin": False, "site_admin": False,
"name": "%s %s" % (request.user.first_name, request.user.last_name), "name": request.user.name,
"company": "", "company": "",
"blog": "", "blog": "",
"location": "", "location": "",

View File

@ -1,2 +1,2 @@
"""passbook saml_idp Header""" """passbook saml_idp Header"""
__version__ = '0.0.11-alpha' __version__ = '0.0.13-alpha'

View File

@ -157,7 +157,7 @@ class Processor:
{ {
'FriendlyName': 'cn', 'FriendlyName': 'cn',
'Name': 'urn:oid:2.5.4.3', 'Name': 'urn:oid:2.5.4.3',
'Value': self._django_request.user.first_name, 'Value': self._django_request.user.name,
}, },
{ {
'FriendlyName': 'mail', 'FriendlyName': 'mail',

View File

@ -40,11 +40,11 @@ class SAMLProvider(Provider):
def link_download_metadata(self): def link_download_metadata(self):
"""Get link to download XML metadata for admin interface""" """Get link to download XML metadata for admin interface"""
# pylint: disable=no-member try:
if self.application:
# pylint: disable=no-member # pylint: disable=no-member
return reverse('passbook_saml_idp:metadata_xml', return reverse('passbook_saml_idp:metadata_xml',
kwargs={'application': self.application.slug}) kwargs={'application': self.application.slug})
except Provider.application.RelatedObjectDoesNotExist:
return None return None
class Meta: class Meta:

View File

@ -8,7 +8,9 @@ from django.core.validators import URLValidator
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render, reverse from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt
from signxml.util import strip_pem_header from signxml.util import strip_pem_header
from passbook.core.models import Application from passbook.core.models import Application
@ -26,6 +28,7 @@ def _generate_response(request, provider: SAMLProvider):
"""Generate a SAML response using processor_instance and return it in the proper Django """Generate a SAML response using processor_instance and return it in the proper Django
response.""" response."""
try: try:
provider.processor.init_deep_link(request, '')
ctx = provider.processor.generate_response() ctx = provider.processor.generate_response()
ctx['remote'] = provider ctx['remote'] = provider
ctx['is_login'] = True ctx['is_login'] = True
@ -54,10 +57,11 @@ class ProviderMixin:
return self._provider return self._provider
class LoginBeginView(CSRFExemptMixin, View): class LoginBeginView(LoginRequiredMixin, View):
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and """Receives a SAML 2.0 AuthnRequest from a Service Provider and
stores it in the session prior to enforcing login.""" stores it in the session prior to enforcing login."""
@method_decorator(csrf_exempt)
def dispatch(self, request, application): def dispatch(self, request, application):
if request.method == 'POST': if request.method == 'POST':
source = request.POST source = request.POST
@ -71,12 +75,12 @@ class LoginBeginView(CSRFExemptMixin, View):
return HttpResponseBadRequest('the SAML request payload is missing') return HttpResponseBadRequest('the SAML request payload is missing')
request.session['RelayState'] = source.get('RelayState', '') 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 'application': application
}) }))
class RedirectToSPView(View): class RedirectToSPView(LoginRequiredMixin, View):
"""Return autosubmit form""" """Return autosubmit form"""
def get(self, request, acs_url, saml_response, relay_state): def get(self, request, acs_url, saml_response, relay_state):
@ -90,16 +94,17 @@ class RedirectToSPView(View):
}) })
class LoginProcessView(ProviderMixin, View): class LoginProcessView(ProviderMixin, LoginRequiredMixin, View):
"""Processor-based login continuation. """Processor-based login continuation.
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.""" Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
def dispatch(self, request, application): def get(self, request, application):
"""Handle get request, i.e. render form"""
LOGGER.debug("Request: %s", request) LOGGER.debug("Request: %s", request)
# Check if user has access # Check if user has access
access = True access = True
# TODO: Check access here # TODO: Check access here
if self.provider.skip_authorization and access: if self.provider.application.skip_authorization and access:
ctx = self.provider.processor.generate_response() ctx = self.provider.processor.generate_response()
# TODO: AuditLog Skipped Authz # TODO: AuditLog Skipped Authz
return RedirectToSPView.as_view()( return RedirectToSPView.as_view()(
@ -107,7 +112,19 @@ class LoginProcessView(ProviderMixin, View):
acs_url=ctx['acs_url'], acs_url=ctx['acs_url'],
saml_response=ctx['saml_response'], saml_response=ctx['saml_response'],
relay_state=ctx['relay_state']) relay_state=ctx['relay_state'])
if request.method == 'POST' and request.POST.get('ACSUrl', None) and access: try:
full_res = _generate_response(request, self.provider)
return full_res
except exceptions.CannotHandleAssertion as exc:
LOGGER.debug(exc)
def post(self, request, application):
"""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:
# User accepted request # User accepted request
# TODO: AuditLog accepted # TODO: AuditLog accepted
return RedirectToSPView.as_view()( return RedirectToSPView.as_view()(
@ -122,7 +139,7 @@ class LoginProcessView(ProviderMixin, View):
LOGGER.debug(exc) LOGGER.debug(exc)
class LogoutView(CSRFExemptMixin, View): class LogoutView(CSRFExemptMixin, LoginRequiredMixin, View):
"""Allows a non-SAML 2.0 URL to log out the user and """Allows a non-SAML 2.0 URL to log out the user and
returns a standard logged-out page. (SalesForce and others use this method, returns a standard logged-out page. (SalesForce and others use this method,
though it's technically not SAML 2.0).""" though it's technically not SAML 2.0)."""