Compare commits

...

85 Commits

Author SHA1 Message Date
4439378fd4 bump version: 0.1.1-beta -> 0.1.2-beta 2019-03-07 14:14:51 +01:00
acf65eafdd make naming of Providers more consistent 2019-03-07 14:14:49 +01:00
c2ebff55ef fix IDP-initiated login not working 2019-03-07 14:10:06 +01:00
99c82676b6 Add some more failsafe for administration 2019-03-07 14:09:52 +01:00
4991e9b825 Merge branch '1-suspicious-request' into 'master'
fix broken E-Mail templatetag

Closes #1

See merge request BeryJu.org/passbook!5
2019-03-03 20:18:23 +00:00
612f95c3ba fix broken E-Mail templatetag 2019-03-03 21:05:17 +01:00
cd91d5ca15 Merge branch '1-suspicious-request' into 'master'
Resolve "Suspicious request detector (many invalid logins from one IP, many attempts on one username, etc)"

Closes #1

See merge request BeryJu.org/passbook!3
2019-03-03 20:04:56 +00:00
cbbbb5dc08 Merge branch '20-sentry' into 'master'
Resolve "Sentry Error Tracking"

Closes #20

See merge request BeryJu.org/passbook!4
2019-03-03 19:58:18 +00:00
c1640b9411 fix prospector/isort errors 2019-03-03 20:54:23 +01:00
a4842c1f95 add sentry configuration 2019-03-03 20:48:31 +01:00
a4707ddc54 fix failing unittests 2019-03-03 20:34:00 +01:00
fb82d56307 create suspicious request detector and policy, add request to policy engine 2019-03-03 20:26:25 +01:00
1a1005f80d remove audit's LoginAttempt 2019-03-03 20:13:54 +01:00
e86cae6cac Merge branch '18-password-expiry' into 'master'
Resolve "Password Expiry"

Closes #18

See merge request BeryJu.org/passbook!2
2019-03-03 16:53:31 +00:00
0b282f45e0 fix pylint messages 2019-03-03 17:45:20 +01:00
791e88ffc1 Fix negate on FieldMatcherPolicy 2019-03-03 17:21:58 +01:00
7bd3c4bccf Better handle Policy.action and Policy.negate 2019-03-03 17:12:53 +01:00
722e2e4050 Show warning when un-attached policies exist 2019-03-03 17:12:35 +01:00
c7fc444c95 add password policy 2019-03-03 17:12:05 +01:00
20ad062814 Log SAML Authorization actions 2019-03-03 00:34:34 +01:00
fcb5d36e07 cleanup SAML urls 2019-03-03 00:07:40 +01:00
9b131b619f Show warning message when no Factor exists 2019-03-02 23:54:40 +01:00
54427f7c68 use HTML5 autocomplete values to better handle password managers 2019-03-02 23:19:58 +01:00
35eef9c28d improve worker warning 2019-03-02 22:41:25 +01:00
e88a82553d use separate Form for Admin user editing (allow is_staff and is_active) 2019-03-02 22:41:14 +01:00
01a9520140 add import_users script to import users from CSV with already hashed passwords 2019-03-02 22:40:47 +01:00
46667615c3 switch releases to beta 2019-02-27 17:47:41 +01:00
c6721a83a4 bump version: 0.1.1-alpha -> 0.1.1-beta 2019-02-27 17:45:10 +01:00
46866e8ef0 bump version: 0.1.0-beta -> 0.1.1-alpha 2019-02-27 17:43:28 +01:00
4a49681127 Fix docker build failing 2019-02-27 17:43:24 +01:00
4c3fced4e9 bump version: 0.1.0-alpha -> 0.1.0-beta 2019-02-27 16:45:52 +01:00
172347d90f bump version: 0.0.13-alpha -> 0.1.0-alpha 2019-02-27 16:42:52 +01:00
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
e7fb48eba2 bump version: 0.0.10-alpha -> 0.0.11-alpha 2019-02-26 13:06:26 +01:00
b19b5b644d remove hardcoded passwords 2019-02-26 13:06:22 +01:00
250b6691d4 bump version: 0.0.9-alpha -> 0.0.10-alpha 2019-02-26 12:44:02 +01:00
e3b02a6e78 fix isort/pylint issues 2019-02-26 12:43:59 +01:00
e94ef34d8f bump version: 0.0.8-alpha -> 0.0.9-alpha 2019-02-26 12:35:28 +01:00
49e945307a Re-enable OTP Disable View 2019-02-26 12:35:24 +01:00
edfe0e5450 fix broken Docker build and helm package 2019-02-26 12:34:51 +01:00
06b65a7882 add unittests, woo 2019-02-26 10:57:05 +01:00
ff9bc8aa70 Automatically create PasswordFactor on initial setup closes #16 2019-02-26 09:54:51 +01:00
28da67abe6 Improve partially broken Delete Views, show success message on deletion 2019-02-26 09:49:42 +01:00
39d9fe9bf0 add passbook.pretend to use passbook in applications which don't support generic OAuth 2019-02-26 09:10:37 +01:00
750117b0fd Cleanup templates, handle OAuth Provider without application better 2019-02-26 09:09:19 +01:00
983462f80d user/ -> _/user/ to prevent duplicate URLs 2019-02-26 09:08:49 +01:00
4ae31d409b directly use paths instead of including oauth2_provider's 2019-02-26 09:08:22 +01:00
98b414f3e2 add SignUp Confirmation (required by default, can be disabled in invitations) closes #6 2019-02-25 21:03:24 +01:00
a0d42092e3 add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7 2019-02-25 20:46:23 +01:00
f2569b6424 improve placeholder on login template 2019-02-25 19:43:33 +01:00
9d344d887c add more information to administrator Overview 2019-02-25 17:52:51 +01:00
7e9154a0ea bump version: 0.0.7-alpha -> 0.0.8-alpha 2019-02-25 17:39:39 +01:00
e0ef061771 fix pylint errors.... 2019-02-25 17:32:52 +01:00
b8694a7ade fix bandit error (SHA1 has to be used) 2019-02-25 17:23:42 +01:00
10d6a30f2c add experimental HaveIBeenPwned Password Policy 2019-02-25 17:21:56 +01:00
8c94aef6d0 add stub test so coverage doesn't crash 2019-02-25 17:21:06 +01:00
19bd3bfffb fix allauth imports 2019-02-25 17:20:53 +01:00
8611ac624c Make links on admin overview site actually useful 2019-02-25 17:11:52 +01:00
fa93b59a8c switch to toast notifications everywhere 2019-02-25 16:41:53 +01:00
8b66b40f0d move forgot password to PasswordFactor 2019-02-25 16:41:33 +01:00
c2756f15fc Correctly display action on Create/Update templates 2019-02-25 16:40:46 +01:00
408e205c5f add signal for password change, add field for password policies 2019-02-25 15:41:36 +01:00
5f3ab49535 fix bug when Empty username is given to LoginAttempt.attempt 2019-02-25 14:10:29 +01:00
33431ae013 improve OAuth Source Setup process, fix login template, closes #3 2019-02-25 14:10:10 +01:00
b40ac6dc5d more Icons cause everyone loves icons 2019-02-25 13:31:11 +01:00
161 changed files with 2721 additions and 931 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.7-alpha current_version = 0.1.2-beta
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>.*)
@ -9,11 +9,14 @@ tag_name = version/{new_version}
[bumpversion:part:release] [bumpversion:part:release]
optional_value = stable optional_value = stable
first_value = beta
values = values =
alpha alpha
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]
@ -34,6 +37,10 @@ values =
[bumpversion:file:passbook/lib/__init__.py] [bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/hibp_policy/__init__.py]
[bumpversion:file:passbook/password_expiry_policy/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py] [bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py] [bumpversion:file:passbook/audit/__init__.py]

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
@ -52,9 +51,9 @@ package-docker:
name: gcr.io/kaniko-project/executor:debug name: gcr.io/kaniko-project/executor:debug
entrypoint: [""] entrypoint: [""]
before_script: before_script:
- echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /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.7-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.1.2-beta
stage: build stage: build
only: only:
- tags - tags
@ -65,7 +64,7 @@ package-helm:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- helm init --client-only - helm init --client-only
- helm package helm/passbook - helm package helm/passbook
- ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz - ./manage.py nexus_upload --method put --url $NEXUS_URL --auth $NEXUS_AUTH --repo helm *.tgz
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/

View File

@ -6,10 +6,13 @@ COPY ./requirements.txt /app/
WORKDIR /app/ WORKDIR /app/
RUN mkdir /app/static/ && \ RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
mkdir /app/static/ && \
pip install -r requirements.txt && \ pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
FROM python:3.6-slim-stretch FROM python:3.6-slim-stretch
@ -20,9 +23,12 @@ COPY --from=build /app/static /app/static/
WORKDIR /app/ WORKDIR /app/
RUN pip install -r requirements.txt && \ RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
adduser --system --home /app/ passbook && \ adduser --system --home /app/ passbook && \
chown -R passbook /app/ chown -R passbook /app/ && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
USER passbook USER passbook

View File

@ -1,6 +1,5 @@
"""passbook provider""" """passbook provider"""
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from allauth_passbook.provider import PassbookProvider from allauth_passbook.provider import PassbookProvider
urlpatterns = default_urlpatterns(PassbookProvider) urlpatterns = default_urlpatterns(PassbookProvider)

View File

@ -1,10 +1,10 @@
"""passbook adapter""" """passbook adapter"""
import requests import requests
from allauth.socialaccount import app_settings from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter, from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2CallbackView, OAuth2CallbackView,
OAuth2LoginView) OAuth2LoginView)
from allauth_passbook.provider import PassbookProvider from allauth_passbook.provider import PassbookProvider

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.0.7-alpha" appVersion: "0.1.2-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: 1.0.0 version: "0.1.2-beta"
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.1.2-beta
nameOverride: "" nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.0.7-alpha' __version__ = '0.1.2-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.7-alpha' __version__ = '0.1.2-beta'

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

