Compare commits

...

28 Commits

Author SHA1 Message Date
e94ef34d8f bump version: 0.0.8-alpha -> 0.0.9-alpha 2019-02-26 12:35:28 +01:00
49e945307a Re-enable OTP Disable View 2019-02-26 12:35:24 +01:00
edfe0e5450 fix broken Docker build and helm package 2019-02-26 12:34:51 +01:00
06b65a7882 add unittests, woo 2019-02-26 10:57:05 +01:00
ff9bc8aa70 Automatically create PasswordFactor on initial setup closes #16 2019-02-26 09:54:51 +01:00
28da67abe6 Improve partially broken Delete Views, show success message on deletion 2019-02-26 09:49:42 +01:00
39d9fe9bf0 add passbook.pretend to use passbook in applications which don't support generic OAuth 2019-02-26 09:10:37 +01:00
750117b0fd Cleanup templates, handle OAuth Provider without application better 2019-02-26 09:09:19 +01:00
983462f80d user/ -> _/user/ to prevent duplicate URLs 2019-02-26 09:08:49 +01:00
4ae31d409b directly use paths instead of including oauth2_provider's 2019-02-26 09:08:22 +01:00
98b414f3e2 add SignUp Confirmation (required by default, can be disabled in invitations) closes #6 2019-02-25 21:03:24 +01:00
a0d42092e3 add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7 2019-02-25 20:46:23 +01:00
f2569b6424 improve placeholder on login template 2019-02-25 19:43:33 +01:00
9d344d887c add more information to administrator Overview 2019-02-25 17:52:51 +01:00
7e9154a0ea bump version: 0.0.7-alpha -> 0.0.8-alpha 2019-02-25 17:39:39 +01:00
e0ef061771 fix pylint errors.... 2019-02-25 17:32:52 +01:00
b8694a7ade fix bandit error (SHA1 has to be used) 2019-02-25 17:23:42 +01:00
10d6a30f2c add experimental HaveIBeenPwned Password Policy 2019-02-25 17:21:56 +01:00
8c94aef6d0 add stub test so coverage doesn't crash 2019-02-25 17:21:06 +01:00
19bd3bfffb fix allauth imports 2019-02-25 17:20:53 +01:00
8611ac624c Make links on admin overview site actually useful 2019-02-25 17:11:52 +01:00
fa93b59a8c switch to toast notifications everywhere 2019-02-25 16:41:53 +01:00
8b66b40f0d move forgot password to PasswordFactor 2019-02-25 16:41:33 +01:00
c2756f15fc Correctly display action on Create/Update templates 2019-02-25 16:40:46 +01:00
408e205c5f add signal for password change, add field for password policies 2019-02-25 15:41:36 +01:00
5f3ab49535 fix bug when Empty username is given to LoginAttempt.attempt 2019-02-25 14:10:29 +01:00
33431ae013 improve OAuth Source Setup process, fix login template, closes #3 2019-02-25 14:10:10 +01:00
b40ac6dc5d more Icons cause everyone loves icons 2019-02-25 13:31:11 +01:00
93 changed files with 1383 additions and 631 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.0.7-alpha
current_version = 0.0.9-alpha
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -52,9 +52,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.0.9-alpha
stage: build
only:
- tags
@ -65,7 +65,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/.*$/

View File

@ -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)

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -9,33 +9,35 @@
{% 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' %}" 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>
</div>
{% endblock %}

View File

@ -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 %}

View File

@ -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 }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>

View File

@ -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' %}" 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 %}

View File

@ -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,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 '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' %}">
<span class="pficon pficon-ok"></span>{{ provider_count }}
</a>
</span>
</p>
</div>
</div>
@ -43,11 +64,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 +83,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 +102,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 +121,56 @@
<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="#">
<span class="pficon pficon-ok"></span>{{ worker_count }}
</a>
</span>
</p>
</div>
</div>

View File

@ -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 }}">{{ 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>
</div>
{% endblock %}

View File

@ -10,45 +10,49 @@
{% 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 }}">{{ name }}</a></li>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for provider in object_list %}
<tr>
<td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -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">
@ -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>

View File

@ -5,34 +5,38 @@
{% 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 '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>
<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 %}

View File

@ -4,4 +4,8 @@
{% block above_form %}
<h1>{% trans 'Create' %}</h1>
{% endblock %}
{% endblock %}
{% block action %}
{% trans 'Create' %}
{% endblock %}

