Compare commits
58 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
c6721a83a4 | |||
46866e8ef0 | |||
4a49681127 | |||
4c3fced4e9 | |||
172347d90f | |||
f54520b5cf | |||
d7c4697625 | |||
5584f5bda8 | |||
2ce6f5a714 | |||
c66945623a | |||
cbae05c74c | |||
5b771da972 | |||
2db1738e4a | |||
95de6a14fd | |||
17132ebc19 | |||
289be46388 | |||
6c300b7b31 | |||
b726583084 | |||
48055d1cfd | |||
436070f5bd | |||
3ee79818db | |||
e7a02104db | |||
556740d7bc | |||
421f51770c | |||
96f7e70f9e | |||
ad96f7dbb8 | |||
e7fb48eba2 | |||
b19b5b644d | |||
250b6691d4 | |||
e3b02a6e78 | |||
e94ef34d8f | |||
49e945307a | |||
edfe0e5450 | |||
06b65a7882 | |||
ff9bc8aa70 | |||
28da67abe6 | |||
39d9fe9bf0 | |||
750117b0fd | |||
983462f80d | |||
4ae31d409b | |||
98b414f3e2 | |||
a0d42092e3 | |||
f2569b6424 | |||
9d344d887c | |||
7e9154a0ea | |||
e0ef061771 | |||
b8694a7ade | |||
10d6a30f2c | |||
8c94aef6d0 | |||
19bd3bfffb | |||
8611ac624c | |||
fa93b59a8c | |||
8b66b40f0d | |||
c2756f15fc | |||
408e205c5f | |||
5f3ab49535 | |||
33431ae013 | |||
b40ac6dc5d |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.0.7-alpha
|
||||
current_version = 0.1.1-beta
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -14,6 +14,8 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:helm/passbook/values.yaml]
|
||||
|
||||
[bumpversion:file:helm/passbook/Chart.yaml]
|
||||
|
||||
[bumpversion:file:.gitlab-ci.yml]
|
||||
|
@ -16,7 +16,6 @@ variables:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77'
|
||||
SUPERVISR_ENV: ci
|
||||
|
||||
include:
|
||||
- /allauth/.gitlab-ci.yml
|
||||
@ -52,9 +51,9 @@ package-docker:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
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:
|
||||
- /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.1-beta
|
||||
stage: build
|
||||
only:
|
||||
- tags
|
||||
@ -65,7 +64,7 @@ package-helm:
|
||||
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
|
||||
- helm init --client-only
|
||||
- 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:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
|
14
Dockerfile
14
Dockerfile
@ -6,10 +6,13 @@ COPY ./requirements.txt /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 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
|
||||
|
||||
@ -20,9 +23,12 @@ COPY --from=build /app/static /app/static/
|
||||
|
||||
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 && \
|
||||
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
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""passbook provider"""
|
||||
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
|
||||
|
||||
from allauth_passbook.provider import PassbookProvider
|
||||
|
||||
urlpatterns = default_urlpatterns(PassbookProvider)
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""passbook adapter"""
|
||||
import requests
|
||||
|
||||
from allauth.socialaccount import app_settings
|
||||
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
|
||||
OAuth2CallbackView,
|
||||
OAuth2LoginView)
|
||||
|
||||
from allauth_passbook.provider import PassbookProvider
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.0.7-alpha"
|
||||
appVersion: "0.1.1-beta"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: 1.0.0
|
||||
version: "0.1.1-beta"
|
||||
icon: https://passbook.beryju.org/images/logo.png
|
||||
|
@ -36,7 +36,7 @@ data:
|
||||
debug: false
|
||||
secure_proxy_header:
|
||||
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_report_enabled: {{ .Values.config.error_reporting }}
|
||||
|
||||
@ -105,10 +105,9 @@ data:
|
||||
email: mail # or userPrincipalName
|
||||
user_attribute_map:
|
||||
active_directory:
|
||||
sAMAccountName: username
|
||||
mail: email
|
||||
given_name: first_name
|
||||
name: last_name
|
||||
username: "%(sAMAccountName)s"
|
||||
email: "%(mail)s"
|
||||
name: "%(displayName)"
|
||||
# # Create new users in LDAP upon sign-up
|
||||
# create_users: true
|
||||
# # Reset LDAP password when user reset their password
|
||||
|
@ -5,7 +5,7 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
tag: latest
|
||||
tag: 0.1.1-beta
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook admin"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -11,7 +11,7 @@ class UserSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['is_superuser', 'username', 'first_name', 'last_name', 'email', 'date_joined',
|
||||
fields = ['is_superuser', 'username', 'name', 'email', 'date_joined',
|
||||
'uuid']
|
||||
|
||||
|
||||
|
@ -9,33 +9,37 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Applications" %}</h1>
|
||||
<span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span>
|
||||
<hr>
|
||||
<a href="{% url 'passbook_admin:application-create' %}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Provider' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in object_list %}
|
||||
<tr>
|
||||
<td>{{ application.name }}</td>
|
||||
<td>{{ application.provider }}</td>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
<hr>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Provider' %}</th>
|
||||
<th>{% trans 'Provider Type' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in object_list %}
|
||||
<tr>
|
||||
<td>{{ application.name }}</td>
|
||||
<td>{{ application.get_provider }}</td>
|
||||
<td>{{ application.get_provider|verbose_name }}</td>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -8,80 +8,80 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans "Audit Log" %}</h1>
|
||||
<div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
|
||||
<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">
|
||||
{% for entry in object_list %}
|
||||
<div class="list-group-item">
|
||||
<div class="list-view-pf-main-info">
|
||||
<div class="list-view-pf-left">
|
||||
<span class="fa fa-plane list-view-pf-icon-sm"></span>
|
||||
<div class="list-view-pf-main-info">
|
||||
<div class="list-view-pf-left">
|
||||
<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 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>
|
||||
{% endfor %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Row Checkbox Selection
|
||||
$("#pf-list-standard input[type='checkbox']").change(function (e) {
|
||||
if ($(this).is(":checked")) {
|
||||
$(this).closest('.list-group-item').addClass("active");
|
||||
} else {
|
||||
$(this).closest('.list-group-item').removeClass("active");
|
||||
}
|
||||
});
|
||||
// toggle dropdown menu
|
||||
$('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () {
|
||||
var $this = $(this);
|
||||
var $dropdown = $this.find('.dropdown');
|
||||
var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true);
|
||||
$dropdown.toggleClass('dropup', space < 10);
|
||||
});
|
||||
// allow users to select multiple list items with shift key
|
||||
$('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) {
|
||||
var $list = $('.list-group');
|
||||
var prevIndex = $list.data('preIndex');
|
||||
var $listItems = $list.children('.list-group-item');
|
||||
var $currentItem = $(this).closest('.list-group-item');
|
||||
if (event.shiftKey && prevIndex > -1 && this.checked) {
|
||||
var currentIndex = $listItems.index($currentItem);
|
||||
var $selectScope = currentIndex - prevIndex > 0
|
||||
? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack())
|
||||
: $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack());
|
||||
$selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true);
|
||||
}
|
||||
$list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1);
|
||||
});
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Row Checkbox Selection
|
||||
$("#pf-list-standard input[type='checkbox']").change(function (e) {
|
||||
if ($(this).is(":checked")) {
|
||||
$(this).closest('.list-group-item').addClass("active");
|
||||
} else {
|
||||
$(this).closest('.list-group-item').removeClass("active");
|
||||
}
|
||||
});
|
||||
// toggle dropdown menu
|
||||
$('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () {
|
||||
var $this = $(this);
|
||||
var $dropdown = $this.find('.dropdown');
|
||||
var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true);
|
||||
$dropdown.toggleClass('dropup', space < 10);
|
||||
});
|
||||
// allow users to select multiple list items with shift key
|
||||
$('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) {
|
||||
var $list = $('.list-group');
|
||||
var prevIndex = $list.data('preIndex');
|
||||
var $listItems = $list.children('.list-group-item');
|
||||
var $currentItem = $(this).closest('.list-group-item');
|
||||
if (event.shiftKey && prevIndex > -1 && this.checked) {
|
||||
var currentIndex = $listItems.index($currentItem);
|
||||
var $selectScope = currentIndex - prevIndex > 0
|
||||
? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack())
|
||||
: $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack());
|
||||
$selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true);
|
||||
}
|
||||
$list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1);
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% include 'partials/pagination.html' %}
|
||||
});
|
||||
</script>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
@ -20,7 +20,8 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -39,7 +40,7 @@
|
||||
{% for factor in object_list %}
|
||||
<tr>
|
||||
<td>{{ factor.name }} ({{ factor.slug }})</td>
|
||||
<td>{{ factor.type }}</td>
|
||||
<td>{{ factor|verbose_name }}</td>
|
||||
<td>{{ factor.order }}</td>
|
||||
<td>{{ factor.enabled }}</td>
|
||||
<td>
|
||||
|
@ -9,32 +9,35 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Invitations" %}</h1>
|
||||
<span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span>
|
||||
<hr>
|
||||
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Expiry' %}</th>
|
||||
<th>{% trans 'Link' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invitation in object_list %}
|
||||
<tr>
|
||||
<td>{{ invitation.expires|default:"Never" }}</td>
|
||||
<td><pre>{{ invitation.link }}</pre></td>
|
||||
<td>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
<hr>
|
||||
<a href="{% url 'passbook_admin:invitation-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Expiry' %}</th>
|
||||
<th>{% trans 'Link' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invitation in object_list %}
|
||||
<tr>
|
||||
<td>{{ invitation.expires|default:"Never" }}</td>
|
||||
<td>
|
||||
<pre>{{ invitation.link }}</pre>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -7,11 +7,18 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -19,11 +26,18 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,11 +45,22 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,11 +68,18 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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">
|
||||
<a href="{% url 'passbook_admin:factors' %}">
|
||||
<span class="pficon pficon-ok"></span>{{ factor_count }}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,11 +87,18 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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">
|
||||
<a href="{% url 'passbook_admin:policies' %}">
|
||||
<span class="pficon pficon-ok"></span>{{ policy_count }}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,11 +106,18 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -79,11 +125,61 @@
|
||||
<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="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>
|
||||
<div class="card-pf-body">
|
||||
<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 may not work.' %}"></span> {{ worker_count }}
|
||||
{% else %}
|
||||
<span class="pficon pficon-ok"></span>{{ worker_count }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,42 +9,46 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Policies" %}</h1>
|
||||
<span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||
{% trans 'Create...' %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</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 policy in object_list %}
|
||||
<tr>
|
||||
<td>{{ policy.name }}</td>
|
||||
<td>{{ policy|fieldtype }}</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>
|
||||
<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>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||
{% trans 'Create...' %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% for type, name in types.items %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Type' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for policy in object_list %}
|
||||
<tr>
|
||||
<td>{{ policy.name }}</td>
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
@ -10,45 +10,57 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Providers" %}</h1>
|
||||
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||
{% trans 'Create...' %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</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>
|
||||
<h1><span class="pficon-integration"></span> {% trans "Providers" %}</h1>
|
||||
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||
{% trans 'Create...' %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% for type, name in types.items %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||
href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
<hr>
|
||||
<div class="dropdown">
|
||||
@ -17,7 +17,7 @@
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||
{% for type, name in types.items %}
|
||||
<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 %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -27,6 +27,7 @@
|
||||
<tr>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Class' %}</th>
|
||||
<th>{% trans 'Additional Info' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -35,6 +36,7 @@
|
||||
<tr>
|
||||
<td>{{ source.name }}</td>
|
||||
<td>{{ source|fieldtype }}</td>
|
||||
<td>{{ source.additional_info }}</td>
|
||||
<td>
|
||||
<a class="btn btn-default btn-sm"
|
||||
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
|
@ -5,34 +5,36 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Users" %}</h1>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Username' %}</th>
|
||||
<th>{% trans 'First Name' %}</th>
|
||||
<th>{% trans 'Last Name' %}</th>
|
||||
<th>{% trans 'Active' %}</th>
|
||||
<th>{% trans 'Last Login' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in object_list %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.first_name|default:'-' }}</td>
|
||||
<td>{{ user.last_name|default:'-' }}</td>
|
||||
<td>{{ user.is_active }}</td>
|
||||
<td>{{ user.last_login }}</td>
|
||||
<td>
|
||||
<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" href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Username' %}</th>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Active' %}</th>
|
||||
<th>{% trans 'Last Login' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in object_list %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.name|default:'-' }}</td>
|
||||
<td>{{ user.is_active }}</td>
|
||||
<td>{{ user.last_login }}</td>
|
||||
<td>
|
||||
<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"
|
||||
href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a class="btn btn-default btn-sm"
|
||||
href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,12 @@
|
||||
{% extends "generic/form.html" %}
|
||||
|
||||
{% load utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>{% trans 'Create' %}</h1>
|
||||
{% endblock %}
|
||||
<h1>{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
@ -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 %}
|
@ -11,7 +11,7 @@
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% include 'partials/form.html' with form=form %}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,12 @@
|
||||
{% extends "generic/form.html" %}
|
||||
|
||||
{% load utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>{% trans 'Update' %}</h1>
|
||||
{% endblock %}
|
||||
<h1>{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
@ -56,6 +56,8 @@ urlpatterns = [
|
||||
users.UserUpdateView.as_view(), name='user-update'),
|
||||
path('users/<int:pk>/delete/',
|
||||
users.UserDeleteView.as_view(), name='user-delete'),
|
||||
path('users/<int:pk>/reset/',
|
||||
users.UserPasswordResetView.as_view(), name='user-password-reset'),
|
||||
# Audit Log
|
||||
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
|
||||
# Groups
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook Application administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -13,6 +14,7 @@ class ApplicationListView(AdminRequiredMixin, ListView):
|
||||
"""Show list of all applications"""
|
||||
|
||||
model = Application
|
||||
ordering = 'name'
|
||||
template_name = 'administration/application/list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -28,6 +30,10 @@ class ApplicationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView)
|
||||
success_url = reverse_lazy('passbook_admin:applications')
|
||||
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):
|
||||
"""Update application"""
|
||||
@ -45,5 +51,10 @@ class ApplicationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView)
|
||||
|
||||
model = Application
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
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)
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook Factor administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -33,25 +34,24 @@ class FactorListView(AdminRequiredMixin, ListView):
|
||||
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create new Factor"""
|
||||
|
||||
template_name = 'generic/create_inheritance.html'
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully created Factor')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
source_type = self.request.GET.get('type')
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||
factor_type = self.request.GET.get('type')
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
|
||||
kwargs['type'] = model._meta.verbose_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get('type')
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||
factor_type = self.request.GET.get('type')
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||
"""Update factor"""
|
||||
|
||||
@ -61,11 +61,12 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||
success_message = _('Successfully updated Factor')
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get('type')
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
|
||||
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
"""Delete factor"""
|
||||
@ -73,7 +74,11 @@ class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
model = Factor
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully updated Factor')
|
||||
success_message = _('Successfully deleted Factor')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
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)
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook Invitation administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
@ -26,6 +27,10 @@ class InvitationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
success_message = _('Successfully created Invitation')
|
||||
form_class = InvitationForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['type'] = 'Invitation'
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
@ -42,4 +47,8 @@ class InvitationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
model = Invitation
|
||||
template_name = 'generic/delete.html'
|
||||
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)
|
||||
|
@ -2,6 +2,8 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
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,
|
||||
Provider, Source, User)
|
||||
|
||||
@ -19,4 +21,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
kwargs['source_count'] = len(Source.objects.all())
|
||||
kwargs['factor_count'] = len(Factor.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)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -32,7 +32,7 @@ class PolicyListView(AdminRequiredMixin, ListView):
|
||||
class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create new Policy"""
|
||||
|
||||
template_name = 'generic/create_inheritance.html'
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:policies')
|
||||
success_message = _('Successfully created Policy')
|
||||
|
||||
@ -68,11 +68,15 @@ class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
model = Policy
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:policies')
|
||||
success_message = _('Successfully updated Policy')
|
||||
success_message = _('Successfully deleted Policy')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
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):
|
||||
"""View to test policy(s)"""
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook Provider administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -28,7 +29,7 @@ class ProviderListView(AdminRequiredMixin, ListView):
|
||||
class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create new Provider"""
|
||||
|
||||
template_name = 'generic/create_inheritance.html'
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:providers')
|
||||
success_message = _('Successfully created Provider')
|
||||
|
||||
@ -64,7 +65,11 @@ class ProviderDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
model = Provider
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:providers')
|
||||
success_message = _('Successfully updated Provider')
|
||||
success_message = _('Successfully deleted Provider')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
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)
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook Source administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -33,7 +34,7 @@ class SourceListView(AdminRequiredMixin, ListView):
|
||||
class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create new Source"""
|
||||
|
||||
template_name = 'generic/create_inheritance.html'
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:sources')
|
||||
success_message = _('Successfully created Source')
|
||||
|
||||
@ -66,9 +67,13 @@ class SourceDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
"""Delete source"""
|
||||
|
||||
model = Source
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:sources')
|
||||
success_message = _('Successfully updated Source')
|
||||
success_message = _('Successfully deleted Source')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
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)
|
||||
|
@ -1,12 +1,15 @@
|
||||
"""passbook User administration"""
|
||||
from django.contrib import messages
|
||||
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.views import View
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core.forms.users import UserDetailForm
|
||||
from passbook.core.models import User
|
||||
from passbook.core.models import Nonce, User
|
||||
|
||||
|
||||
class UserListView(AdminRequiredMixin, ListView):
|
||||
@ -31,6 +34,24 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
"""Delete user"""
|
||||
|
||||
model = User
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
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')
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook api"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -14,8 +14,8 @@ class OpenIDUserInfoView(ScopedResourceMixin, View):
|
||||
payload = {
|
||||
'sub': request.user.uuid.int,
|
||||
'name': request.user.get_full_name(),
|
||||
'given_name': request.user.first_name,
|
||||
'family_name': request.user.last_name,
|
||||
'given_name': request.user.name,
|
||||
'family_name': '',
|
||||
'preferred_username': request.user.username,
|
||||
'email': request.user.email,
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook audit Header"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -51,7 +51,10 @@ class AuditEntry(UUIDModel):
|
||||
def create(action, request, **kwargs):
|
||||
"""Create AuditEntry from arguments"""
|
||||
client_ip, _ = get_client_ip(request)
|
||||
user = request.user
|
||||
if not hasattr(request, 'user'):
|
||||
user = None
|
||||
else:
|
||||
user = request.user
|
||||
if isinstance(user, AnonymousUser):
|
||||
user = kwargs.get('user', None)
|
||||
entry = AuditEntry.objects.create(
|
||||
@ -60,7 +63,7 @@ class AuditEntry(UUIDModel):
|
||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||
request_ip=client_ip or '255.255.255.255',
|
||||
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
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -84,6 +87,8 @@ class LoginAttempt(CreatedUpdatedModel):
|
||||
@staticmethod
|
||||
def attempt(target_uid, request):
|
||||
"""Helper function to create attempt or count up existing one"""
|
||||
if not target_uid:
|
||||
return
|
||||
client_ip, _ = get_client_ip(request)
|
||||
# Since we can only use 254 chars for target_uid, truncate target_uid.
|
||||
target_uid = target_uid[:254]
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook captcha_factor Header"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook core"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -1,15 +1,19 @@
|
||||
"""passbook multi-factor authentication engine"""
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms.utils import ErrorList
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from passbook.core.auth.factor import AuthenticationFactor
|
||||
from passbook.core.auth.view import AuthenticationView
|
||||
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
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
@ -21,6 +25,29 @@ class PasswordFactor(FormView, AuthenticationFactor):
|
||||
form_class = PasswordFactorForm
|
||||
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):
|
||||
"""Authenticate against django's authentication backend"""
|
||||
uid_fields = CONFIG.y('passbook.uid_fields')
|
||||
|
@ -4,15 +4,23 @@ from logging import getLogger
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from passbook.core.models import Factor, User
|
||||
from passbook.core.policies import PolicyEngine
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
|
||||
from passbook.lib.utils.urls import is_url_absolute
|
||||
|
||||
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):
|
||||
"""Wizard-like Multi-factor authenticator"""
|
||||
@ -37,7 +45,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
# Function from UserPassesTestMixin
|
||||
if 'next' in self.request.GET:
|
||||
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):
|
||||
# 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])
|
||||
else:
|
||||
# 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
|
||||
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
|
||||
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()
|
||||
self.pending_factors = []
|
||||
for factor in _all_factors:
|
||||
if factor.passes(self.pending_user):
|
||||
policy_engine = PolicyEngine(factor.policies.all())
|
||||
policy_engine.for_user(self.pending_user)
|
||||
if policy_engine.result[0]:
|
||||
self.pending_factors.append((factor.uuid.hex, factor.type))
|
||||
# Read and instantiate factor from session
|
||||
factor_uuid, factor_class = None, None
|
||||
@ -101,8 +111,8 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
self.pending_factors
|
||||
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
||||
LOGGER.debug("Rendering Factor is %s", next_factor)
|
||||
# return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
|
||||
return redirect(reverse('passbook_core:auth-process'))
|
||||
# return _redirect_with_qs('passbook_core:auth-process', kwargs={'factor': next_factor})
|
||||
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
|
||||
# User passed all factors
|
||||
LOGGER.debug("User passed all factors, logging in")
|
||||
return self._user_passed()
|
||||
@ -111,8 +121,8 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
"""Show error message, user cannot login.
|
||||
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
||||
LOGGER.debug("User invalid")
|
||||
self._cleanup()
|
||||
return redirect(reverse('passbook_core:auth-denied'))
|
||||
self.cleanup()
|
||||
return _redirect_with_qs('passbook_core:auth-denied', self.request.GET)
|
||||
|
||||
def _user_passed(self):
|
||||
"""User Successfully passed all factors"""
|
||||
@ -121,13 +131,13 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
login(self.request, self.pending_user, backend=backend)
|
||||
LOGGER.debug("Logged in user %s", self.pending_user)
|
||||
# Cleanup
|
||||
self._cleanup()
|
||||
self.cleanup()
|
||||
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(reverse('passbook_core:overview'))
|
||||
return _redirect_with_qs('passbook_core:overview')
|
||||
|
||||
def _cleanup(self):
|
||||
def cleanup(self):
|
||||
"""Remove temporary data from session"""
|
||||
session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS,
|
||||
self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ]
|
||||
|
10
passbook/core/exceptions.py
Normal file
10
passbook/core/exceptions.py
Normal 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
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.lib.utils.ui import human_list
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
@ -15,13 +16,16 @@ class LoginForm(forms.Form):
|
||||
"""Allow users to login"""
|
||||
|
||||
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)
|
||||
|
||||
def __init__(self, *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'].widget.attrs = {
|
||||
'placeholder': _(human_list([x.title() for x in CONFIG.y('passbook.uid_fields')]))
|
||||
}
|
||||
|
||||
def clean_uid_field(self):
|
||||
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
|
||||
@ -34,10 +38,8 @@ class SignUpForm(forms.Form):
|
||||
"""SignUp Form"""
|
||||
|
||||
title = _('Sign Up')
|
||||
first_name = forms.CharField(label=_('First Name'),
|
||||
widget=forms.TextInput(attrs={'placeholder': _('First Name')}))
|
||||
last_name = forms.CharField(label=_('Last Name'),
|
||||
widget=forms.TextInput(attrs={'placeholder': _('Last Name')}))
|
||||
name = forms.CharField(label=_('Name'),
|
||||
widget=forms.TextInput(attrs={'placeholder': _('Name')}))
|
||||
username = forms.CharField(label=_('Username'),
|
||||
widget=forms.TextInput(attrs={'placeholder': _('Username')}))
|
||||
email = forms.EmailField(label=_('E-Mail'),
|
||||
@ -87,4 +89,7 @@ class SignUpForm(forms.Form):
|
||||
class PasswordFactorForm(forms.Form):
|
||||
"""Password authentication form"""
|
||||
|
||||
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))
|
||||
password = forms.CharField(widget=forms.PasswordInput(attrs={
|
||||
'placeholder': _('Password'),
|
||||
'autofocus': 'autofocus'
|
||||
}))
|
||||
|
@ -11,7 +11,7 @@ class PasswordFactorForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = PasswordFactor
|
||||
fields = GENERAL_FIELDS + ['backends']
|
||||
fields = GENERAL_FIELDS + ['backends', 'password_policies']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'order': forms.NumberInput(),
|
||||
|
@ -27,7 +27,7 @@ class InvitationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = Invitation
|
||||
fields = ['expires', 'fixed_username', 'fixed_email']
|
||||
fields = ['expires', 'fixed_username', 'fixed_email', 'needs_confirmation']
|
||||
labels = {
|
||||
'fixed_username': "Force user's username (optional)",
|
||||
'fixed_email': "Force user's email (optional)",
|
||||
|
@ -3,7 +3,8 @@
|
||||
from django import forms
|
||||
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', ]
|
||||
|
||||
@ -50,3 +51,25 @@ class DebugPolicyForm(forms.ModelForm):
|
||||
labels = {
|
||||
'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'),
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ class UserDetailForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ['username', 'first_name', 'last_name', 'email']
|
||||
fields = ['username', 'name', 'email']
|
||||
widgets = {
|
||||
'name': forms.TextInput
|
||||
}
|
||||
|
||||
class PasswordChangeForm(forms.Form):
|
||||
"""Form to update password"""
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""passbook nexus_upload management command"""
|
||||
from getpass import getpass
|
||||
from base64 import b64decode
|
||||
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
@ -24,9 +24,9 @@ class Command(BaseCommand):
|
||||
help='Nexus root URL',
|
||||
required=True)
|
||||
parser.add_argument(
|
||||
'--user',
|
||||
'--auth',
|
||||
action='store',
|
||||
help='Username to use for Nexus upload',
|
||||
help='base64-encoded string of username:password',
|
||||
required=True)
|
||||
parser.add_argument(
|
||||
'--method',
|
||||
@ -37,29 +37,21 @@ class Command(BaseCommand):
|
||||
help=('Method used for uploading files to nexus. '
|
||||
'Apt repositories use post, Helm uses put.'),
|
||||
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
|
||||
parser.add_argument('file', nargs='+', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Upload debian package to nexus repository"""
|
||||
if options.get('password') is None:
|
||||
options['password'] = getpass()
|
||||
auth = tuple(b64decode(options.get('auth')).decode('utf-8').split(':', 1))
|
||||
responses = {}
|
||||
url = 'https://%(url)s/repository/%(repo)s//' % options
|
||||
url = 'https://%(url)s/repository/%(repo)s/' % options
|
||||
method = options.get('method')
|
||||
exit_code = 0
|
||||
for file in options.get('file'):
|
||||
if method == 'post':
|
||||
responses[file] = requests.post(url, data=open(file, mode='rb'),
|
||||
auth=(options.get('user'), options.get('password')))
|
||||
responses[file] = requests.post(url, data=open(file, mode='rb'), auth=auth)
|
||||
else:
|
||||
responses[file] = requests.put(url+file, data=open(file, mode='rb'),
|
||||
auth=(options.get('user'), options.get('password')))
|
||||
responses[file] = requests.put(url+file, data=open(file, mode='rb'), auth=auth)
|
||||
self.stdout.write('Upload results:\n')
|
||||
sep = '-' * 60
|
||||
self.stdout.write('%s\n' % sep)
|
||||
|
25
passbook/core/migrations/0011_auto_20190225_1438.py
Normal file
25
passbook/core/migrations/0011_auto_20190225_1438.py
Normal 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,
|
||||
),
|
||||
]
|
31
passbook/core/migrations/0012_nonce.py
Normal file
31
passbook/core/migrations/0012_nonce.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
25
passbook/core/migrations/0014_auto_20190226_0850.py
Normal file
25
passbook/core/migrations/0014_auto_20190226_0850.py
Normal 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)
|
||||
]
|
@ -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,
|
||||
),
|
||||
]
|
38
passbook/core/migrations/0016_auto_20190227_1355.py
Normal file
38
passbook/core/migrations/0016_auto_20190227_1355.py
Normal 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')]),
|
||||
),
|
||||
]
|
@ -1,21 +1,30 @@
|
||||
"""passbook core models"""
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from logging import getLogger
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
from typing import Tuple, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from passbook.core.signals import password_changed
|
||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
def default_nonce_duration():
|
||||
"""Default duration a Nonce is valid"""
|
||||
return now() + timedelta(hours=4)
|
||||
|
||||
class Group(UUIDModel):
|
||||
"""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"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||
name = models.TextField()
|
||||
|
||||
sources = models.ManyToManyField('Source', through='UserSourceConnection')
|
||||
applications = models.ManyToManyField('Application')
|
||||
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):
|
||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||
@ -55,13 +73,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
||||
|
||||
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):
|
||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||
|
||||
@ -87,6 +98,7 @@ class PasswordFactor(Factor):
|
||||
"""Password-based Django-backend Authentication Factor"""
|
||||
|
||||
backends = ArrayField(models.TextField())
|
||||
password_policies = models.ManyToManyField('Policy', blank=True)
|
||||
|
||||
type = 'passbook.core.auth.factors.password.PasswordFactor'
|
||||
form = 'passbook.core.forms.factors.PasswordFactorForm'
|
||||
@ -94,6 +106,13 @@ class PasswordFactor(Factor):
|
||||
def has_user_settings(self):
|
||||
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):
|
||||
return "Password Factor %s" % self.slug
|
||||
|
||||
@ -136,6 +155,10 @@ class Application(PolicyModel):
|
||||
from passbook.core.policies import PolicyEngine
|
||||
return PolicyEngine(self.policies.all()).for_user(user).result
|
||||
|
||||
def get_provider(self):
|
||||
"""Get casted provider instance"""
|
||||
return Provider.objects.get_subclass(pk=self.provider.pk)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -155,10 +178,15 @@ class Source(PolicyModel):
|
||||
return False
|
||||
|
||||
@property
|
||||
def get_url(self):
|
||||
"""Return URL used for logging in"""
|
||||
def get_login_button(self):
|
||||
"""Return a tuple of URL, Icon name and Name"""
|
||||
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):
|
||||
return self.name
|
||||
|
||||
@ -195,7 +223,7 @@ class Policy(UUIDModel, CreatedUpdatedModel):
|
||||
return self.name
|
||||
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"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@ -219,8 +247,7 @@ class FieldMatcherPolicy(Policy):
|
||||
|
||||
USER_FIELDS = (
|
||||
('username', _('Username'),),
|
||||
('first_name', _('First Name'),),
|
||||
('last_name', _('Last Name'),),
|
||||
('name', _('Name'),),
|
||||
('email', _('E-Mail'),),
|
||||
('is_staff', _('Is staff'),),
|
||||
('is_active', _('Is active'),),
|
||||
@ -240,7 +267,7 @@ class FieldMatcherPolicy(Policy):
|
||||
description = "%s: %s" % (self.name, 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"""
|
||||
if not hasattr(user, self.user_field):
|
||||
raise ValueError("Field does not exist")
|
||||
@ -275,10 +302,11 @@ class PasswordPolicy(Policy):
|
||||
amount_symbols = models.IntegerField(default=0)
|
||||
length_min = models.IntegerField(default=0)
|
||||
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
|
||||
error_message = models.TextField()
|
||||
|
||||
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
|
||||
if not hasattr(user, '__password__'):
|
||||
return True
|
||||
@ -293,6 +321,8 @@ class PasswordPolicy(Policy):
|
||||
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
|
||||
result = bool(re.compile(filter_regex).match(password))
|
||||
LOGGER.debug("User got %r", result)
|
||||
if not result:
|
||||
return result, self.error_message
|
||||
return result
|
||||
|
||||
class Meta:
|
||||
@ -351,7 +381,7 @@ class DebugPolicy(Policy):
|
||||
wait = SystemRandom().randrange(self.wait_min, self.wait_max)
|
||||
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
|
||||
sleep(wait)
|
||||
return self.result
|
||||
return self.result, 'Debugging'
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -365,6 +395,7 @@ class Invitation(UUIDModel):
|
||||
expires = models.DateTimeField(default=None, blank=True, null=True)
|
||||
fixed_username = models.TextField(blank=True, default=None)
|
||||
fixed_email = models.TextField(blank=True, default=None)
|
||||
needs_confirmation = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
@ -378,3 +409,17 @@ class Invitation(UUIDModel):
|
||||
|
||||
verbose_name = _('Invitation')
|
||||
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')
|
||||
|
@ -42,7 +42,11 @@ class PolicyEngine:
|
||||
@property
|
||||
def result(self):
|
||||
"""Get policy-checking result"""
|
||||
messages = []
|
||||
for policy_result in self._group.get():
|
||||
if isinstance(policy_result, (tuple, list)):
|
||||
policy_result, policy_message = policy_result
|
||||
messages.append(policy_message)
|
||||
if policy_result is False:
|
||||
return False
|
||||
return True
|
||||
return False, messages
|
||||
return True, messages
|
||||
|
@ -73,6 +73,8 @@ INSTALLED_APPS = [
|
||||
'passbook.saml_idp.apps.PassbookSAMLIDPConfig',
|
||||
'passbook.otp.apps.PassbookOTPConfig',
|
||||
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
|
||||
'passbook.hibp_policy.apps.PassbookHIBPConfig',
|
||||
'passbook.pretend.apps.PassbookPretendConfig',
|
||||
]
|
||||
|
||||
# Message Tag fix for bootstrap CSS Classes
|
||||
@ -289,6 +291,7 @@ TEST_OUTPUT_FILE_NAME = 'unittest.xml'
|
||||
if any('test' in arg for arg in sys.argv):
|
||||
LOGGING = None
|
||||
TEST = True
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
|
||||
# Load subapps's INSTALLED_APPS
|
||||
|
@ -1,11 +1,26 @@
|
||||
"""passbook core signals"""
|
||||
|
||||
from django.core.signals import Signal
|
||||
from django.dispatch import receiver
|
||||
|
||||
# from django.db.models.signals import post_save, pre_delete
|
||||
# from django.dispatch import receiver
|
||||
# from passbook.core.models import Invitation, User
|
||||
from passbook.core.exceptions import PasswordPolicyInvalid
|
||||
|
||||
user_signed_up = Signal(providing_args=['request', 'user'])
|
||||
invitation_created = Signal(providing_args=['request', 'invitation'])
|
||||
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)
|
||||
passing, messages = policy_engine.result
|
||||
if not passing:
|
||||
raise PasswordPolicyInvalid(*messages)
|
||||
|
17
passbook/core/tasks.py
Normal file
17
passbook/core/tasks.py
Normal 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()
|
@ -6,6 +6,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>
|
||||
{% block title %}
|
||||
{% title %}
|
||||
@ -19,6 +20,7 @@
|
||||
.login-pf {
|
||||
background-attachment: fixed;
|
||||
scroll-behavior: smooth;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
{% block head %}
|
||||
|
84
passbook/core/templates/email/account_confirm.html
Normal file
84
passbook/core/templates/email/account_confirm.html
Normal 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 %}
|
78
passbook/core/templates/email/account_password_reset.html
Normal file
78
passbook/core/templates/email/account_password_reset.html
Normal 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 %}
|
129
passbook/core/templates/email/base.html
Normal file
129
passbook/core/templates/email/base.html
Normal 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’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>
|
26
passbook/core/templates/email/generic_email.html
Normal file
26
passbook/core/templates/email/generic_email.html
Normal 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 %}
|
@ -5,16 +5,20 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% block above_form %}
|
||||
<h1>{% trans 'Delete' %}</h1>
|
||||
{% endblock %}
|
||||
<div class="">
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>Are you sure you want to delete "{{ object }}"?</p>
|
||||
<a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a>
|
||||
<input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" />
|
||||
</form>
|
||||
</div>
|
||||
{% block above_form %}
|
||||
<h1>{% blocktrans with object_type=object|fieldtype|title %}Delete {{ object_type }}{% endblocktrans %}</h1>
|
||||
{% endblock %}
|
||||
<div class="">
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans with object_type=object|fieldtype|title name=object %}
|
||||
Are you sure you want to delete {{ object_type }} "{{ object }}"?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a>
|
||||
<input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -23,16 +23,16 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="toast-notifications-list-pf">
|
||||
{% include 'partials/messages.html' %}
|
||||
</div>
|
||||
<div class="login-pf-page">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-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">
|
||||
<div class="col-sm-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">
|
||||
<header class="login-pf-page-header">
|
||||
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
|
||||
alt="PatternFly logo" />
|
||||
alt="passbook logo" />
|
||||
{% if config.login.subtext %}
|
||||
<p>{{ config.login.subtext }}</p>
|
||||
{% endif %}
|
||||
|
@ -18,7 +18,6 @@
|
||||
<header class="login-pf-header">
|
||||
<h1>{% trans title %}</h1>
|
||||
</header>
|
||||
{% include 'partials/messages.html' %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% include 'partials/form_login.html' %}
|
||||
|
@ -2,3 +2,8 @@
|
||||
|
||||
{% 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 %}
|
||||
|
@ -8,10 +8,11 @@
|
||||
<h1>{% trans title %}</h1>
|
||||
</header>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% block above_form %}
|
||||
{% endblock %}
|
||||
{% include 'partials/form_login.html' %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button>
|
||||
</form>
|
||||
{% if show_sign_up_notice %}
|
||||
|
@ -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® 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® 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>
|
@ -1,51 +1,79 @@
|
||||
{% extends 'login/base.html' %}
|
||||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block row %}
|
||||
{% include 'partials/messages.html' %}
|
||||
<div class="col-md-6">
|
||||
<div class="card-pf">
|
||||
{% block head %}
|
||||
<style>
|
||||
.login-pf-page .login-pf-page-footer-links {
|
||||
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">
|
||||
<h1>{% trans title %}</h1>
|
||||
</header>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% block above_form %}
|
||||
{% endblock %}
|
||||
{% include 'partials/form_login.html' %}
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button>
|
||||
</form>
|
||||
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in to your patternfly account">
|
||||
<form method="POST">
|
||||
{% block above_form %}
|
||||
{% endblock %}
|
||||
{% include 'partials/form_login.html' %}
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</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">
|
||||
{% 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 %}
|
||||
<p class="login-pf-signup">
|
||||
{% trans 'Need an account?' %}
|
||||
<a href="{% url 'passbook_core:auth-sign-up' %}">{% trans 'Sign up' %}</a>
|
||||
</p>
|
||||
{% 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 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 %}
|
||||
|
@ -5,83 +5,83 @@
|
||||
{% load is_active %}
|
||||
|
||||
{% block body %}
|
||||
<div class="toast-notifications-list-pf">
|
||||
{% include 'partials/messages.html' %}
|
||||
</div>
|
||||
<nav class="navbar navbar-default navbar-pf" role="navigation">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1">
|
||||
<span class="sr-only">{% trans 'Toggle navigation' %}</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>
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1">
|
||||
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
{% 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>
|
||||
</li>
|
||||
</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>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid container-cards-pf">
|
||||
<div class="container">
|
||||
{% include 'partials/messages.html' %}
|
||||
</div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// initialize tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$(document).ready(function () {
|
||||
// initialize tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
// Initialize the vertical navigation
|
||||
$().setupVerticalNavigation(true);
|
||||
});
|
||||
// Initialize the vertical navigation
|
||||
$().setupVerticalNavigation(true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
<div class="form-group {% if field.errors %} has-error {% endif %}">
|
||||
{% 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 }}">
|
||||
{{ field.label }}
|
||||
@ -40,11 +40,9 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<hr>
|
||||
<div class="alert alert-danger">
|
||||
<span class="pficon pficon-error-circle-o"></span>
|
||||
<strong>{{ error }}</strong>
|
||||
</div>
|
||||
<span class="help-block">
|
||||
{{ error }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -3,48 +3,48 @@
|
||||
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group login-pf-settings">
|
||||
{% 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 }}">
|
||||
{{ field.label }}
|
||||
<div class="form-group login-pf-settings {% if field.errors %} has-error {% endif %}">
|
||||
{% 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 }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
{% for c in field %}
|
||||
<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 %}>
|
||||
<label class="col-sm-2 control-label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}>
|
||||
<label class="col-sm-2 control-label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% 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 }}">
|
||||
{{ field.label }}
|
||||
{% 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 }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="select col-sm-10">
|
||||
{{ field }}
|
||||
{{ field }}
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
<label class="checkbox-label">
|
||||
{{ field }} {{ field.label }}
|
||||
{{ field }} {{ field.label }}
|
||||
</label>
|
||||
{% if show_password_forget_notice %}
|
||||
<a href="#">{% trans 'Forgot password?' %}</a>
|
||||
{% else %}
|
||||
<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 %}
|
||||
{% else %}
|
||||
<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 %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-block">
|
||||
{{ error }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -1,19 +1,19 @@
|
||||
{% if messages %}
|
||||
{% for msg in messages %}
|
||||
<div class="alert alert-{{ msg.level_tag }}">
|
||||
{% for msg in messages %}
|
||||
<div class="toast-pf alert alert-dismissable alert-{{ msg.level_tag }}">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span class="pficon pficon-close"></span>
|
||||
<span class="pficon pficon-close"></span>
|
||||
</button>
|
||||
{% 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' %}
|
||||
<span class="pficon pficon-warning-triangle-o"></span>
|
||||
<span class="pficon pficon-warning-triangle-o"></span>
|
||||
{% elif msg.level_tag == 'success' %}
|
||||
<span class="pficon pficon-ok"></span>
|
||||
<span class="pficon pficon-ok"></span>
|
||||
{% elif msg.level_tag == 'info' %}
|
||||
<span class="pficon pficon-info"></span>
|
||||
<span class="pficon pficon-info"></span>
|
||||
{% endif %}
|
||||
<strong>{{ msg.message|safe }}</strong>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{{ msg.message|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
@ -3,6 +3,7 @@
|
||||
from django import template
|
||||
|
||||
from passbook.core.models import Factor
|
||||
from passbook.core.policies import PolicyEngine
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@ -14,6 +15,8 @@ def user_factors(context):
|
||||
matching_factors = []
|
||||
for factor in _all_factors:
|
||||
_link = factor.has_user_settings()
|
||||
if factor.passes(user) and _link:
|
||||
policy_engine = PolicyEngine(factor.policies.all())
|
||||
policy_engine.for_user(user)
|
||||
if policy_engine.result[0] and _link:
|
||||
matching_factors.append(_link)
|
||||
return matching_factors
|
||||
|
0
passbook/core/tests/__init__.py
Normal file
0
passbook/core/tests/__init__.py
Normal file
150
passbook/core/tests/test_views_authentication.py
Normal file
150
passbook/core/tests/test_views_authentication.py
Normal 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'))
|
25
passbook/core/tests/test_views_overview.py
Normal file
25
passbook/core/tests/test_views_overview.py
Normal 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)
|
47
passbook/core/tests/test_views_user.py
Normal file
47
passbook/core/tests/test_views_user.py
Normal 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'))
|
25
passbook/core/tests/test_views_utils.py
Normal file
25
passbook/core/tests/test_views_utils.py
Normal 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)
|
@ -19,13 +19,17 @@ core_urls = [
|
||||
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
||||
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/<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/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/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||
# User views
|
||||
path('user/', user.UserSettingsView.as_view(), name='user-settings'),
|
||||
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
|
||||
path('user/change_password/', user.UserChangePasswordView.as_view(),
|
||||
path('_/user/', user.UserSettingsView.as_view(), name='user-settings'),
|
||||
path('_/user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
|
||||
path('_/user/change_password/', user.UserChangePasswordView.as_view(),
|
||||
name='user-change-password'),
|
||||
# Overview
|
||||
path('', overview.OverviewView.as_view(), name='overview'),
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""passbook access helper classes"""
|
||||
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
|
||||
|
||||
@ -11,14 +12,18 @@ class AccessMixin:
|
||||
"""Mixin class for usage in Authorization views.
|
||||
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):
|
||||
"""Lookup application assigned to provider, throw error if no application assigned"""
|
||||
try:
|
||||
return provider.application
|
||||
except Application.DoesNotExist as exc:
|
||||
# TODO: Log that no provider has no application assigned
|
||||
LOGGER.warning('Provider "%s" has no application assigned...', provider)
|
||||
raise Http404 from exc
|
||||
messages.error(self.request, _('Provider "%(name)s" has no application assigned' % {
|
||||
'name': provider
|
||||
}))
|
||||
raise exc
|
||||
|
||||
def user_has_access(self, application, user):
|
||||
"""Check if user has access to application."""
|
||||
|
@ -1,24 +1,28 @@
|
||||
"""Core views"""
|
||||
"""passbook core authentication views"""
|
||||
from logging import getLogger
|
||||
from typing import Dict
|
||||
|
||||
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.forms.utils import ErrorList
|
||||
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.views import View
|
||||
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.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.tasks import send_email
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class LoginView(UserPassesTestMixin, FormView):
|
||||
"""Allow users to sign in"""
|
||||
|
||||
@ -41,15 +45,20 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||
kwargs['title'] = _('Log in to your account')
|
||||
kwargs['primary_action'] = _('Log in')
|
||||
kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled')
|
||||
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled')
|
||||
kwargs['sources'] = Source.objects.filter(enabled=True).select_subclasses()
|
||||
if any(source.is_link for source in kwargs['sources']):
|
||||
self.template_name = 'login/test.html'
|
||||
kwargs['sources'] = []
|
||||
sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||
if any(source.is_link for source in sources):
|
||||
for source in sources:
|
||||
kwargs['sources'].append(source.get_login_button)
|
||||
self.template_name = 'login/with_sources.html'
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_user(self, uid_value) -> User:
|
||||
"""Find user instance. Returns None if no user was found."""
|
||||
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})
|
||||
if users.exists():
|
||||
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)
|
||||
self.request.session.flush()
|
||||
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:
|
||||
"""Handle login for disabled users/invalid login attempts"""
|
||||
messages.error(request, _('Failed to authenticate.'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
|
||||
class LogoutView(LoginRequiredMixin, View):
|
||||
"""Log current user out"""
|
||||
|
||||
@ -133,7 +143,32 @@ class SignUpView(UserPassesTestMixin, FormView):
|
||||
|
||||
def form_valid(self, form: SignUpForm) -> HttpResponse:
|
||||
"""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()
|
||||
messages.success(self.request, _("Successfully signed up!"))
|
||||
LOGGER.debug("Successfully signed up %s",
|
||||
@ -162,26 +197,58 @@ class SignUpView(UserPassesTestMixin, FormView):
|
||||
The user created
|
||||
|
||||
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
|
||||
new_user = User.objects.create_user(
|
||||
new_user = User.objects.create(
|
||||
username=data.get('username'),
|
||||
email=data.get('email'),
|
||||
first_name=data.get('first_name'),
|
||||
last_name=data.get('last_name'),
|
||||
name=data.get('name'),
|
||||
)
|
||||
new_user.is_active = True
|
||||
new_user.set_password(data.get('password'))
|
||||
new_user.save()
|
||||
request.user = new_user
|
||||
# Send signal for other auth sources
|
||||
user_signed_up.send(
|
||||
sender=SignUpView,
|
||||
user=new_user,
|
||||
request=request)
|
||||
# TODO: Implement Verification, via email or others
|
||||
# if needs_confirmation:
|
||||
# Create Account Confirmation UUID
|
||||
# AccountConfirmation.objects.create(user=new_user)
|
||||
return new_user
|
||||
try:
|
||||
new_user.set_password(data.get('password'))
|
||||
new_user.save()
|
||||
request.user = new_user
|
||||
# Send signal for other auth sources
|
||||
user_signed_up.send(
|
||||
sender=SignUpView,
|
||||
user=new_user,
|
||||
request=request)
|
||||
return new_user
|
||||
except PasswordPolicyInvalid as exc:
|
||||
new_user.delete()
|
||||
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')
|
||||
|
@ -1,19 +1,27 @@
|
||||
"""passbook core user views"""
|
||||
from django.contrib import messages
|
||||
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.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import DeleteView, FormView, UpdateView
|
||||
|
||||
from passbook.core.exceptions import PasswordPolicyInvalid
|
||||
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
|
||||
class UserSettingsView(UpdateView):
|
||||
class UserSettingsView(SuccessMessageMixin, UpdateView):
|
||||
"""Update User settings"""
|
||||
|
||||
template_name = 'user/settings.html'
|
||||
form_class = UserDetailForm
|
||||
|
||||
success_message = _('Successfully updated user.')
|
||||
success_url = reverse_lazy('passbook_core:user-settings')
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
@ -37,10 +45,20 @@ class UserChangePasswordView(FormView):
|
||||
template_name = 'login/form_with_user.html'
|
||||
|
||||
def form_valid(self, form: PasswordChangeForm):
|
||||
self.request.user.set_password(form.cleaned_data.get('password'))
|
||||
self.request.user.save()
|
||||
update_session_auth_hash(self.request, self.request.user)
|
||||
messages.success(self.request, _('Successfully changed password'))
|
||||
try:
|
||||
self.request.user.set_password(form.cleaned_data.get('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')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
2
passbook/hibp_policy/__init__.py
Normal file
2
passbook/hibp_policy/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""passbook hibp_policy"""
|
||||
__version__ = '0.0.7-alpha'
|
5
passbook/hibp_policy/admin.py
Normal file
5
passbook/hibp_policy/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Passbook HIBP Admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_hibp_policy')
|
11
passbook/hibp_policy/apps.py
Normal file
11
passbook/hibp_policy/apps.py
Normal 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'
|
19
passbook/hibp_policy/forms.py
Normal file
19
passbook/hibp_policy/forms.py
Normal 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(),
|
||||
}
|
28
passbook/hibp_policy/migrations/0001_initial.py
Normal file
28
passbook/hibp_policy/migrations/0001_initial.py
Normal 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',),
|
||||
),
|
||||
]
|
17
passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
Normal file
17
passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
Normal 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'},
|
||||
),
|
||||
]
|
17
passbook/hibp_policy/migrations/0003_auto_20190227_1505.py
Normal file
17
passbook/hibp_policy/migrations/0003_auto_20190227_1505.py
Normal 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'},
|
||||
),
|
||||
]
|
0
passbook/hibp_policy/migrations/__init__.py
Normal file
0
passbook/hibp_policy/migrations/__init__.py
Normal file
45
passbook/hibp_policy/models.py
Normal file
45
passbook/hibp_policy/models.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""passbook HIBP Models"""
|
||||
from hashlib import sha1
|
||||
from logging import getLogger
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from requests import get
|
||||
|
||||
from passbook.core.models import Policy, User
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
class HaveIBeenPwendPolicy(Policy):
|
||||
"""Check if password is on HaveIBeenPwned's list by upload the first
|
||||
5 characters of the SHA1 Hash."""
|
||||
|
||||
allowed_count = models.IntegerField(default=0)
|
||||
|
||||
form = 'passbook.hibp_policy.forms.HaveIBeenPwnedPolicyForm'
|
||||
|
||||
def passes(self, user: User) -> bool:
|
||||
"""Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
|
||||
characters of Password in request and checks if full hash is in response. Returns 0
|
||||
if Password is not in result otherwise the count of how many times it was used."""
|
||||
# Only check if password is being set
|
||||
if not hasattr(user, '__password__'):
|
||||
return True
|
||||
password = getattr(user, '__password__')
|
||||
pw_hash = sha1(password.encode('utf-8')).hexdigest() # nosec
|
||||
url = 'https://api.pwnedpasswords.com/range/%s' % pw_hash[:5]
|
||||
result = get(url).text
|
||||
final_count = 0
|
||||
for line in result.split('\r\n'):
|
||||
full_hash, count = line.split(':')
|
||||
if pw_hash[5:] == full_hash.lower():
|
||||
final_count = int(count)
|
||||
LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5])
|
||||
if final_count > self.allowed_count:
|
||||
return False, _("Password exists on %(count)d online lists." % {'count': final_count})
|
||||
return True
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Have I Been Pwned Policy')
|
||||
verbose_name_plural = _('Have I Been Pwned Policies')
|
@ -1,2 +1,2 @@
|
||||
"""Passbook ldap app Header"""
|
||||
__version__ = '0.0.7-alpha'
|
||||
__version__ = '0.1.1-beta'
|
||||
|
@ -39,7 +39,7 @@ class LDAPSourceForm(forms.ModelForm):
|
||||
# (MODE_CREATE_USERS, _('Create Users'))
|
||||
# )
|
||||
|
||||
# namespace = 'supervisr.mod.auth.ldap'
|
||||
# namespace = 'passbook.ldap'
|
||||
# settings = ['enabled', 'mode']
|
||||
|
||||
# widgets = {
|
||||
@ -51,7 +51,7 @@ class LDAPSourceForm(forms.ModelForm):
|
||||
# class ConnectionSettings(SettingsForm):
|
||||
# """Connection settings form"""
|
||||
|
||||
# namespace = 'supervisr.mod.auth.ldap'
|
||||
# namespace = 'passbook.ldap'
|
||||
# settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain']
|
||||
|
||||
# attrs_map = {
|
||||
@ -68,7 +68,7 @@ class LDAPSourceForm(forms.ModelForm):
|
||||
# class AuthenticationBackendSettings(SettingsForm):
|
||||
# """Authentication backend settings"""
|
||||
|
||||
# namespace = 'supervisr.mod.auth.ldap'
|
||||
# namespace = 'passbook.ldap'
|
||||
# settings = ['base']
|
||||
|
||||
# attrs_map = {
|
||||
@ -79,7 +79,7 @@ class LDAPSourceForm(forms.ModelForm):
|
||||
# class CreateUsersSettings(SettingsForm):
|
||||
# """Create users settings"""
|
||||
|
||||
# namespace = 'supervisr.mod.auth.ldap'
|
||||
# namespace = 'passbook.ldap'
|
||||
# settings = ['create_base']
|
||||
|
||||
# attrs_map = {
|
||||
|
@ -129,7 +129,7 @@ class LDAPConnector:
|
||||
# Create the user data.
|
||||
field_map = {
|
||||
'username': '%(' + USERNAME_FIELD + ')s',
|
||||
'first_name': '%(givenName)s %(sn)s',
|
||||
'name': '%(givenName)s %(sn)s',
|
||||
'email': '%(mail)s',
|
||||
}
|
||||
user_fields = {}
|
||||
@ -224,9 +224,9 @@ class LDAPConnector:
|
||||
'cn': str(username),
|
||||
'description': str('t=' + time()),
|
||||
'sAMAccountName': str(username_trunk),
|
||||
'givenName': str(user.first_name),
|
||||
'givenName': str(user.name),
|
||||
'displayName': str(user.username),
|
||||
'name': str(user.first_name),
|
||||
'name': str(user.name),
|
||||
'mail': str(user.email),
|
||||
'userPrincipalName': str(username + '@' + self._source.domain),
|
||||
'objectClass': ['top', 'person', 'organizationalPerson', 'user'],
|
||||
|
@ -29,7 +29,7 @@ class LDAPSource(Source):
|
||||
form = 'passbook.ldap.forms.LDAPSourceForm'
|
||||
|
||||
@property
|
||||
def get_url(self):
|
||||
def get_login_button(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
class Meta:
|
||||
@ -57,7 +57,7 @@ class LDAPSource(Source):
|
||||
|
||||
|
||||
# class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel):
|
||||
# """Model to map an LDAP Group to a supervisr group"""
|
||||
# """Model to map an LDAP Group to a passbook group"""
|
||||
|
||||
# ldap_dn = models.TextField()
|
||||
# group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# from django.conf.urls import url
|
||||
|
||||
# from supervisr.mod.auth.ldap import views
|
||||
# from passbook.mod.auth.ldap import views
|
||||
|
||||
# urlpatterns = [
|
||||
# url(r'^settings/$', views.admin_settings, name='admin_settings'),
|
||||
|
@ -1,4 +1,4 @@
|
||||
# """Supervisr Mod LDAP Views"""
|
||||
# """passbook LDAP Views"""
|
||||
|
||||
|
||||
# from django.contrib import messages
|
||||
@ -8,7 +8,7 @@
|
||||
# from django.urls import reverse
|
||||
# from django.utils.translation import ugettext as _
|
||||
|
||||
# from supervisr.mod.auth.ldap.forms import (AuthenticationBackendSettings,
|
||||
# from passbook.ldap.forms import (AuthenticationBackendSettings,
|
||||
# ConnectionSettings,
|
||||
# CreateUsersSettings,
|
||||
# GeneralSettingsForm)
|
||||
@ -34,5 +34,5 @@
|
||||
# if form.is_valid():
|
||||
# update_count += form.save()
|
||||
# messages.success(request, _('Successfully updated %d settings.' % update_count))
|
||||
# return redirect(reverse('supervisr_mod_auth_ldap:admin_settings'))
|
||||
# return redirect(reverse('passbook_ldap:admin_settings'))
|
||||
# return render(request, 'ldap/settings.html', render_data)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user