@ -0,0 +1,17 @@
"""passbook administrative user forms"""
from django import forms
from passbook.core.models import User
class UserForm(forms.ModelForm):
"""Update User Details"""
class Meta:
model = User
fields = ['username', 'name', 'email', 'is_staff', 'is_active']
widgets = {
'name': forms.TextInput
}

View File

@ -9,33 +9,37 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% 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>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Provider' %}</th> <th>{% trans 'Provider' %}</th>
<th></th> <th>{% trans 'Provider Type' %}</th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
{% for application in object_list %} <tbody>
<tr> {% for application in object_list %}
<td>{{ application.name }}</td> <tr>
<td>{{ application.provider }}</td> <td>{{ application.name }}</td>
<td> <td>{{ application.get_provider }}</td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <td>{{ application.get_provider|verbose_name }}</td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> <td>
</td> <a class="btn btn-default btn-sm"
</tr> href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
{% endfor %} <a class="btn btn-default btn-sm"
</tbody> href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -8,80 +8,80 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Audit Log" %}</h1> <h1><span class="pficon-catalog"></span> {% trans "Audit Log" %}</h1>
<div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view"> <div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
{% for entry in object_list %} {% for entry in object_list %}
<div class="list-group-item"> <div class="list-group-item">
<div class="list-view-pf-main-info"> <div class="list-view-pf-main-info">
<div class="list-view-pf-left"> <div class="list-view-pf-left">
<span class="fa fa-plane list-view-pf-icon-sm"></span> <span class="fa fa-plane list-view-pf-icon-sm"></span>
</div>
<div class="list-view-pf-body">
<div class="list-view-pf-description">
<div class="list-group-item-heading">
{{ entry.action }}
</div>
<div class="list-group-item-text">
{{ entry.context }}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="fa fa-clock-o"></span>
<strong>{{ entry.created }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
</div>
</div>
</div> </div>
<div class="list-view-pf-body">
<div class="list-view-pf-description">
<div class="list-group-item-heading">
{{ entry.action }}
</div>
<div class="list-group-item-text">
{{ entry.context }}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="fa fa-clock-o"></span>
<strong>{{ entry.created }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
</div>
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
// Row Checkbox Selection // Row Checkbox Selection
$("#pf-list-standard input[type='checkbox']").change(function (e) { $("#pf-list-standard input[type='checkbox']").change(function (e) {
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
$(this).closest('.list-group-item').addClass("active"); $(this).closest('.list-group-item').addClass("active");
} else { } else {
$(this).closest('.list-group-item').removeClass("active"); $(this).closest('.list-group-item').removeClass("active");
} }
}); });
// toggle dropdown menu // toggle dropdown menu
$('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () { $('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () {
var $this = $(this); var $this = $(this);
var $dropdown = $this.find('.dropdown'); var $dropdown = $this.find('.dropdown');
var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true); var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true);
$dropdown.toggleClass('dropup', space < 10); $dropdown.toggleClass('dropup', space < 10);
}); });
// allow users to select multiple list items with shift key // allow users to select multiple list items with shift key
$('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) { $('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) {
var $list = $('.list-group'); var $list = $('.list-group');
var prevIndex = $list.data('preIndex'); var prevIndex = $list.data('preIndex');
var $listItems = $list.children('.list-group-item'); var $listItems = $list.children('.list-group-item');
var $currentItem = $(this).closest('.list-group-item'); var $currentItem = $(this).closest('.list-group-item');
if (event.shiftKey && prevIndex > -1 && this.checked) { if (event.shiftKey && prevIndex > -1 && this.checked) {
var currentIndex = $listItems.index($currentItem); var currentIndex = $listItems.index($currentItem);
var $selectScope = currentIndex - prevIndex > 0 var $selectScope = currentIndex - prevIndex > 0
? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack()) ? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack())
: $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack()); : $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack());
$selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true); $selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true);
} }
$list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1); $list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1);
}); });
}); });
</script> </script>
{% include 'partials/pagination.html' %} {% include 'partials/pagination.html' %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Factors" %}</h1> <h1><span class="pficon-plugged"></span> {% trans "Factors" %}</h1>
<span>{% trans "Factors required for a user to successfully authenticate." %}</span> <span>{% trans "Factors required for a user to successfully authenticate." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
@ -20,7 +20,8 @@
</button> </button>
<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" href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:factor-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -39,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

@ -9,32 +9,35 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% 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>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>{% trans 'Expiry' %}</th> <th>{% trans 'Expiry' %}</th>
<th>{% trans 'Link' %}</th> <th>{% trans 'Link' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for invitation in object_list %} {% for invitation in object_list %}
<tr> <tr>
<td>{{ invitation.expires|default:"Never" }}</td> <td>{{ invitation.expires|default:"Never" }}</td>
<td><pre>{{ invitation.link }}</pre></td> <td>
<td> <pre>{{ invitation.link }}</pre>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> </td>
</td> <td>
</tr> <a class="btn btn-default btn-sm"
{% endfor %} href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,11 +7,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}</a> <a href="{% url 'passbook_admin:applications' %}">
<span class="pficon-applications"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ application_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:applications' %}">
<span class="pficon pficon-ok"></span>{{ application_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -19,11 +26,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}</a> <a href="{% url 'passbook_admin:sources' %}">
<span class="pficon-resource-pool"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ source_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:sources' %}">
<span class="pficon pficon-ok"></span>{{ source_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -31,11 +45,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}</a> <a href="{% url 'passbook_admin:providers' %}">
<span class="pficon-integration"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ provider_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:providers' %}">
{% 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>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -43,11 +68,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}</a> <a href="{% url 'passbook_admin:factors' %}">
<span class="pficon-plugged"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ factor_count }}</a></span> <span class="card-pf-aggregate-status-notification">
{% if factor_count < 1 %}
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No Factors configured. No Users will be able to login.' %}"></span>
{{ factor_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ factor_count }}
{% endif %}
</span>
</p> </p>
</div> </div>
</div> </div>
@ -55,11 +91,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}</a> <a href="{% url 'passbook_admin:policies' %}">
<span class="pficon-infrastructure"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Policies' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ invitation_count }}</a></span> <span class="card-pf-aggregate-status-notification">
{% if policies_without_attachment > 0 %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'Policies without attachment exist.' %}"></span>
{{ policy_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ policy_count }}
{% endif %}
</span>
</p> </p>
</div> </div>
</div> </div>
@ -67,11 +114,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Policies' %}</a> <a href="{% url 'passbook_admin:invitations' %}">
<span class="pficon-migration"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ policy_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:invitations' %}">
<span class="pficon pficon-ok"></span>{{ invitation_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -79,11 +133,61 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}</a> <a href="{% url 'passbook_admin:users' %}">
<span class="pficon-users"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ user_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:users' %}">
<span class="pficon pficon-ok"></span>{{ user_count }}
</a>
</span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#">
<span class="pficon-bundle"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %}
</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#">
{{ version }}
</a>
</span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#">
<span class="pficon-server"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Worker(s)' %}
</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#">
{% if worker_count < 1%}
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No workers connected. Policies will not work and you may expect other issues.' %}"></span> {{ worker_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ worker_count }}
{% endif %}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>

View File

@ -9,42 +9,54 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Policies" %}</h1> <h1><span class="pficon-infrastructure"></span> {% trans "Policies" %}</h1>
<span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span> <span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<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" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
{% endfor %} href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
</ul> {% endfor %}
</div> </ul>
<hr> </div>
<table class="table table-striped table-bordered"> <hr>
<thead> <table class="table table-striped table-bordered">
<tr> <thead>
<th>{% trans 'Name' %}</th> <tr>
<th>{% trans 'Class' %}</th> <th></th>
<th></th> <th>{% trans 'Name' %}</th>
</tr> <th>{% trans 'Type' %}</th>
</thead> <th></th>
<tbody> </tr>
{% for policy in object_list %} </thead>
<tr> <tbody>
<td>{{ policy.name }}</td> {% for policy in object_list %}
<td>{{ policy|fieldtype }}</td> <tr {% if not policy.policymodel_set.exists %} class="warning" {% endif %}>
<td> <th>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> {% if not policy.policymodel_set.exists %}
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-test' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a> <span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Policy is not assigned.' %}"></span>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-delete' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> {% else %}
</td> <span class="pficon-ok" data-toggle="tooltip" data-placement="right" title="{% blocktrans with objects=policy.policymodel_set.all|join:', ' %}Assigned to objects {{ objects }}{% endblocktrans %}"></span>
</tr> {% endif %}
{% endfor %} </th>
</tbody> <td>{{ policy.name }}</td>
</table> <td>{{ policy|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-test' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-delete' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -10,45 +10,57 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Providers" %}</h1> <h1><span class="pficon-integration"></span> {% trans "Providers" %}</h1>
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span> <span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<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" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
{% endfor %} href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for provider in object_list %}
<tr>
<td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %} {% endfor %}
</td> </ul>
</tr> </div>
{% endfor %} <hr>
</tbody> <table class="table table-striped table-bordered">
</table> <thead>
<tr>
<th></th>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for provider in object_list %}
<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|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Sources" %}</h1> <h1><span class="pficon-resource-pool"></span> {% trans "Sources" %}</h1>
<span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span> <span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
@ -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>
@ -27,6 +27,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Class' %}</th>
<th>{% trans 'Additional Info' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -35,6 +36,7 @@
<tr> <tr>
<td>{{ source.name }}</td> <td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td> <td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -5,34 +5,36 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Users" %}</h1> <h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
<hr> <hr>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<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> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for user in object_list %}
{% for user in object_list %} <tr>
<tr> <td>{{ user.username }}</td>
<td>{{ user.username }}</td> <td>{{ user.name|default:'-' }}</td>
<td>{{ user.first_name|default:'-' }}</td> <td>{{ user.is_active }}</td>
<td>{{ user.last_name|default:'-' }}</td> <td>{{ user.last_login }}</td>
<td>{{ user.is_active }}</td> <td>
<td>{{ user.last_login }}</td> <a class="btn btn-default btn-sm"
<td> href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <a class="btn btn-default btn-sm"
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td> <a class="btn btn-default btn-sm"
</tr> href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
{% endfor %} </td>
</tbody> </tr>
</table> {% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,7 +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 %}
{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}
{% 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

@ -11,7 +11,7 @@
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% include 'partials/form.html' with form=form %} {% include 'partials/form.html' with form=form %}
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a> <a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-primary" value="{% trans 'Create' %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,7 +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 %}
{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}
{% endblock %}