View File

@ -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>

View File

@ -4,4 +4,8 @@
{% block above_form %}
<h1>{% trans 'Update' %}</h1>
{% endblock %}
{% endblock %}
{% block action %}
{% trans 'Update' %}
{% endblock %}

View File

@ -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

View File

@ -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 _
@ -45,5 +46,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)

View File

@ -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
@ -39,19 +40,18 @@ class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
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)

View File

@ -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
@ -42,4 +43,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)

View File

@ -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,6 @@ 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))
return super().get_context_data(**kwargs)

View File

@ -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)"""

View File

@ -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
@ -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)

View File

@ -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
@ -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)

View File

@ -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')

View File

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

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -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]

View File

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

View File

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

View File

@ -1,15 +1,18 @@
"""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
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.lib.config import CONFIG
LOGGER = getLogger(__name__)
@ -21,6 +24,20 @@ 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))
# TODO: Send email to user
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')

View File

@ -111,7 +111,7 @@ 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()
self.cleanup()
return redirect(reverse('passbook_core:auth-denied'))
def _user_passed(self):
@ -121,13 +121,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):
return redirect(next_param)
return redirect(reverse('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, ]

View File

@ -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"""

View File

@ -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(),

View File

@ -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)",

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
"""passbook core models"""
import re
from datetime import timedelta
from logging import getLogger
from random import SystemRandom
from time import sleep
@ -9,13 +10,20 @@ 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"""
@ -38,6 +46,12 @@ class User(AbstractUser):
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):
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"""
@ -87,6 +101,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 +109,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
@ -155,10 +177,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
@ -365,6 +392,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 +406,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')

View File

@ -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

View File

@ -9,3 +9,4 @@ from django.core.signals import Signal
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'])

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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' %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -1,51 +1,79 @@
{% extends 'login/base.html' %}
{% extends 'base/skeleton.html' %}
{% load static %}
{% load 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 %}

View File

@ -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 %}

View File

@ -25,9 +25,6 @@
<label class="checkbox-label">
{{ field }} {{ field.label }}
</label>
{% if show_password_forget_notice %}
<a href="#">{% trans 'Forgot password?' %}</a>
{% endif %}
{% else %}
<label class="col-sm-2 sr-only" {% if field.field.required %}class="required"{% endif %} for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }}

View File

@ -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 %}

View File

View File

@ -0,0 +1,147 @@
"""passbook Core Account Test"""
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 = {
'first_name': 'Test',
'last_name': 'User',
'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='test123')
def test_sign_up_view(self):
"""Test account.sign_up view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-sign-up'))
self.assertEqual(response.status_code, 200)
def test_login_view(self):
"""Test account.login view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-login'))
self.assertEqual(response.status_code, 200)
# test login with post
form = LoginForm(self.login_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-login'), data=form.cleaned_data)
self.assertEqual(response.status_code, 302)
def test_logout_view(self):
"""Test account.logout view"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
self.assertEqual(response.status_code, 302)
def test_sign_up_view_auth(self):
"""Test account.sign_up view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
self.assertEqual(response.status_code, 302)
def test_login_view_auth(self):
"""Test account.login view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-login'))
self.assertEqual(response.status_code, 302)
def test_login_view_post(self):
"""Test account.login view POST (Anonymous)"""
login_response = self.client.post(reverse('passbook_core:auth-login'), data=self.login_data)
self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response.url, reverse('passbook_core:auth-process'))
def test_sign_up_view_post(self):
"""Test account.sign_up view POST (Anonymous)"""
form = SignUpForm(self.sign_up_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-sign-up'), data=form.cleaned_data)
self.assertEqual(response.status_code, 302)
# def test_reset_password_init_view(self):
# """Test account.reset_password_init view POST (Anonymous)"""
# form = SignUpForm(self.sign_up_data)
# self.assertTrue(form.is_valid())
# res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=form.cleaned_data)
# self.assertEqual(res.status_code, 302)
# res = test_request(accounts.PasswordResetInitView.as_view())
# self.assertEqual(res.status_code, 200)
# def test_resend_confirmation(self):
# """Test AccountController.resend_confirmation"""
# form = SignUpForm(self.sign_up_data)
# self.assertTrue(form.is_valid())
# res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=form.cleaned_data)
# self.assertEqual(res.status_code, 302)
# user = User.objects.get(email=self.sign_up_data['email'])
# # Invalidate all other links for this user
# old_acs = AccountConfirmation.objects.filter(
# user=user)
# for old_ac in old_acs:
# old_ac.confirmed = True
# old_ac.save()
# # Create Account Confirmation UUID
# new_ac = AccountConfirmation.objects.create(user=user)
# self.assertFalse(new_ac.is_expired)
# on_user_confirm_resend.send(
# sender=None,
# user=user,
# request=None)
# def test_reset_passowrd(self):
# """Test reset password POST"""
# # Signup user first
# sign_up_form = SignUpForm(self.sign_up_data)
# self.assertTrue(sign_up_form.is_valid())
# sign_up_res = test_request(accounts.SignUpView.as_view(),
# method='POST',
# req_kwargs=sign_up_form.cleaned_data)
# self.assertEqual(sign_up_res.status_code, 302)
# user = User.objects.get(email=self.sign_up_data['email'])
# # Invalidate all other links for this user
# old_acs = AccountConfirmation.objects.filter(
# user=user)
# for old_ac in old_acs:
# old_ac.confirmed = True
# old_ac.save()
# # Create Account Confirmation UUID
# new_ac = AccountConfirmation.objects.create(user=user)
# self.assertFalse(new_ac.is_expired)
# uuid = AccountConfirmation.objects.filter(user=user).first().pk
# reset_res = test_request(accounts.PasswordResetFinishView.as_view(),
# method='POST',
# user=user,
# url_kwargs={'uuid': uuid},
# req_kwargs=self.change_data)
# self.assertEqual(reset_res.status_code, 302)
# self.assertEqual(reset_res.url, reverse('common-index'))

View File

@ -0,0 +1,21 @@
"""passbook user view tests"""
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='test123')
self.client.force_login(self.user)
def test_overview(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:overview')).status_code, 200)

View File

@ -0,0 +1,43 @@
"""passbook user view tests"""
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='test123')
self.client.force_login(self.user)
def test_user_settings(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:user-settings')).status_code, 200)
def test_user_delete(self):
"""Test UserDeleteView"""
self.assertEqual(self.client.post(reverse('passbook_core:user-delete')).status_code, 302)
self.assertEqual(User.objects.filter(username='unittest user').exists(), False)
self.setUp()
def test_user_change_password(self):
"""Test UserChangePasswordView"""
form_data = {
'password': 'test2',
'password_repeat': 'test2'
}
form = PasswordChangeForm(data=form_data)
self.assertTrue(form.is_valid())
self.assertEqual(self.client.get(
reverse('passbook_core:user-change-password')).status_code, 200)
self.assertEqual(self.client.post(
reverse('passbook_core:user-change-password'), data=form_data).status_code, 302)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('test2'))