View File

@ -56,6 +56,8 @@ urlpatterns = [
users.UserUpdateView.as_view(), name='user-update'), users.UserUpdateView.as_view(), name='user-update'),
path('users/<int:pk>/delete/', path('users/<int:pk>/delete/',
users.UserDeleteView.as_view(), name='user-delete'), users.UserDeleteView.as_view(), name='user-delete'),
path('users/<int:pk>/reset/',
users.UserPasswordResetView.as_view(), name='user-password-reset'),
# Audit Log # Audit Log
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
# Groups # Groups

View File

@ -1,4 +1,5 @@
"""passbook Application administration""" """passbook Application administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -13,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):
@ -28,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"""
@ -45,5 +51,10 @@ class ApplicationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView)
model = Application model = Application
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully updated Application') success_message = _('Successfully deleted Application')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,4 +1,5 @@
"""passbook Factor administration""" """passbook Factor administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -33,25 +34,24 @@ 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')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
source_type = self.request.GET.get('type') factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
kwargs['type'] = model._meta.verbose_name kwargs['type'] = model._meta.verbose_name
return kwargs return kwargs
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get('type') factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update factor""" """Update factor"""
@ -61,11 +61,12 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
success_message = _('Successfully updated Factor') success_message = _('Successfully updated Factor')
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get('type') form_class_path = self.get_object().form
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) form_class = path_to_class(form_class_path)
if not model: return form_class
raise Http404
return path_to_class(model.form) def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete factor""" """Delete factor"""
@ -73,7 +74,11 @@ class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Factor model = Factor
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully updated Factor') success_message = _('Successfully deleted Factor')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,4 +1,5 @@
"""passbook Invitation administration""" """passbook Invitation administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -26,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
@ -42,4 +47,8 @@ class InvitationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Invitation model = Invitation
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:invitations') success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully updated Invitation') success_message = _('Successfully deleted Invitation')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -2,6 +2,8 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.celery import CELERY_APP
from passbook.core.models import (Application, Factor, Invitation, Policy, from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User) Provider, Source, User)
@ -19,4 +21,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs['source_count'] = len(Source.objects.all()) kwargs['source_count'] = len(Source.objects.all())
kwargs['factor_count'] = len(Factor.objects.all()) kwargs['factor_count'] = len(Factor.objects.all())
kwargs['invitation_count'] = len(Invitation.objects.all()) kwargs['invitation_count'] = len(Invitation.objects.all())
kwargs['version'] = __version__
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
return super().get_context_data(**kwargs) 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')
@ -68,11 +68,15 @@ class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Policy model = Policy
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully updated Policy') success_message = _('Successfully deleted Policy')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class PolicyTestView(AdminRequiredMixin, DetailView, FormView): class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
"""View to test policy(s)""" """View to test policy(s)"""

View File

@ -1,4 +1,5 @@
"""passbook Provider administration""" """passbook Provider administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -28,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')
@ -64,7 +65,11 @@ class ProviderDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Provider model = Provider
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully updated Provider') success_message = _('Successfully deleted Provider')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,4 +1,5 @@
"""passbook Source administration""" """passbook Source administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -33,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')
@ -66,9 +67,13 @@ class SourceDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete source""" """Delete source"""
model = Source model = Source
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully updated Source') success_message = _('Successfully deleted Source')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,12 +1,15 @@
"""passbook User administration""" """passbook User administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import DeleteView, ListView, UpdateView from django.views.generic import DeleteView, ListView, UpdateView
from passbook.admin.forms.users import UserForm
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.users import UserDetailForm from passbook.core.models import Nonce, User
from passbook.core.models import User
class UserListView(AdminRequiredMixin, ListView): class UserListView(AdminRequiredMixin, ListView):
@ -20,7 +23,7 @@ class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update user""" """Update user"""
model = User model = User
form_class = UserDetailForm form_class = UserForm
template_name = 'generic/update.html' template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy('passbook_admin:users')
@ -31,6 +34,24 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete user""" """Delete user"""
model = User model = User
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy('passbook_admin:users')
success_message = _('Successfully updated User') success_message = _('Successfully deleted User')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class UserPasswordResetView(AdminRequiredMixin, View):
"""Get Password reset link for user"""
# pylint: disable=invalid-name
def get(self, request, pk):
"""Create nonce for user and return link"""
user = get_object_or_404(User, pk=pk)
nonce = Nonce.objects.create(user=user)
link = request.build_absolute_uri(reverse(
'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid}))
messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link}))
return redirect('passbook_admin:users')

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.7-alpha' __version__ = '0.1.2-beta'

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.7-alpha' __version__ = '0.1.2-beta'

View File

@ -1,5 +1,4 @@
"""passbook audit models""" """passbook audit models"""
from datetime import timedelta
from logging import getLogger from logging import getLogger
from django.conf import settings from django.conf import settings
@ -7,11 +6,10 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ipware import get_client_ip from ipware import get_client_ip
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import UUIDModel
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -51,7 +49,10 @@ class AuditEntry(UUIDModel):
def create(action, request, **kwargs): def create(action, request, **kwargs):
"""Create AuditEntry from arguments""" """Create AuditEntry from arguments"""
client_ip, _ = get_client_ip(request) client_ip, _ = get_client_ip(request)
user = request.user if not hasattr(request, 'user'):
user = None
else:
user = request.user
if isinstance(user, AnonymousUser): if isinstance(user, AnonymousUser):
user = kwargs.get('user', None) user = kwargs.get('user', None)
entry = AuditEntry.objects.create( entry = AuditEntry.objects.create(
@ -60,7 +61,7 @@ class AuditEntry(UUIDModel):
# User 255.255.255.255 as fallback if IP cannot be determined # User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255', request_ip=client_ip or '255.255.255.255',
context=kwargs) context=kwargs)
LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip) LOGGER.debug("Logged %s from %s (%s)", action, user, client_ip)
return entry return entry
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -72,41 +73,3 @@ class AuditEntry(UUIDModel):
verbose_name = _('Audit Entry') verbose_name = _('Audit Entry')
verbose_name_plural = _('Audit Entries') verbose_name_plural = _('Audit Entries')
class LoginAttempt(CreatedUpdatedModel):
"""Track failed login-attempts"""
target_uid = models.CharField(max_length=254)
request_ip = models.GenericIPAddressField()
attempts = models.IntegerField(default=1)
@staticmethod
def attempt(target_uid, request):
"""Helper function to create attempt or count up existing one"""
client_ip, _ = get_client_ip(request)
# Since we can only use 254 chars for target_uid, truncate target_uid.
target_uid = target_uid[:254]
time_threshold = timezone.now() - timedelta(minutes=10)
existing_attempts = LoginAttempt.objects.filter(
target_uid=target_uid,
request_ip=client_ip,
last_updated__gt=time_threshold).order_by('created')
if existing_attempts.exists():
attempt = existing_attempts.first()
attempt.attempts += 1
attempt.save()
LOGGER.debug("Increased attempts on %s", attempt)
else:
attempt = LoginAttempt.objects.create(
target_uid=target_uid,
request_ip=client_ip)
LOGGER.debug("Created new attempt %s", attempt)
def __str__(self):
return "LoginAttempt to %s from %s (x%d)" % (self.target_uid,
self.request_ip, self.attempts)
class Meta:
unique_together = (('target_uid', 'request_ip', 'created'),)

View File

@ -1 +0,0 @@
django-ipware

View File

@ -1,9 +1,8 @@
"""passbook audit signal listener""" """passbook audit signal listener"""
from django.contrib.auth.signals import (user_logged_in, user_logged_out, from django.contrib.auth.signals import user_logged_in, user_logged_out
user_login_failed)
from django.dispatch import receiver from django.dispatch import receiver
from passbook.audit.models import AuditEntry, LoginAttempt from passbook.audit.models import AuditEntry
from passbook.core.signals import (invitation_created, invitation_used, from passbook.core.signals import (invitation_created, invitation_used,
user_signed_up) user_signed_up)
@ -34,8 +33,3 @@ def on_invitation_used(sender, request, invitation, **kwargs):
"""Log Invitation usage""" """Log Invitation usage"""
AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request, AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request,
invitation_uuid=invitation.uuid.hex) invitation_uuid=invitation.uuid.hex)
@receiver(user_login_failed)
def on_user_login_failed(sender, request, credentials, **kwargs):
"""Log failed login attempt"""
LoginAttempt.attempt(target_uid=credentials.get('username'), request=request)

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.0.7-alpha' __version__ = '0.1.2-beta'

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.0.7-alpha' __version__ = '0.1.2-beta'

View File

@ -1,15 +1,19 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from logging import getLogger from logging import getLogger
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, 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
from passbook.core.auth.factor import AuthenticationFactor 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.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -21,6 +25,29 @@ class PasswordFactor(FormView, AuthenticationFactor):
form_class = PasswordFactorForm form_class = PasswordFactorForm
template_name = 'login/factors/backend.html' template_name = 'login/factors/backend.html'
def get_context_data(self, **kwargs):
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled')
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
if 'password-forgotten' in request.GET:
nonce = Nonce.objects.create(user=self.pending_user)
LOGGER.debug("DEBUG %s", str(nonce.uuid))
# 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()
messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login')
return super().get(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
"""Authenticate against django's authentication backend""" """Authenticate against django's authentication backend"""
uid_fields = CONFIG.y('passbook.uid_fields') uid_fields = CONFIG.y('passbook.uid_fields')

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).with_request(request).build()
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()
@ -111,8 +121,8 @@ class AuthenticationView(UserPassesTestMixin, View):
"""Show error message, user cannot login. """Show error message, user cannot login.
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"""
@ -121,13 +131,13 @@ class AuthenticationView(UserPassesTestMixin, View):
login(self.request, self.pending_user, backend=backend) login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in user %s", self.pending_user) LOGGER.debug("Logged in user %s", self.pending_user)
# 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"""
session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS, session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS,
self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ] self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ]

View File

@ -5,9 +5,8 @@ import os
import celery import celery
from django.conf import settings from django.conf import settings
from raven import Client
# from raven import Client from raven.contrib.celery import register_logger_signal, register_signal
# from raven.contrib.celery import register_logger_signal, register_signal
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")
@ -18,16 +17,17 @@ LOGGER = logging.getLogger(__name__)
class Celery(celery.Celery): class Celery(celery.Celery):
"""Custom Celery class with Raven configured""" """Custom Celery class with Raven configured"""
# def on_configure(self): # pylint: disable=method-hidden
# """Update raven client""" def on_configure(self):
# try: """Update raven client"""
# client = Client(settings.RAVEN_CONFIG.get('dsn')) try:
# # register a custom filter to filter out duplicate logs client = Client(settings.RAVEN_CONFIG.get('dsn'))
# register_logger_signal(client) # register a custom filter to filter out duplicate logs
# # hook into the Celery error handler register_logger_signal(client)
# register_signal(client) # hook into the Celery error handler
# except RecursionError: # This error happens when pdoc is running register_signal(client)
# pass except RecursionError: # This error happens when pdoc is running
pass
# pylint: disable=unused-argument # pylint: disable=unused-argument

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

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from passbook.core.models import User from passbook.core.models import User
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.utils.ui import human_list
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -15,13 +16,16 @@ class LoginForm(forms.Form):
"""Allow users to login""" """Allow users to login"""
title = _('Log in to your account') title = _('Log in to your account')
uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')})) uid_field = forms.CharField()
remember_me = forms.BooleanField(required=False) remember_me = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if CONFIG.y('passbook.uid_fields') == ['email']: if CONFIG.y('passbook.uid_fields') == ['e-mail']:
self.fields['uid_field'] = forms.EmailField() self.fields['uid_field'] = forms.EmailField()
self.fields['uid_field'].widget.attrs = {
'placeholder': _(human_list([x.title() for x in CONFIG.y('passbook.uid_fields')]))
}
def clean_uid_field(self): def clean_uid_field(self):
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields""" """Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
@ -34,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'),
@ -79,12 +81,14 @@ class SignUpForm(forms.Form):
password_repeat = self.cleaned_data.get('password_repeat') password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat: if password != password_repeat:
raise ValidationError(_("Passwords don't match")) raise ValidationError(_("Passwords don't match"))
# TODO: Password policy? Via Plugin? via Policy?
# return check_password(self)
return self.cleaned_data.get('password_repeat') return self.cleaned_data.get('password_repeat')
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',
'autocomplete': 'current-password'
}))

View File

@ -11,7 +11,7 @@ class PasswordFactorForm(forms.ModelForm):
class Meta: class Meta:
model = PasswordFactor model = PasswordFactor
fields = GENERAL_FIELDS + ['backends'] fields = GENERAL_FIELDS + ['backends', 'password_policies']
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),

View File