View File

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

View File

@ -19,13 +19,17 @@ core_urls = [
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/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'),

View File

@ -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."""

View File

@ -3,17 +3,17 @@ 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.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.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.lib.config import CONFIG
@ -41,15 +41,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)
@ -134,6 +139,15 @@ class SignUpView(UserPassesTestMixin, FormView):
def form_valid(self, form: SignUpForm) -> HttpResponse:
"""Create user"""
self._user = SignUpView.create_user(form.cleaned_data, self.request)
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))
# TODO: Send E-Mail to user
self._user.is_active = False
self._user.save()
self.consume_invitation()
messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s",
@ -180,8 +194,35 @@ class SignUpView(UserPassesTestMixin, FormView):
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
class SignUpConfirmView(View):
"""Confirm registration from Nonce"""
def get(self, request, nonce):
"""Verify UUID and activate user"""
nonce = get_object_or_404(Nonce, uuid=nonce)
nonce.user.is_active = True
nonce.user.save()
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _('Successfully confirmed registration.'))
return redirect('passbook_core:overview')
class PasswordResetView(View):
"""Temporarily authenticate User and allow them to reset their password"""
def get(self, request, nonce):
"""Authenticate user with nonce and redirect to password change view"""
# 3. (Optional) Trap user in password change view
nonce = get_object_or_404(Nonce, uuid=nonce)
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _(('Temporarily authenticated with Nonce, '
'please change your password')))
return redirect('passbook_core:user-change-password')

View File

@ -11,6 +11,7 @@ from passbook.lib.config import CONFIG
class UserSettingsView(UpdateView):
"""Update User settings"""
template_name = 'user/settings.html'
form_class = UserDetailForm

View File

@ -0,0 +1,2 @@
"""passbook hibp_policy"""
__version__ = '0.0.7-alpha'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
"""passbook HIBP Models"""
from hashlib import sha1
from django.db import models
from django.utils.translation import gettext as _
from requests import get
from passbook.core.models import Policy, User
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)
if final_count > self.allowed_count:
return False
return True
class Meta:
verbose_name = _('have i been pwned Policy')
verbose_name_plural = _('have i been pwned Policies')

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -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:

View File

@ -1,2 +1,2 @@
"""passbook lib"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

7
passbook/lib/utils/ui.py Normal file
View File

@ -0,0 +1,7 @@
"""passbook UI utils"""
def human_list(_list) -> str:
"""Convert a list of items into 'a, b or c'"""
last_item = _list.pop()
result = ', '.join(_list)
return '%s or %s' % (result, last_item)

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -26,9 +26,17 @@ class OAuthSource(Source):
return True
@property
def get_url(self):
return reverse_lazy('passbook_oauth_client:oauth-client-login',
kwargs={'source_slug': self.slug})
def get_login_button(self):
url = reverse_lazy('passbook_oauth_client:oauth-client-login',
kwargs={'source_slug': self.slug})
if self.provider_type == 'github':
return url, 'github-logo', _('GitHub')
return url, 'generic', _('Generic')
@property
def additional_info(self):
return "Callback URL: '%s'" % reverse_lazy('passbook_oauth_client:oauth-client-callback',
kwargs={'source_slug': self.slug})
class Meta:

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -22,6 +22,8 @@ OAUTH2_PROVIDER = {
'SCOPES': {
'openid:userinfo': 'Access OpenID Userinfo',
# 'write': 'Write scope',
# 'groups': 'Access to your groups'
# 'groups': 'Access to your groups',
'user:email': 'GitHub Compatibility: User E-Mail',
'read:org': 'GitHub Compatibility: User Groups',
}
}

View File

@ -11,7 +11,6 @@
<header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1>
</header>
{% include 'partials/messages.html' %}
<form method="POST">
{% csrf_token %}
{% if not error %}

View File

@ -1,6 +1,7 @@
"""passbook oauth_provider urls"""
from django.urls import include, path
from django.urls import path
from oauth2_provider import views
from passbook.oauth_provider.views import oauth2
@ -13,5 +14,8 @@ urlpatterns = [
path('authorize/permission_denied/', oauth2.OAuthPermissionDenied.as_view(),
name='oauth2-permission-denied'),
# OAuth API
path('', include('oauth2_provider.urls', namespace='oauth2_provider')),
path("authorize/", views.AuthorizationView.as_view(), name="authorize"),
path("token/", views.TokenView.as_view(), name="token"),
path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"),
path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"),
]

View File

@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
from oauth2_provider.views.base import AuthorizationView
from passbook.audit.models import AuditEntry
from passbook.core.models import Application
from passbook.core.views.access import AccessMixin
from passbook.core.views.utils import LoadingView, PermissionDeniedView
from passbook.oauth_provider.models import OAuth2Provider
@ -38,14 +39,17 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView):
# Get client_id to get provider, so we can update skip_authorization field
client_id = request.GET.get('client_id')
provider = get_object_or_404(OAuth2Provider, client_id=client_id)
application = self.provider_to_application(provider)
try:
application = self.provider_to_application(provider)
except Application.DoesNotExist:
return redirect('passbook_oauth_provider:oauth2-permission-denied')
# Update field here so oauth-toolkit does work for us
provider.skip_authorization = application.skip_authorization
provider.save()
self._application = application
# Check permissions
if not self.user_has_access(self._application, request.user):
return redirect(reverse('passbook_oauth_provider:oauth2-permission-denied'))
return redirect('passbook_oauth_provider:oauth2-permission-denied')
actual_response = super().dispatch(request, *args, **kwargs)
if actual_response.status_code == 400:
LOGGER.debug(request.GET.get('redirect_uri'))