@ -27,7 +27,7 @@ class InvitationForm(forms.ModelForm):
class Meta: class Meta:
model = Invitation model = Invitation
fields = ['expires', 'fixed_username', 'fixed_email'] fields = ['expires', 'fixed_username', 'fixed_email', 'needs_confirmation']
labels = { labels = {
'fixed_username': "Force user's username (optional)", 'fixed_username': "Force user's username (optional)",
'fixed_email': "Force user's email (optional)", 'fixed_email': "Force user's email (optional)",

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,16 +13,23 @@ 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"""
password = forms.CharField(label=_('Password'), password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput(attrs={'placeholder': _('New Password')})) widget=forms.PasswordInput(attrs={
'placeholder': _('New Password'),
'autocomplete': 'new-password'
}))
password_repeat = forms.CharField(label=_('Repeat Password'), password_repeat = forms.CharField(label=_('Repeat Password'),
widget=forms.PasswordInput(attrs={ widget=forms.PasswordInput(attrs={
'placeholder': _('Repeat Password') 'placeholder': _('Repeat Password'),
'autocomplete': 'new-password'
})) }))
def clean_password_repeat(self): def clean_password_repeat(self):
@ -31,5 +38,4 @@ class PasswordChangeForm(forms.Form):
password_repeat = self.cleaned_data.get('password_repeat') password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat: if password != password_repeat:
raise ValidationError(_("Passwords don't match")) raise ValidationError(_("Passwords don't match"))
# TODO: Password policy check
return self.cleaned_data.get('password_repeat') return self.cleaned_data.get('password_repeat')

View File

@ -0,0 +1,44 @@
"""passbook import_users management command"""
from csv import DictReader
from logging import getLogger
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from passbook.core.models import User
LOGGER = getLogger(__name__)
class Command(BaseCommand):
"""Import users from CSV file"""
def add_arguments(self, parser):
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Create Users from CSV file"""
for file in options.get('file'):
with open(file, 'r') as _file:
reader = DictReader(_file)
for user in reader:
LOGGER.debug('User %s', user.get('username'))
try:
# only import users with valid email addresses
if user.get('email'):
validator = EmailValidator()
validator(user.get('email'))
# use combination of username and email to check for existing user
if User.objects.filter(
username=user.get('username'),
email=user.get('email')).exists():
LOGGER.debug('User %s exists already, skipping', user.get('username'))
# Create user
User.objects.create(
username=user.get('username'),
email=user.get('email'),
name=user.get('name'))
LOGGER.debug('Created User %s', user.get('username'))
except ValidationError as exc:
LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
continue

View File

@ -1,5 +1,5 @@
"""passbook nexus_upload management command""" """passbook nexus_upload management command"""
from getpass import getpass from base64 import b64decode
import requests import requests
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@ -24,9 +24,9 @@ class Command(BaseCommand):
help='Nexus root URL', help='Nexus root URL',
required=True) required=True)
parser.add_argument( parser.add_argument(
'--user', '--auth',
action='store', action='store',
help='Username to use for Nexus upload', help='base64-encoded string of username:password',
required=True) required=True)
parser.add_argument( parser.add_argument(
'--method', '--method',
@ -37,29 +37,21 @@ class Command(BaseCommand):
help=('Method used for uploading files to nexus. ' help=('Method used for uploading files to nexus. '
'Apt repositories use post, Helm uses put.'), 'Apt repositories use post, Helm uses put.'),
required=True) required=True)
parser.add_argument(
'--password',
action='store',
help=("Password to use for Nexus upload. "
"If parameter not given, we'll interactively ask"))
# Positional arguments # Positional arguments
parser.add_argument('file', nargs='+', type=str) parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options): def handle(self, *args, **options):
"""Upload debian package to nexus repository""" """Upload debian package to nexus repository"""
if options.get('password') is None: auth = tuple(b64decode(options.get('auth')).decode('utf-8').split(':', 1))
options['password'] = getpass()
responses = {} responses = {}
url = 'https://%(url)s/repository/%(repo)s//' % options url = 'https://%(url)s/repository/%(repo)s/' % options
method = options.get('method') method = options.get('method')
exit_code = 0 exit_code = 0
for file in options.get('file'): for file in options.get('file'):
if method == 'post': if method == 'post':
responses[file] = requests.post(url, data=open(file, mode='rb'), responses[file] = requests.post(url, data=open(file, mode='rb'), auth=auth)
auth=(options.get('user'), options.get('password')))
else: else:
responses[file] = requests.put(url+file, data=open(file, mode='rb'), responses[file] = requests.put(url+file, data=open(file, mode='rb'), auth=auth)
auth=(options.get('user'), options.get('password')))
self.stdout.write('Upload results:\n') self.stdout.write('Upload results:\n')
sep = '-' * 60 sep = '-' * 60
self.stdout.write('%s\n' % sep) self.stdout.write('%s\n' % sep)

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.7 on 2019-02-25 14:38
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
]
operations = [
migrations.AddField(
model_name='passwordfactor',
name='password_policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',
name='password_change_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 2.1.7 on 2019-02-25 19:12
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0011_auto_20190225_1438'),
]
operations = [
migrations.CreateModel(
name='Nonce',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-02-25 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0012_nonce'),
]
operations = [
migrations.AddField(
model_name='invitation',
name='needs_confirmation',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.7 on 2019-02-26 08:50
from django.db import migrations
def create_initial_factor(apps, schema_editor):
"""Create initial PasswordFactor if none exists"""
PasswordFactor = apps.get_model("passbook_core", "PasswordFactor")
if not PasswordFactor.objects.exists():
PasswordFactor.objects.create(
name='password',
slug='password',
order=0,
backends=[]
)
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0013_invitation_needs_confirmation'),
]
operations = [
migrations.RunPython(create_initial_factor)
]

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

@ -1,21 +1,30 @@
"""passbook core models""" """passbook core models"""
import re import re
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
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
def default_nonce_duration():
"""Default duration a Nonce is valid"""
return now() + timedelta(hours=4)
class Group(UUIDModel): class Group(UUIDModel):
"""Custom Group model which supports a basic hierarchy""" """Custom Group model which supports a basic hierarchy"""
@ -35,9 +44,18 @@ 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)
def set_password(self, password):
if self.pk:
password_changed.send(sender=self, user=self, password=password)
self.password_change_date = now()
return super().set_password(password)
class Provider(models.Model): class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -55,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"""
@ -87,6 +98,7 @@ class PasswordFactor(Factor):
"""Password-based Django-backend Authentication Factor""" """Password-based Django-backend Authentication Factor"""
backends = ArrayField(models.TextField()) backends = ArrayField(models.TextField())
password_policies = models.ManyToManyField('Policy', blank=True)
type = 'passbook.core.auth.factors.password.PasswordFactor' type = 'passbook.core.auth.factors.password.PasswordFactor'
form = 'passbook.core.forms.factors.PasswordFactorForm' form = 'passbook.core.forms.factors.PasswordFactorForm'
@ -94,6 +106,13 @@ class PasswordFactor(Factor):
def has_user_settings(self): def has_user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password' return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
def __str__(self): def __str__(self):
return "Password Factor %s" % self.slug return "Password Factor %s" % self.slug
@ -134,7 +153,13 @@ class Application(PolicyModel):
def user_is_authorized(self, user: User) -> bool: def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application""" """Check if user is authorized to use this application"""
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).build().result
def get_provider(self):
"""Get casted provider instance"""
if not self.provider:
return None
return Provider.objects.get_subclass(pk=self.provider.pk)
def __str__(self): def __str__(self):
return self.name return self.name
@ -155,10 +180,15 @@ class Source(PolicyModel):
return False return False
@property @property
def get_url(self): def get_login_button(self):
"""Return URL used for logging in""" """Return a tuple of URL, Icon name and Name"""
raise NotImplementedError raise NotImplementedError
@property
def additional_info(self):
"""Return additional Info, such as a callback URL. Show in the administration interface."""
return None
def __str__(self): def __str__(self):
return self.name return self.name
@ -195,7 +225,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()
@ -219,8 +249,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'),),
@ -240,7 +269,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")
@ -257,8 +286,7 @@ class FieldMatcherPolicy(Policy):
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP: if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value) pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value)) passes = bool(pattern.match(user_field_value))
if self.negate:
passes = not passes
LOGGER.debug("User got '%r'", passes) LOGGER.debug("User got '%r'", passes)
return passes return passes
@ -275,10 +303,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
@ -293,6 +322,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:
@ -351,7 +382,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:
@ -365,6 +396,7 @@ class Invitation(UUIDModel):
expires = models.DateTimeField(default=None, blank=True, null=True) expires = models.DateTimeField(default=None, blank=True, null=True)
fixed_username = models.TextField(blank=True, default=None) fixed_username = models.TextField(blank=True, default=None)
fixed_email = models.TextField(blank=True, default=None) fixed_email = models.TextField(blank=True, default=None)
needs_confirmation = models.BooleanField(default=True)
@property @property
def link(self): def link(self):
@ -378,3 +410,17 @@ class Invitation(UUIDModel):
verbose_name = _('Invitation') verbose_name = _('Invitation')
verbose_name_plural = _('Invitations') verbose_name_plural = _('Invitations')
class Nonce(UUIDModel):
"""One-time link for password resets/signup-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE)
def __str__(self):
return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires)
class Meta:
verbose_name = _('Nonce')
verbose_name_plural = _('Nonces')

View File

@ -2,6 +2,7 @@
from logging import getLogger from logging import getLogger
from celery import group from celery import group
from ipware import get_client_ip
from passbook.core.celery import CELERY_APP from passbook.core.celery import CELERY_APP
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
@ -17,32 +18,64 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
setattr(user_obj, key, value) setattr(user_obj, key, value)
LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name, LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
policy_obj.pk.hex, user_obj) policy_obj.pk.hex, user_obj)
return policy_obj.passes(user_obj) policy_result = policy_obj.passes(user_obj)
# Handle policy result correctly if result, message or just result
message = None
if isinstance(policy_result, (tuple, list)):
policy_result, message = policy_result
# Invert result if policy.negate is set
if policy_obj.negate:
policy_result = not policy_result
LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
return policy_obj.action, policy_result, message
class PolicyEngine: class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result""" """Orchestrate policy checking, launch tasks and return result"""
policies = None policies = None
_group = None _group = None
_request = None
_user = None
def __init__(self, policies): def __init__(self, policies):
self.policies = policies self.policies = policies
self._request = None
self._user = None
def for_user(self, user): def for_user(self, user):
"""Check policies for user""" """Check policies for user"""
self._user = user
return self
def with_request(self, request):
"""Set request"""
self._request = request
return self
def build(self):
"""Build task group"""
signatures = [] signatures = []
kwargs = { kwargs = {
'__password__': getattr(user, '__password__', None) '__password__': getattr(self._user, '__password__', None),
} }
if self._request:
kwargs['remote_ip'], _ = get_client_ip(self._request)
if not kwargs['remote_ip']:
kwargs['remote_ip'] = '255.255.255.255'
for policy in self.policies: for policy in self.policies:
signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs)) signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs))
self._group = group(signatures)() self._group = group(signatures)()
return self return self
@property @property
def result(self): def result(self):
"""Get policy-checking result""" """Get policy-checking result"""
for policy_result in self._group.get(): messages = []
if policy_result is False: for policy_action, policy_result, policy_message in self._group.get():
return False passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
return True (policy_action == Policy.ACTION_DENY and not policy_result)
if policy_message:
messages.append(policy_message)
if not passing:
return False, messages
return True, messages

View File

@ -1,5 +1,6 @@
django>=2.0 django>=2.0
django-model-utils django-model-utils
django-ipware
djangorestframework djangorestframework
PyYAML PyYAML
raven raven

View File

@ -62,6 +62,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'drf_yasg', 'drf_yasg',
'raven.contrib.django.raven_compat',
'passbook.core.apps.PassbookCoreConfig', 'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig', 'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
@ -73,6 +74,10 @@ INSTALLED_APPS = [
'passbook.saml_idp.apps.PassbookSAMLIDPConfig', 'passbook.saml_idp.apps.PassbookSAMLIDPConfig',
'passbook.otp.apps.PassbookOTPConfig', 'passbook.otp.apps.PassbookOTPConfig',
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', 'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
'passbook.hibp_policy.apps.PassbookHIBPConfig',
'passbook.pretend.apps.PassbookPretendConfig',
'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig',
'passbook.suspicious_policy.apps.PassbookSuspiciousPolicyConfig',
] ]
# Message Tag fix for bootstrap CSS Classes # Message Tag fix for bootstrap CSS Classes
@ -101,6 +106,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
] ]
ROOT_URLCONF = 'passbook.core.urls' ROOT_URLCONF = 'passbook.core.urls'
@ -181,6 +187,14 @@ CELERY_TASK_DEFAULT_QUEUE = 'passbook'
CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis') CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis')
CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis') CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis')
# Raven settings
RAVEN_CONFIG = {
'dsn': ('https://55b5dd780bc14f4c96bba69b7a9abbcc:449af483bd0745'
'0d83be640d834e5458@sentry.services.beryju.org/8'),
'release': VERSION,
'environment': 'dev' if DEBUG else 'production',
}
# CherryPY settings # CherryPY settings
with CONFIG.cd('web'): with CONFIG.cd('web'):
CHERRYPY_SERVER = { CHERRYPY_SERVER = {
@ -289,6 +303,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,11 +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'])
@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).build()
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

@ -5,16 +5,20 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{% block above_form %} {% block above_form %}
<h1>{% trans 'Delete' %}</h1> <h1>{% blocktrans with object_type=object|fieldtype|title %}Delete {{ object_type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
<div class=""> <div class="">
<form method="post" class="form-horizontal"> <form method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p> <p>
<a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a> {% blocktrans with object_type=object|fieldtype|title name=object %}
<input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" /> Are you sure you want to delete {{ object_type }} "{{ object }}"?
</form> {% endblocktrans %}
</div> </p>
<a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a>
<input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" />
</form>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -23,16 +23,16 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="toast-notifications-list-pf">
{% include 'partials/messages.html' %}
</div>
<div class="login-pf-page"> <div class="login-pf-page">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-sm-12 col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3">
{% include 'partials/messages.html' %}
</div>
<div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
<header class="login-pf-page-header"> <header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" <img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
alt="PatternFly logo" /> alt="passbook logo" />
{% if config.login.subtext %} {% if config.login.subtext %}
<p>{{ config.login.subtext }}</p> <p>{{ config.login.subtext }}</p>
{% endif %} {% endif %}

View File

@ -18,7 +18,6 @@
<header class="login-pf-header"> <header class="login-pf-header">
<h1>{% trans title %}</h1> <h1>{% trans title %}</h1>
</header> </header>
{% include 'partials/messages.html' %}
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{% include 'partials/form_login.html' %} {% include 'partials/form_login.html' %}

View File

@ -2,3 +2,8 @@
{% load i18n %} {% load i18n %}
{% block beneath_form %}
{% if show_password_forget_notice %}
<a href="{% url 'passbook_core:auth-process' %}?password-forgotten">{% trans 'Forgot password?' %}</a>
{% endif %}
{% endblock %}

View File

@ -8,10 +8,11 @@
<h1>{% trans title %}</h1> <h1>{% trans title %}</h1>
</header> </header>
<form method="POST"> <form method="POST">
{% csrf_token %}
{% block above_form %} {% block above_form %}
{% endblock %} {% endblock %}
{% include 'partials/form_login.html' %} {% include 'partials/form_login.html' %}
{% block beneath_form %}
{% endblock %}
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button> <button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button>
</form> </form>
{% if show_sign_up_notice %} {% if show_sign_up_notice %}

View File

@ -1,131 +0,0 @@
{% load static %}
<!DOCTYPE html>
<!--[if IE 9]><html lang="en-us" class="ie9 login-pf"><![endif]-->
<!--[if gt IE 9]><!-->
<html lang="en-us" class="login-pf">
<!--<![endif]-->
<head>
<title>Login Social Account (two column) - Red Hat&reg; Common User Experience</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static 'img/favicon.ico' %}">
<!-- iPad retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{% static 'img/apple-touch-icon-precomposed-152.png' %}">
<!-- iPad retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{% static 'img/apple-touch-icon-precomposed-144.png' %}">
<!-- iPad non-retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{% static 'img/apple-touch-icon-precomposed-76.png' %}">
<!-- iPad non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="{% static 'img/apple-touch-icon-precomposed-72.png' %}">
<!-- iPhone 6 Plus icon -->
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{% static 'img/apple-touch-icon-precomposed-180.png' %}">
<!-- iPhone retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="{% static 'img/apple-touch-icon-precomposed-114.png' %}">
<!-- iPhone non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="{% static 'img/apple-touch-icon-precomposed-57.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.2/js/bootstrap-select.min.js"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script>
</head>
<div class="toast-notifications-list-pf">
<div class="toast-pf alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<span class="pficon pficon-warning-triangle-o"></span>
These examples are included for development testing purposes. For official documentation, see <a href="https://www.patternfly.org" class="alert-link">https://www.patternfly.org</a> and <a href="http://getbootstrap.com" class="alert-link">http://getbootstrap.com</a>.
</div>
</div>
<body>
<div class="login-pf-page login-pf-page-accounts">
<header class="login-pf-page-header">
<img class="login-pf-brand" src="/" alt="Red Hat&reg; logo" />
</header>
<div class="card-pf login-pf-accounts">
<header class="login-pf-header">
<select class="selectpicker">
<option>English</option>
<option>French</option>
<option>Italian</option>
</select>
<h1>Log In to Your Account</h1>
</header>
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in to your patternfly account">
<form>
<div class="form-group">
<label class="sr-only" for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control input-lg" id="exampleInputEmail1" placeholder="Email address">
</div>
<div class="form-group">
<label class="sr-only" for="exampleInputPassword1">Password
</label>
<input type="password" class="form-control input-lg" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="login-pf-settings">
<label class="checkbox-label">
<input type="checkbox"> Keep me logged in for 30 days
</label>
<a href="#">Forgot password?</a>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">Log In</button>
</form>
</section><!--login-pf-section-->
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
<ul class="login-pf-social login-pf-social-double-col list-unstyled">
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/google-logo.svg' %}" alt="Google account login">Google</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/github-logo.svg' %}" alt="github account login">Github</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/facebook-logo.svg' %}" alt="Facebook account login">Facebook</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/twitter-logo.svg' %}" alt="Twitter account login">Twitter</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/linkedin-logo.svg' %}" alt="LinkIn account login">LinkIn</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/stack-exchange-logo.svg' %}" alt="Stack Exchange logo">Stack Exchange</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/open-id-logo.svg' %}" alt="Open ID account login">Open ID</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/instagram-logo.png' %}" alt="Instagram account login">Instagram</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/git-logo.svg' %}" alt="Git account login">Git</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/dropbox-logo.svg' %}" alt="dropbox account login">Dropbox</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/fedora-logo.png' %}" alt="fedora account login">Fedora</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/skype-logo.svg' %}" alt="skype account logingit ">Skype</a></li>
</ul>
<button type="button" id="socialAccountsToggle" class="btn btn-link login-pf-social-toggle">More<span class="caret"></span></button>
</section><!--login-pf-section-->
<p class="login-pf-signup">Need an account?<a href="#">Sign up</a></p>
</div><!-- card -->
<div class="row">
<div class="col-md-6 col-md-offset-3">
<footer class="login-pf-page-footer">
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div>
</div>
</div><!-- login-pf-page -->
<script>
$("#socialAccountsToggle").on("click", function(e) {
var $toggle = $(e.target);
var text = $toggle.contents().first()[0];
var socialContainer = $('.login-pf-social-section > .login-pf-social');
if ($toggle.hasClass('login-pf-social-toggle-active')) {
$toggle.removeClass('login-pf-social-toggle-active');
text.textContent = 'More';
socialContainer.removeClass('login-pf-social-all');
} else {
$toggle.addClass('login-pf-social-toggle-active');
text.textContent = 'Less';
socialContainer.addClass('login-pf-social-all');
}
});
</script>
</body>
</html>

View File

@ -1,51 +1,79 @@
{% extends 'login/base.html' %} {% extends 'base/skeleton.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block row %} {% block head %}
{% include 'partials/messages.html' %} <style>
<div class="col-md-6"> .login-pf-page .login-pf-page-footer-links {
<div class="card-pf"> padding: 15px;
background-color: #fff;
border-top: 2px solid transparent;
box-shadow: 0 1px 1px rgba(3, 3, 3, .175);
}
.login-pf-page .login-pf-page-footer-link {
color: #72767b;
}
.login-pf-page .login-pf-page-footer-links li:not(:last-of-type):after {
color: #72767b;
}
</style>
{% endblock %}
{% block body %}
<div class="toast-notifications-list-pf">
{% include 'partials/messages.html' %}
</div>
<div class="login-pf-page login-pf-page-accounts">
<header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" alt="passbook logo" />
{% if config.login.subtext %}
<p>{{ config.login.subtext }}</p>
{% endif %}
</header>
<div class="card-pf login-pf-accounts">
<header class="login-pf-header"> <header class="login-pf-header">
<h1>{% trans title %}</h1> <h1>{% trans title %}</h1>
</header> </header>
<form method="POST"> <section class="login-pf-social-section" role="contentinfo" aria-label="Log in to your patternfly account">
{% csrf_token %} <form method="POST">
{% block above_form %} {% block above_form %}
{% endblock %} {% endblock %}
{% include 'partials/form_login.html' %} {% include 'partials/form_login.html' %}
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button> <button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button>
</form> </form>
</section>
<!--login-pf-section-->
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
<ul class="login-pf-social login-pf-social-double-col list-unstyled">
{% for url, icon, name in sources %}
<li class="login-pf-social-link">
<a href="{{ url }}">
<img src="{% static 'img/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% if show_sign_up_notice %} {% if show_sign_up_notice %}
<p class="login-pf-signup"> <p class="login-pf-signup">
{% trans 'Need an account?' %} {% trans 'Need an account?' %}
<a href="{% url 'passbook_core:auth-sign-up' %}">{% trans 'Sign up' %}</a> <a href="{% url 'passbook_core:auth-sign-up' %}">{% trans 'Sign up' %}</a>
</p> </p>
{% endif %} {% endif %}
</div><!-- card -->
<div class="row">
<div class="col-md-6 col-md-offset-3">
<footer class="login-pf-page-footer">
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div>
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="card-pf">
<header class="login-pf-header">
<h1>{% trans title %}</h1>
<ul>
{% for source in sources %}
<li>
<a class="btn btn-block btn-primary" href="{{ source.get_url }}">{{ source }}</a>
</li>
{% endfor %}
</ul>
</header>
</div>
</div>
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2">
<footer class="login-pf-page-footer">
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div>
{% endblock %} {% endblock %}

View File

@ -5,83 +5,83 @@
{% load is_active %} {% load is_active %}
{% block body %} {% block body %}
<div class="toast-notifications-list-pf">
{% include 'partials/messages.html' %}
</div>
<nav class="navbar navbar-default navbar-pf" role="navigation"> <nav class="navbar navbar-default navbar-pf" role="navigation">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1">
<span class="sr-only">{% trans 'Toggle navigation' %}</span> <span class="sr-only">{% trans 'Toggle navigation' %}</span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="{% static 'img/brand.svg' %}" alt="passbook" />
</a>
</div>
<div class="collapse navbar-collapse navbar-collapse-1">
<ul class="nav navbar-nav navbar-utility">
<li class="dropdown">
<button class="btn btn-link nav-item-iconic" id="horizontalDropdownMenu1" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<span title="Help" class="fa pficon-help dropdown-title"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="horizontalDropdownMenu1"> <a class="navbar-brand" href="/">
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %} <img src="{% static 'img/brand.svg' %}" alt="passbook" />
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li> </a>
</div>
<div class="collapse navbar-collapse navbar-collapse-1">
<ul class="nav navbar-nav navbar-utility">
<li class="dropdown">
<button class="btn btn-link nav-item-iconic" id="horizontalDropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<span title="Help" class="fa pficon-help dropdown-title"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="horizontalDropdownMenu1">
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %}
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li>
</ul>
</li>
<li class="dropdown">
<button class="btn btn-link dropdown-toggle" data-toggle="dropdown">
<span class="pficon pficon-user"></span>
<span class="dropdown-title">
{{ user.username }} <b class="caret"></b>
</span>
</button>
<ul class="dropdown-menu">
<li>
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a>
</li>
<li>
<a href="{% url 'passbook_core:user-change-password' %}">{% trans 'Change Password' %}</a>
</li>
<li class="divider"></li>
<li>
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</li>
</ul>
</li>
</ul> </ul>
</li> {% is_active_app 'passbook_admin' as is_admin %}
<li class="dropdown"> <ul class="nav navbar-nav navbar-primary {% if is_admin == 'active' %}persistent-secondary{% endif %}">
<button class="btn btn-link dropdown-toggle" data-toggle="dropdown"> <li class="{% is_active_url 'passbook_core:overview' %}">
<span class="pficon pficon-user"></span> <a href="{% url 'passbook_core:overview' %}">{% trans 'Overview' %}</a>
<span class="dropdown-title"> </li>
{{ user.username }} <b class="caret"></b> {% if user.is_superuser %}
</span> <li class="{% is_active_app 'passbook_admin' %}">
</button> <a href="{% url 'passbook_admin:overview' %}">{% trans 'Administration' %}</a>
<ul class="dropdown-menu"> {% block nav_secondary %}
<li> {% endblock %}
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a> </li>
</li> {% endif %}
<li>
<a href="{% url 'passbook_core:user-change-password' %}">{% trans 'Change Password' %}</a>
</li>
<li class="divider"></li>
<li>
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</li>
</ul> </ul>
</li> </div>
</ul>
{% is_active_app 'passbook_admin' as is_admin %}
<ul class="nav navbar-nav navbar-primary {% if is_admin == 'active' %}persistent-secondary{% endif %}">
<li class="{% is_active_url 'passbook_core:overview' %}">
<a href="{% url 'passbook_core:overview' %}">{% trans 'Overview' %}</a>
</li>
{% if user.is_superuser %}
<li class="{% is_active_app 'passbook_admin' %}">
<a href="{% url 'passbook_admin:overview' %}">{% trans 'Administration' %}</a>
{% block nav_secondary %}
{% endblock %}
</li>
{% endif %}
</ul>
</div>
</nav> </nav>
<div class="container-fluid container-cards-pf"> <div class="container-fluid container-cards-pf">
<div class="container"> {% block content %}
{% include 'partials/messages.html' %} {% endblock %}
</div>
{% block content %}
{% endblock %}
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
// initialize tooltips // initialize tooltips
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
// Initialize the vertical navigation // Initialize the vertical navigation
$().setupVerticalNavigation(true); $().setupVerticalNavigation(true);
}); });
</script> </script>
{% endblock %} {% 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,48 +3,48 @@
{% 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 %}
{{ field.label }} for="{{ field.name }}-{{ forloop.counter0 }}">
{{ 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 }}"
<label class="col-sm-2 control-label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label> 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>
</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 %}
{{ field.label }} for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }}
</label> </label>
<div class="select col-sm-10"> <div class="select col-sm-10">
{{ field }} {{ field }}
</div> </div>
{% elif field.field.widget|fieldtype == 'CheckboxInput' %} {% elif field.field.widget|fieldtype == 'CheckboxInput' %}
<label class="checkbox-label"> <label class="checkbox-label">
{{ field }} {{ field.label }} {{ field }} {{ field.label }}
</label> </label>
{% if show_password_forget_notice %} {% else %}
<a href="#">{% trans 'Forgot password?' %}</a> <label class="col-sm-2 sr-only" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }}
</label>
{{ field|css_class:'form-control input-lg' }}
{% if field.help_text %}
<span>
{{ field.help_text }}
</span>
{% endif %} {% endif %}
{% else %} {% endif %}
<label class="col-sm-2 sr-only" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}"> {% for error in field.errors %}
{{ field.label }} <span class="help-block">
</label> {{ error }}
{{ field|css_class:'form-control input-lg' }} </span>
{% if field.help_text %} {% endfor %}
<span>
{{ field.help_text }}
</span>
{% endif %}
{% endif %}
{% for error in field.errors %}
<hr>
<div class="alert alert-danger alert-block">
<span class="pficon pficon-error-circle-o"></span>
<strong>{{ error }}</strong>
</div>
{% endfor %}
</div> </div>
{% endfor %} {% endfor %}