View File

@ -1,2 +1,2 @@
"""passbook otp Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -6,11 +6,11 @@ from logging import getLogger
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import FormView, TemplateView
from django.views.generic import FormView, TemplateView, DeleteView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice
from qrcode import make
@ -41,28 +41,27 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
kwargs['state'] = totp_devices.exists() and static.exists()
return kwargs
class DisableView(LoginRequiredMixin, TemplateView):
class DisableView(LoginRequiredMixin, View):
"""Disable TOTP for user"""
# TODO: Use Django DeleteView with custom delete?
# def
# # Delete all the devices for user
# static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
# static_tokens = StaticToken.objects.filter(device=static).order_by('token')
# totp = TOTPDevice.objects.filter(user=request.user, confirmed=True)
# static.delete()
# totp.delete()
# for token in static_tokens:
# token.delete()
# messages.success(request, 'Successfully disabled TOTP')
# # Create event with email notification
# # Event.create(
# # user=request.user,
# # message=_('You disabled TOTP.'),
# # current=True,
# # request=request,
# # send_notification=True)
# return redirect(reverse('passbook_core:overview'))
def get(self, request, *args, **kwargs):
"""Delete all the devices for user"""
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
totp = TOTPDevice.objects.filter(user=request.user, confirmed=True)
static.delete()
totp.delete()
for token in static_tokens:
token.delete()
messages.success(request, 'Successfully disabled OTP')
# Create event with email notification
# Event.create(
# user=request.user,
# message=_('You disabled TOTP.'),
# current=True,
# request=request,
# send_notification=True)
return redirect(reverse('passbook_otp:otp-user-settings'))
class EnableView(LoginRequiredMixin, FormView):
"""View to set up OTP"""

View File

11
passbook/pretend/apps.py Normal file
View File

@ -0,0 +1,11 @@
"""passbook pretend config"""
from django.apps import AppConfig
class PassbookPretendConfig(AppConfig):
"""passbook pretend config"""
name = 'passbook.pretend'
label = 'passbook_pretend'
verbose_name = 'passbook Pretender'
mountpoint = ''

16
passbook/pretend/urls.py Normal file
View File

@ -0,0 +1,16 @@
"""passbook pretend urls"""
from django.urls import include, path
from oauth2_provider.views import TokenView
from passbook.oauth_provider.views.oauth2 import PassbookAuthorizationView
from passbook.pretend.views.github import GitHubUserView
github_urlpatterns = [
path('login/oauth/authorize', PassbookAuthorizationView.as_view(), name='github-authorize'),
path('login/oauth/access_token', TokenView.as_view(), name='github-access-token'),
path('user', GitHubUserView.as_view(), name='github-user'),
]
urlpatterns = [
path('', include(github_urlpatterns))
]

View File

View File

@ -0,0 +1,55 @@
"""passbook pretend GitHub Views"""
from django.http import JsonResponse
from django.views import View
class GitHubUserView(View):
"""Emulate GitHub's /user API Endpoint"""
def get(self, request):
"""Emulate GitHub's /user API Endpoint"""
return JsonResponse({
"login": request.user.username,
"id": request.user.pk,
"node_id": "",
"avatar_url": "",
"gravatar_id": "",
"url": "",
"html_url": "",
"followers_url": "",
"following_url": "",
"gists_url": "",
"starred_url": "",
"subscriptions_url": "",
"organizations_url": "",
"repos_url": "",
"events_url": "",
"received_events_url": "",
"type": "User",
"site_admin": False,
"name": "%s %s" % (request.user.first_name, request.user.last_name),
"company": "",
"blog": "",
"location": "",
"email": request.user.email,
"hireable": False,
"bio": "",
"public_repos": 0,
"public_gists": 0,
"followers": 0,
"following": 0,
"created_at": request.user.date_joined,
"updated_at": request.user.date_joined,
"private_gists": 0,
"total_private_repos": 0,
"owned_private_repos": 0,
"disk_usage": 0,
"collaborators": 0,
"two_factor_authentication": True,
"plan": {
"name": "None",
"space": 0,
"private_repos": 0,
"collaborators": 0
}
})

View File

@ -1,2 +1,2 @@
"""passbook saml_idp Header"""
__version__ = '0.0.7-alpha'
__version__ = '0.0.9-alpha'

View File

@ -11,7 +11,6 @@
<header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1>
</header>
{% include 'partials/messages.html' %}
<form method="POST" action="{{ acs_url }}">>
{% csrf_token %}
<input type="hidden" name="ACSUrl" value="{{ acs_url }}">