View File

@ -1,19 +1,19 @@
{% if messages %} {% if messages %}
{% for msg in messages %} {% for msg in messages %}
<div class="alert alert-{{ msg.level_tag }}"> <div class="toast-pf alert alert-dismissable alert-{{ msg.level_tag }}">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span class="pficon pficon-close"></span> <span class="pficon pficon-close"></span>
</button> </button>
{% if msg.level_tag == 'danger' %} {% if msg.level_tag == 'danger' %}
<span class="pficon pficon-error-circle-o"></span> <span class="pficon pficon-error-circle-o"></span>
{% elif msg.level_tag == 'warning' %} {% elif msg.level_tag == 'warning' %}
<span class="pficon pficon-warning-triangle-o"></span> <span class="pficon pficon-warning-triangle-o"></span>
{% elif msg.level_tag == 'success' %} {% elif msg.level_tag == 'success' %}
<span class="pficon pficon-ok"></span> <span class="pficon pficon-ok"></span>
{% elif msg.level_tag == 'info' %} {% elif msg.level_tag == 'info' %}
<span class="pficon pficon-info"></span> <span class="pficon pficon-info"></span>
{% endif %} {% endif %}
<strong>{{ msg.message|safe }}</strong> {{ msg.message|safe }}
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

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).with_request(context.get('request')).build()
if policy_engine.result[0] and _link:
matching_factors.append(_link) matching_factors.append(_link)
return matching_factors return matching_factors

View File

View File

@ -0,0 +1,150 @@
"""passbook Core Account Test"""
import string
from random import SystemRandom
from django.test import TestCase
from django.urls import reverse
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import User
class TestAuthenticationViews(TestCase):
"""passbook Core Account Test"""
def setUp(self):
super().setUp()
self.sign_up_data = {
'name': 'Test',
'username': 'beryjuorg',
'email': 'unittest@passbook.beryju.org',
'password': 'B3ryju0rg!',
'password_repeat': 'B3ryju0rg!',
}
self.login_data = {
'uid_field': 'unittest@example.com',
}
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
def test_sign_up_view(self):
"""Test account.sign_up view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-sign-up'))
self.assertEqual(response.status_code, 200)
def test_login_view(self):
"""Test account.login view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-login'))
self.assertEqual(response.status_code, 200)
# test login with post
form = LoginForm(self.login_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-login'), data=form.cleaned_data)
self.assertEqual(response.status_code, 302)
def test_logout_view(self):
"""Test account.logout view"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
self.assertEqual(response.status_code, 302)
def test_sign_up_view_auth(self):
"""Test account.sign_up view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
self.assertEqual(response.status_code, 302)
def test_login_view_auth(self):
"""Test account.login view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-login'))
self.assertEqual(response.status_code, 302)
def test_login_view_post(self):
"""Test account.login view POST (Anonymous)"""
login_response = self.client.post(reverse('passbook_core:auth-login'), data=self.login_data)
self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response.url, reverse('passbook_core:auth-process'))
def test_sign_up_view_post(self):
"""Test account.sign_up view POST (Anonymous)"""
form = SignUpForm(self.sign_up_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-sign-up'), data=form.cleaned_data)
self.assertEqual(response.status_code, 302)
# def test_reset_password_init_view(self):
# """Test account.reset_password_init view POST (Anonymous)"""
# form = SignUpForm(self.sign_up_data)
# self.assertTrue(form.is_valid())
# res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=form.cleaned_data)
# self.assertEqual(res.status_code, 302)
# res = test_request(accounts.PasswordResetInitView.as_view())
# self.assertEqual(res.status_code, 200)
# def test_resend_confirmation(self):
# """Test AccountController.resend_confirmation"""
# form = SignUpForm(self.sign_up_data)
# self.assertTrue(form.is_valid())
# res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=form.cleaned_data)
# self.assertEqual(res.status_code, 302)
# user = User.objects.get(email=self.sign_up_data['email'])
# # Invalidate all other links for this user
# old_acs = AccountConfirmation.objects.filter(
# user=user)
# for old_ac in old_acs:
# old_ac.confirmed = True
# old_ac.save()
# # Create Account Confirmation UUID
# new_ac = AccountConfirmation.objects.create(user=user)
# self.assertFalse(new_ac.is_expired)
# on_user_confirm_resend.send(
# sender=None,
# user=user,
# request=None)
# def test_reset_passowrd(self):
# """Test reset password POST"""
# # Signup user first
# sign_up_form = SignUpForm(self.sign_up_data)
# self.assertTrue(sign_up_form.is_valid())
# sign_up_res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=sign_up_form.cleaned_data)
# self.assertEqual(sign_up_res.status_code, 302)
# user = User.objects.get(email=self.sign_up_data['email'])
# # Invalidate all other links for this user
# old_acs = AccountConfirmation.objects.filter(
# user=user)
# for old_ac in old_acs:
# old_ac.confirmed = True
# old_ac.save()
# # Create Account Confirmation UUID
# new_ac = AccountConfirmation.objects.create(user=user)
# self.assertFalse(new_ac.is_expired)
# uuid = AccountConfirmation.objects.filter(user=user).first().pk
# reset_res = test_request(accounts.PasswordResetFinishView.as_view(),
# method='POST',
# user=user,
# url_kwargs={'uuid': uuid},
# req_kwargs=self.change_data)
# self.assertEqual(reset_res.status_code, 302)
# self.assertEqual(reset_res.url, reverse('common-index'))

View File

@ -0,0 +1,25 @@
"""passbook user view tests"""
import string
from random import SystemRandom
from django.shortcuts import reverse
from django.test import TestCase
from passbook.core.models import User
class TestOverviewViews(TestCase):
"""Test Overview Views"""
def setUp(self):
super().setUp()
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
self.client.force_login(self.user)
def test_overview(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:overview')).status_code, 200)

View File

@ -0,0 +1,47 @@
"""passbook user view tests"""
import string
from random import SystemRandom
from django.shortcuts import reverse
from django.test import TestCase
from passbook.core.forms.users import PasswordChangeForm
from passbook.core.models import User
class TestUserViews(TestCase):
"""Test User Views"""
def setUp(self):
super().setUp()
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
self.client.force_login(self.user)
def test_user_settings(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:user-settings')).status_code, 200)
def test_user_delete(self):
"""Test UserDeleteView"""
self.assertEqual(self.client.post(reverse('passbook_core:user-delete')).status_code, 302)
self.assertEqual(User.objects.filter(username='unittest user').exists(), False)
self.setUp()
def test_user_change_password(self):
"""Test UserChangePasswordView"""
form_data = {
'password': 'test2',
'password_repeat': 'test2'
}
form = PasswordChangeForm(data=form_data)
self.assertTrue(form.is_valid())
self.assertEqual(self.client.get(
reverse('passbook_core:user-change-password')).status_code, 200)
self.assertEqual(self.client.post(
reverse('passbook_core:user-change-password'), data=form_data).status_code, 302)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('test2'))

View File

@ -0,0 +1,25 @@
"""passbook util view tests"""
from django.test import RequestFactory, TestCase
from passbook.core.views.utils import LoadingView, PermissionDeniedView
class TestUtilViews(TestCase):
"""Test Utility Views"""
def setUp(self):
self.factory = RequestFactory()
def test_loading_view(self):
"""Test loading view"""
request = self.factory.get('something')
response = LoadingView.as_view(target_url='somestring')(request)
response.render()
self.assertIn('somestring', response.content.decode('utf-8'))
def test_permission_denied_view(self):
"""Test PermissionDeniedView"""
request = self.factory.get('something')
response = PermissionDeniedView.as_view()(request)
self.assertEqual(response.status_code, 200)

View File

@ -19,13 +19,17 @@ core_urls = [
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'), path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'), path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
path('auth/sign_up/<uuid:nonce>/confirm/', authentication.SignUpConfirmView.as_view(),
name='auth-sign-up-confirm'),
path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'), path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'),
path('auth/password/reset/<uuid:nonce>/', authentication.PasswordResetView.as_view(),
name='auth-password-reset'),
path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'), path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'),
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'), path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
# User views # User views
path('user/', user.UserSettingsView.as_view(), name='user-settings'), path('_/user/', user.UserSettingsView.as_view(), name='user-settings'),
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'), path('_/user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
path('user/change_password/', user.UserChangePasswordView.as_view(), path('_/user/change_password/', user.UserChangePasswordView.as_view(),
name='user-change-password'), name='user-change-password'),
# Overview # Overview
path('', overview.OverviewView.as_view(), name='overview'), path('', overview.OverviewView.as_view(), name='overview'),

View File

@ -1,7 +1,8 @@
"""passbook access helper classes""" """passbook access helper classes"""
from logging import getLogger from logging import getLogger
from django.http import Http404 from django.contrib import messages
from django.utils.translation import gettext as _
from passbook.core.models import Application from passbook.core.models import Application
@ -11,14 +12,18 @@ class AccessMixin:
"""Mixin class for usage in Authorization views. """Mixin class for usage in Authorization views.
Provider functions to check application access, etc""" Provider functions to check application access, etc"""
# request is set by view but since this Mixin has no base class
request = None
def provider_to_application(self, provider): def provider_to_application(self, provider):
"""Lookup application assigned to provider, throw error if no application assigned""" """Lookup application assigned to provider, throw error if no application assigned"""
try: try:
return provider.application return provider.application
except Application.DoesNotExist as exc: except Application.DoesNotExist as exc:
# TODO: Log that no provider has no application assigned messages.error(self.request, _('Provider "%(name)s" has no application assigned' % {
LOGGER.warning('Provider "%s" has no application assigned...', provider) 'name': provider
raise Http404 from exc }))
raise exc
def user_has_access(self, application, user): def user_has_access(self, application, user):
"""Check if user has access to application.""" """Check if user has access to application."""

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 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 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, 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"""
@ -41,15 +45,20 @@ class LoginView(UserPassesTestMixin, FormView):
kwargs['title'] = _('Log in to your account') kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in') kwargs['primary_action'] = _('Log in')
kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled') kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled')
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled') kwargs['sources'] = []
kwargs['sources'] = Source.objects.filter(enabled=True).select_subclasses() sources = Source.objects.filter(enabled=True).select_subclasses()
if any(source.is_link for source in kwargs['sources']): if any(source.is_link for source in sources):
self.template_name = 'login/test.html' for source in sources:
kwargs['sources'].append(source.get_login_button)
self.template_name = 'login/with_sources.html'
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_user(self, uid_value) -> User: def get_user(self, uid_value) -> User:
"""Find user instance. Returns None if no user was found.""" """Find user instance. Returns None if no user was found."""
for search_field in CONFIG.y('passbook.uid_fields'): for search_field in CONFIG.y('passbook.uid_fields'):
# Workaround for E-Mail -> email
if search_field == 'e-mail':
search_field = 'email'
users = User.objects.filter(**{search_field: uid_value}) users = User.objects.filter(**{search_field: uid_value})
if users.exists(): if users.exists():
LOGGER.debug("Found user %s with uid_field %s", users.first(), search_field) LOGGER.debug("Found user %s with uid_field %s", users.first(), search_field)
@ -64,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"""
@ -133,7 +143,32 @@ class SignUpView(UserPassesTestMixin, FormView):
def form_valid(self, form: SignUpForm) -> HttpResponse: def form_valid(self, form: SignUpForm) -> HttpResponse:
"""Create user""" """Create user"""
self._user = SignUpView.create_user(form.cleaned_data, self.request) try:
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
if self._invitation and not self._invitation.needs_confirmation:
needs_confirmation = False
if needs_confirmation:
nonce = Nonce.objects.create(user=self._user)
LOGGER.debug(str(nonce.uuid))
# 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.save()
self.consume_invitation() self.consume_invitation()
messages.success(self.request, _("Successfully signed up!")) messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s", LOGGER.debug("Successfully signed up %s",
@ -162,26 +197,58 @@ 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
new_user.set_password(data.get('password')) try:
new_user.save() new_user.set_password(data.get('password'))
request.user = new_user new_user.save()
# Send signal for other auth sources request.user = new_user
user_signed_up.send( # Send signal for other auth sources
sender=SignUpView, user_signed_up.send(
user=new_user, sender=SignUpView,
request=request) user=new_user,
# TODO: Implement Verification, via email or others request=request)
# if needs_confirmation: return new_user
# Create Account Confirmation UUID except PasswordPolicyInvalid as exc:
# AccountConfirmation.objects.create(user=new_user) new_user.delete()
return new_user raise exc
class SignUpConfirmView(View):
"""Confirm registration from Nonce"""
def get(self, request, nonce):
"""Verify UUID and activate user"""
nonce = get_object_or_404(Nonce, uuid=nonce)
nonce.user.is_active = True
nonce.user.save()
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _('Successfully confirmed registration.'))
return redirect('passbook_core:overview')
class PasswordResetView(View):
"""Temporarily authenticate User and allow them to reset their password"""
def get(self, request, nonce):
"""Authenticate user with nonce and redirect to password change view"""
# 3. (Optional) Trap user in password change view
nonce = get_object_or_404(Nonce, uuid=nonce)
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _(('Temporarily authenticated with Nonce, '
'please change your password')))
return redirect('passbook_core:user-change-password')

View File

@ -1,19 +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
@ -37,10 +45,21 @@ 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):
self.request.user.set_password(form.cleaned_data.get('password')) try:
self.request.user.save() # user.set_password checks against Policies so we don't need to manually do it here
update_session_auth_hash(self.request, self.request.user) self.request.user.set_password(form.cleaned_data.get('password'))
messages.success(self.request, _('Successfully changed password')) self.request.user.save()
update_session_auth_hash(self.request, self.request.user)
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

@ -10,7 +10,8 @@ https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.settings')
application = get_wsgi_application() application = Sentry(get_wsgi_application())

View File

@ -0,0 +1,2 @@
"""passbook hibp_policy"""
__version__ = '0.1.2-beta'

View File

@ -0,0 +1,5 @@
"""Passbook HIBP Admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_hibp_policy')

View File

@ -0,0 +1,11 @@
"""Passbook hibp app config"""
from django.apps import AppConfig
class PassbookHIBPConfig(AppConfig):
"""Passbook hibp app config"""
name = 'passbook.hibp_policy'
label = 'passbook_hibp_policy'
verbose_name = 'passbook HaveIBeenPwned Policy'

View File

@ -0,0 +1,19 @@
"""passbook HaveIBeenPwned Policy forms"""
from django import forms
from passbook.core.forms.policies import GENERAL_FIELDS
from passbook.hibp_policy.models import HaveIBeenPwendPolicy
class HaveIBeenPwnedPolicyForm(forms.ModelForm):
"""Edit HaveIBeenPwendPolicy instances"""
class Meta:
model = HaveIBeenPwendPolicy
fields = GENERAL_FIELDS + ['allowed_count']
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}

View File

@ -0,0 +1,28 @@
# Generated by Django 2.1.7 on 2019-02-25 15:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0011_auto_20190225_1438'),
]
operations = [
migrations.CreateModel(
name='HaveIBeenPwendPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('allowed_count', models.IntegerField(default=0)),
],
options={
'verbose_name': 'HaveIBeenPwned Policy',
'verbose_name_plural': 'HaveIBeenPwned Policies',
},
bases=('passbook_core.policy',),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2019-02-25 19:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_hibp_policy', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='haveibeenpwendpolicy',
options={'verbose_name': 'have i been pwned Policy', 'verbose_name_plural': 'have i been pwned Policies'},
),
]

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'},
),
]

Some files were not shown because too many files have changed in this diff Show More