Compare commits

...

22 Commits

Author SHA1 Message Date
e6376a05f7 bump version: 0.1.12-beta -> 0.1.13-beta 2019-03-11 11:31:12 +01:00
1f45aff7ad prepare 0.1.13 2019-03-11 11:31:06 +01:00
e1f1f617b6 fix UserChangePasswordView not requiring Login 2019-03-11 11:25:59 +01:00
2690675dca allow custom email server for helm installs 2019-03-11 11:03:25 +01:00
7529b51358 Fix DoesNotExist error when running PolicyEngine against None user 2019-03-11 10:52:50 +01:00
c394066d99 bump version: 0.1.11-beta -> 0.1.12-beta 2019-03-11 09:51:00 +01:00
9c585032ef prepare 0.1.12-beta 2019-03-11 09:50:57 +01:00
d408031304 fix OAuth Authorization View not requiring authentication 2019-03-11 09:48:36 +01:00
c47bc11ec0 disable automatic k8s deployment for now 2019-03-11 09:47:06 +01:00
1deb094afe install updated helm release from local folder 2019-03-10 21:47:22 +01:00
501fed1922 rewrite PasswordFactor to use backends setting instead of trying all backends 2019-03-10 21:47:08 +01:00
ad8125ac1c bump version: 0.1.10-beta -> 0.1.11-beta 2019-03-10 19:56:30 +01:00
b42a551fb2 prepare 0.1.11 2019-03-10 19:56:27 +01:00
3256be23df Merge branch '23-groups' into 'master'
Resolve "Group Management"

Closes #23

See merge request BeryJu.org/passbook!9
2019-03-10 18:49:01 +00:00
f7c0c0146a add LDAP Group Membership Policy 2019-03-10 19:45:16 +01:00
e4baf8c21e Add Group Member policy 2019-03-10 19:32:18 +01:00
364f040b36 always use FilteredSelectMultiple for many-to-many fields 2019-03-10 18:34:09 +01:00
2b8c2b2346 use Django's Admin FilteredSelectMultiple for Group Membership 2019-03-10 18:06:06 +01:00
5f861189e4 Merge branch 'master' into 23-groups
# Conflicts:
#	passbook/admin/templates/administration/base.html
2019-03-10 17:13:29 +01:00
5e11b6687e automatically deploy after release 2019-03-10 17:08:33 +01:00
c4b429825d fix helm labels being on deployments and not pods 2019-03-10 16:39:41 +01:00
2d7e8f1b50 add group administration 2019-03-08 15:49:45 +01:00
52 changed files with 613 additions and 206 deletions

View File

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

View File

@ -8,6 +8,7 @@ stages:
- test - test
- build - build
- docs - docs
- deploy
image: python:3.6 image: python:3.6
services: services:
- postgres:latest - postgres:latest
@ -53,7 +54,7 @@ package-docker:
before_script: before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.10-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.13-beta
stage: build stage: build
only: only:
- tags - tags
@ -113,3 +114,16 @@ package-debian:
# - mkdocs build # - mkdocs build
# - 'rsync -avh --delete web/* "beryjuorg@ory1-web-prod-1.ory1.beryju.org:passbook.beryju.org/"' # - 'rsync -avh --delete web/* "beryjuorg@ory1-web-prod-1.ory1.beryju.org:passbook.beryju.org/"'
# - 'rsync -avh --delete site/* "beryjuorg@ory1-web-prod-1.ory1.beryju.org:passbook.beryju.org/docs/"' # - 'rsync -avh --delete site/* "beryjuorg@ory1-web-prod-1.ory1.beryju.org:passbook.beryju.org/docs/"'
# deploy:
# environment:
# name: production
# url: https://passbook-prod.default.k8s.beryju.org/
# stage: deploy
# only:
# - tags
# - /^version/.*$/
# script:
# - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
# - helm init
# - helm upgrade passbook-prod helm/passbook --devel

32
debian/changelog vendored
View File

@ -1,3 +1,35 @@
passbook (0.1.13) stable; urgency=medium
* bump version: 0.1.11-beta -> 0.1.12-beta
* Fix DoesNotExist error when running PolicyEngine against None user
* allow custom email server for helm installs
* fix UserChangePasswordView not requiring Login
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 10:28:36 +0000
passbook (0.1.12) stable; urgency=medium
* bump version: 0.1.10-beta -> 0.1.11-beta
* rewrite PasswordFactor to use backends setting instead of trying all backends
* install updated helm release from local folder
* disable automatic k8s deployment for now
* fix OAuth Authorization View not requiring authentication
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 08:50:29 +0000
passbook (0.1.11) stable; urgency=medium
* add group administration
* bump version: 0.1.9-beta -> 0.1.10-beta
* fix helm labels being on deployments and not pods
* automatically deploy after release
* use Django's Admin FilteredSelectMultiple for Group Membership
* always use FilteredSelectMultiple for many-to-many fields
* Add Group Member policy
* add LDAP Group Membership Policy
-- Jens Langhammer <jens.langhammer@beryju.org> Sun, 10 Mar 2019 18:55:31 +0000
passbook (0.1.10) stable; urgency=high passbook (0.1.10) stable; urgency=high
* bump version: 0.1.7-beta -> 0.1.8-beta * bump version: 0.1.7-beta -> 0.1.8-beta

View File

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

View File

@ -22,7 +22,7 @@ data:
host: 127.0.0.1 host: 127.0.0.1
port: 514 port: 514
email: email:
host: localhost host: {{ .Values.config.email.host }}
port: 25 port: 25
user: '' user: ''
password: '' password: ''

View File

@ -7,7 +7,6 @@ metadata:
helm.sh/chart: {{ include "passbook.chart" . }} helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/managed-by: {{ .Release.Service }}
passbook.io/component: web
spec: spec:
replicas: {{ .Values.replicaCount }} replicas: {{ .Values.replicaCount }}
selector: selector:
@ -19,6 +18,7 @@ spec:
labels: labels:
app.kubernetes.io/name: {{ include "passbook.name" . }} app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
spec: spec:
volumes: volumes:
- name: config-volume - name: config-volume

View File

@ -7,7 +7,6 @@ metadata:
helm.sh/chart: {{ include "passbook.chart" . }} helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/managed-by: {{ .Release.Service }}
passbook.io/component: worker
spec: spec:
replicas: {{ .Values.replicaCount }} replicas: {{ .Values.replicaCount }}
selector: selector:
@ -19,6 +18,7 @@ spec:
labels: labels:
app.kubernetes.io/name: {{ include "passbook.name" . }} app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: worker
spec: spec:
volumes: volumes:
- name: config-volume - name: config-volume

View File

@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
tag: 0.1.10-beta tag: 0.1.13-beta
nameOverride: "" nameOverride: ""
@ -14,6 +14,8 @@ config:
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o # secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting # Enable error reporting
error_reporting: true error_reporting: true
email:
host: localhost
postgresql: postgresql:
postgresqlDatabase: passbook postgresqlDatabase: passbook

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -39,6 +39,9 @@
<li class="{% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}"> <li class="{% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">{% trans 'Users' %}</a> <a href="{% url 'passbook_admin:users' %}">{% trans 'Users' %}</a>
</li> </li>
<li class="{% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
<a href="{% url 'passbook_admin:groups' %}">{% trans 'Groups' %}</a>
</li>
<li class="{% is_active 'passbook_admin:audit-log' %}"> <li class="{% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">{% trans 'Audit Log' %}</a> <a href="{% url 'passbook_admin:audit-log' %}">{% trans 'Audit Log' %}</a>
</li> </li>

View File

@ -0,0 +1,45 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="pficon-users"></span> {% trans "Groups" %}</h1>
<span>{% trans "Group users together and give them permissions based on the membership." %}</span>
<hr>
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %}
</a>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Parent' %}</th>
<th>{% trans 'Members' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for group in object_list %}
<tr>
<td>{{ group.name }}</td>
<td>{{ group.parent }}</td>
<td>{{ group.user_set.all|length }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:group-update' pk=group.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:group-delete' pk=group.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,83 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load static %}
{% load utils %}
{% block head %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-treeview.min.css'%}">
{% endblock %}
{% block scripts %}
{{ block.super }}
<script src="{% static 'js/bootstrap-treeview.min.js' %}"></script>
<script>
var cleanupData = function (obj) {
return {
text: obj.name,
href: '?group=' + obj.uuid,
nodes: obj.children.map(cleanupData),
};
}
$(function() {
var apiUrl = "{% url 'passbook_admin:group-list' %}?format=json";
$.ajax({
url: apiUrl,
}).done(function(data) {
$('#treeview1').treeview({
collapseIcon: "fa fa-angle-down",
data: data.map(cleanupData),
expandIcon: "fa fa-angle-right",
nodeIcon: "fa pficon-users",
showBorder: true,
enableLinks: true,
onNodeSelected: function (event, node) {
window.location.href = node.href;
}
});
});
});
</script>
{% endblock %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="col-md-3">
<div id="treeview1" class="treeview">
</div>
</div>
<div class="col-md-9">
<h1>{% trans "Invitations" %}</h1>
<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

@ -2,10 +2,20 @@
{% load i18n %} {% load i18n %}
{% load utils %} {% load utils %}
{% load static %}
{% block head %} {% block head %}
{{ block.super }} {{ block.super }}
{{ form.media.css }} {{ form.media.css }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/jquery.init.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/actions.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/urlify.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/prepopulate.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/SelectBox.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/SelectFilter2.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -67,6 +67,11 @@ urlpatterns = [
users.UserDeleteView.as_view(), name='user-delete'), users.UserDeleteView.as_view(), name='user-delete'),
path('users/<int:pk>/reset/', path('users/<int:pk>/reset/',
users.UserPasswordResetView.as_view(), name='user-password-reset'), users.UserPasswordResetView.as_view(), name='user-password-reset'),
# Groups
path('group/', groups.GroupListView.as_view(), name='group'),
path('group/create/', groups.GroupCreateView.as_view(), name='group-create'),
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'),
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
# Audit Log # Audit Log
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
# Groups # Groups

View File

@ -1,12 +1,57 @@
"""passbook Group administration""" """passbook Group administration"""
from django.views.generic import ListView 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 _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group from passbook.core.models import Group
class GroupListView(AdminRequiredMixin, ListView): class GroupListView(AdminRequiredMixin, ListView):
"""Show list of all invitations""" """Show list of all groups"""
model = Group model = Group
template_name = 'administration/groups/list.html' ordering = 'name'
template_name = 'administration/group/list.html'
class GroupCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Group"""
form_class = GroupForm
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:groups')
success_message = _('Successfully created Group')
def get_context_data(self, **kwargs):
kwargs['type'] = 'Group'
return super().get_context_data(**kwargs)
class GroupUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update group"""
model = Group
form_class = GroupForm
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:groups')
success_message = _('Successfully updated Group')
class GroupDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete group"""
model = Group
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:groups')
success_message = _('Successfully deleted Group')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,8 +1,10 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from inspect import Signature
from logging import getLogger from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate from django.contrib.auth import _clean_credentials
from django.contrib.auth.signals import user_login_failed
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
@ -15,10 +17,40 @@ from passbook.core.forms.authentication import PasswordFactorForm
from passbook.core.models import Nonce from passbook.core.models import Nonce
from passbook.core.tasks import send_email from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
def authenticate(request, backends, **credentials):
"""If the given credentials are valid, return a User object.
Customized version of django's authenticate, which accepts a list of backends"""
for backend_path in backends:
backend = path_to_class(backend_path)()
try:
signature = Signature.from_callable(backend.authenticate)
signature.bind(request, **credentials)
except TypeError:
LOGGER.debug("Backend %s doesn't accept our arguments", backend)
# This backend doesn't accept these credentials as arguments. Try the next one.
continue
LOGGER.debug('Attempting authentication with %s...', backend)
try:
user = backend.authenticate(request, **credentials)
except PermissionDenied:
LOGGER.debug('Backend %r threw PermissionDenied', backend)
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
if user is None:
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(sender=__name__, credentials=_clean_credentials(
credentials), request=request)
class PasswordFactor(FormView, AuthenticationFactor): class PasswordFactor(FormView, AuthenticationFactor):
"""Authentication factor which authenticates against django's AuthBackend""" """Authentication factor which authenticates against django's AuthBackend"""
@ -57,7 +89,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
for uid_field in uid_fields: for uid_field in uid_fields:
kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field)
try: try:
user = authenticate(self.request, **kwargs) user = authenticate(self.request, self.authenticator.current_factor.backends, **kwargs)
if user: if user:
# User instance returned from authenticate() has .backend property set # User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user self.authenticator.pending_user = user

View File

@ -1,5 +1,6 @@
"""passbook Core Application forms""" """passbook Core Application forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider from passbook.core.models import Application, Provider
@ -20,6 +21,7 @@ class ApplicationForm(forms.ModelForm):
'name': forms.TextInput(), 'name': forms.TextInput(),
'launch_url': forms.TextInput(), 'launch_url': forms.TextInput(),
'icon_url': forms.TextInput(), 'icon_url': forms.TextInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }
labels = { labels = {
'launch_url': _('Launch URL'), 'launch_url': _('Launch URL'),

View File

@ -1,11 +1,20 @@
"""passbook administration forms""" """passbook administration forms"""
from django import forms from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.core.models import DummyFactor, PasswordFactor from passbook.core.models import DummyFactor, PasswordFactor
from passbook.lib.fields import DynamicArrayField from passbook.lib.utils.reflection import path_to_class
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled'] GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
def get_authentication_backends():
"""Return all available authentication backends as tuple set"""
for backend in settings.AUTHENTICATION_BACKENDS:
klass = path_to_class(backend)
yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__))
class PasswordFactorForm(forms.ModelForm): class PasswordFactorForm(forms.ModelForm):
"""Form to create/edit Password Factors""" """Form to create/edit Password Factors"""
@ -16,9 +25,9 @@ class PasswordFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
} 'policies': FilteredSelectMultiple(_('policies'), False),
field_classes = { 'backends': FilteredSelectMultiple(_('backends'), False,
'backends': DynamicArrayField choices=get_authentication_backends())
} }
class DummyFactorForm(forms.ModelForm): class DummyFactorForm(forms.ModelForm):
@ -31,4 +40,5 @@ class DummyFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }

View File

@ -0,0 +1,32 @@
"""passbook Core Group forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from passbook.core.models import Group, User
class GroupForm(forms.ModelForm):
"""Group Form"""
members = forms.ModelMultipleChoiceField(
User.objects.all(), required=False, widget=FilteredSelectMultiple('users', False))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.initial['members'] = self.instance.user_set.values_list('pk', flat=True)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
if instance.pk:
instance.user_set.clear()
instance.user_set.add(*self.cleaned_data['members'])
return instance
class Meta:
model = Group
fields = ['name', 'parent', 'members', 'tags']
widgets = {
'name': forms.TextInput(),
}

View File

@ -4,7 +4,8 @@ from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy, from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
PasswordPolicy, WebhookPolicy) GroupMembershipPolicy, PasswordPolicy,
WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ] GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
@ -53,6 +54,17 @@ class DebugPolicyForm(forms.ModelForm):
} }
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + ['group', ]
widgets = {
'name': forms.TextInput(),
}
class PasswordPolicyForm(forms.ModelForm): class PasswordPolicyForm(forms.ModelForm):
"""PasswordPolicy Form""" """PasswordPolicy Form"""

View File

@ -11,7 +11,7 @@ def create_initial_factor(apps, schema_editor):
name='password', name='password',
slug='password', slug='password',
order=0, order=0,
backends=[] backends=['django.contrib.auth.backends.ModelBackend']
) )
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.7 on 2019-03-10 16:15
import django.contrib.postgres.fields.hstore
from django.contrib.postgres.operations import HStoreExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0018_provider_property_mappings'),
]
operations = [
migrations.RemoveField(
model_name='group',
name='extra_data',
),
HStoreExtension(),
migrations.AddField(
model_name='group',
name='tags',
field=django.contrib.postgres.fields.hstore.HStoreField(default=dict),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 2.1.7 on 2019-03-10 18:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0019_auto_20190310_1615'),
]
operations = [
migrations.CreateModel(
name='GroupMembershipPolicy',
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')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Group')),
],
options={
'verbose_name': 'Group Membership Policy',
'verbose_name_plural': 'Group Membership Policies',
},
bases=('passbook_core.policy',),
),
]

View File

@ -8,7 +8,7 @@ from typing import Tuple, Union
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField, HStoreField
from django.db import models from django.db import models
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now from django.utils.timezone import now
@ -31,7 +31,7 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80) name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True, parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children') on_delete=models.SET_NULL, related_name='children')
extra_data = models.TextField(blank=True) tags = HStoreField(default=dict)
def __str__(self): def __str__(self):
return "Group %s" % self.name return "Group %s" % self.name
@ -393,6 +393,21 @@ class DebugPolicy(Policy):
verbose_name = _('Debug Policy') verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies') verbose_name_plural = _('Debug Policies')
class GroupMembershipPolicy(Policy):
"""Policy to check if the user is member in a certain group"""
group = models.ForeignKey('Group', on_delete=models.CASCADE)
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
return self.group.user_set.filter(pk=user.pk).exists()
class Meta:
verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies')
class Invitation(UUIDModel): class Invitation(UUIDModel):
"""Single-use invitation link""" """Single-use invitation link"""

View File

@ -12,6 +12,8 @@ LOGGER = getLogger(__name__)
@CELERY_APP.task() @CELERY_APP.task()
def _policy_engine_task(user_pk, policy_pk, **kwargs): def _policy_engine_task(user_pk, policy_pk, **kwargs):
"""Task wrapper to run policy checking""" """Task wrapper to run policy checking"""
if not user_pk:
raise ValueError()
policy_obj = Policy.objects.filter(pk=policy_pk).select_subclasses().first() policy_obj = Policy.objects.filter(pk=policy_pk).select_subclasses().first()
user_obj = User.objects.get(pk=user_pk) user_obj = User.objects.get(pk=user_pk)
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -73,7 +75,12 @@ class PolicyEngine:
def result(self): def result(self):
"""Get policy-checking result""" """Get policy-checking result"""
messages = [] messages = []
for policy_action, policy_result, policy_message in self._group.get(): try:
# ValueError can be thrown from _policy_engine_task when user is None
group_result = self._group.get()
except ValueError as exc:
return False, str(exc)
for policy_action, policy_result, policy_message in group_result:
passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \ passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
(policy_action == Policy.ACTION_DENY and not policy_result) (policy_action == Policy.ACTION_DENY and not policy_result)
LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing) LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing)

View File

@ -47,8 +47,7 @@ SESSION_COOKIE_NAME = 'passbook_session'
LANGUAGE_COOKIE_NAME = 'passbook_language' LANGUAGE_COOKIE_NAME = 'passbook_language'
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend'
'passbook.oauth_client.backends.AuthorizedServiceBackend'
] ]
# Application definition # Application definition
@ -60,6 +59,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.postgres',
'rest_framework', 'rest_framework',
'drf_yasg', 'drf_yasg',
'raven.contrib.django.raven_compat', 'raven.contrib.django.raven_compat',

View File

@ -21,3 +21,177 @@
.dynamic-array-widget .remove:hover { .dynamic-array-widget .remove:hover {
cursor: pointer; cursor: pointer;
} }
/* Selector */
.selector {
display: flex;
width: 100%;
height: 45vh;
}
.selector .selector-filter {
display: flex;
align-items: center;
}
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input {
width: auto;
min-height: 0;
flex: 1 1;
}
.selector-available, .selector-chosen {
width: auto;
flex: 1 1;
display: flex;
flex-direction: column;
}
.selector select {
width: 100%;
flex: 1 0 auto;
margin-bottom: 5px;
}
.selector ul.selector-chooser {
width: 26px;
height: 52px;
padding: 2px 0;
margin: auto 15px;
border-radius: 20px;
transform: translateY(-10px);
list-style: none;
}
.selector-add, .selector-remove {
width: 20px;
height: 20px;
background-size: 20px auto;
}
.selector-add {
background-position: 0 -120px;
}
.selector-remove {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
align-self: center;
}
.stacked {
flex-direction: column;
max-width: 480px;
}
.stacked > * {
flex: 0 1 auto;
}
.stacked select {
margin-bottom: 0;
}
.stacked .selector-available, .stacked .selector-chosen {
width: auto;
}
.stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto;
transform: none;
}
.stacked .selector-chooser li {
padding: 3px;
}
.stacked .selector-add, .stacked .selector-remove {
background-size: 20px auto;
}
.stacked .selector-add {
background-position: 0 -40px;
}
.stacked .active.selector-add {
background-position: 0 -60px;
}
.stacked .selector-remove {
background-position: 0 0;
}
.stacked .active.selector-remove {
background-position: 0 -20px;
}
.help-tooltip, .selector .help-icon {
display: none;
}
form .form-row p.datetime {
width: 100%;
}
.datetime input {
width: 50%;
max-width: 120px;
}
.datetime span {
font-size: 13px;
}
.datetime .timezonewarning {
display: block;
font-size: 11px;
color: #999;
}
.datetimeshortcuts {
color: #ccc;
}
.inline-group {
overflow: auto;
}
.selector-add, .selector-remove {
width: 16px;
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.3;
}
.active.selector-add, .active.selector-remove {
opacity: 1;
}
.active.selector-add:hover, .active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../admin/img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../admin/img/selector-icons.svg) 0 -64px no-repeat;
}

View File

@ -18,9 +18,12 @@ class TestFactorAuthentication(TestCase):
super().setUp() super().setUp()
self.password = ''.join(SystemRandom().choice( self.password = ''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)) string.ascii_uppercase + string.digits) for _ in range(8))
self.factor, _ = PasswordFactor.objects.get_or_create(name='password', self.factor, _ = PasswordFactor.objects.get_or_create(slug='password', defaults={
slug='password', 'name': 'password',
backends=[]) 'slug': 'password',
'order': 0,
'backends': ['django.contrib.auth.backends.ModelBackend']
})
self.user = User.objects.create_user(username='test', self.user = User.objects.create_user(username='test',
email='test@test.test', email='test@test.test',
password=self.password) password=self.password)

View File

@ -1,6 +1,7 @@
"""passbook core user views""" """passbook core user views"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import logout, update_session_auth_hash from django.contrib.auth import logout, update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
@ -13,7 +14,7 @@ from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
class UserSettingsView(SuccessMessageMixin, UpdateView): class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
"""Update User settings""" """Update User settings"""
template_name = 'user/settings.html' template_name = 'user/settings.html'
@ -25,7 +26,8 @@ class UserSettingsView(SuccessMessageMixin, UpdateView):
def get_object(self): def get_object(self):
return self.request.user return self.request.user
class UserDeleteView(DeleteView):
class UserDeleteView(LoginRequiredMixin, DeleteView):
"""Delete user account""" """Delete user account"""
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
@ -38,7 +40,8 @@ class UserDeleteView(DeleteView):
logout(self.request) logout(self.request)
return reverse('passbook_core:auth-login') return reverse('passbook_core:auth-login')
class UserChangePasswordView(FormView):
class UserChangePasswordView(LoginRequiredMixin, FormView):
"""View for users to update their password""" """View for users to update their password"""
form_class = PasswordChangeForm form_class = PasswordChangeForm

View File

@ -1,5 +1,5 @@
"""passbook core utils view""" """passbook core utils view"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
@ -21,7 +21,7 @@ class LoadingView(TemplateView):
kwargs['target_url'] = self.get_url() kwargs['target_url'] = self.get_url()
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class PermissionDeniedView(TemplateView): class PermissionDeniedView(LoginRequiredMixin, TemplateView):
"""Generic Permission denied view""" """Generic Permission denied view"""
template_name = 'login/denied.html' template_name = 'login/denied.html'

View File

@ -1,2 +1,2 @@
"""passbook hibp_policy""" """passbook hibp_policy"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,6 +1,8 @@
"""passbook HaveIBeenPwned Policy forms""" """passbook HaveIBeenPwned Policy forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.core.forms.policies import GENERAL_FIELDS from passbook.core.forms.policies import GENERAL_FIELDS
from passbook.hibp_policy.models import HaveIBeenPwendPolicy from passbook.hibp_policy.models import HaveIBeenPwendPolicy
@ -16,4 +18,5 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header""" """Passbook ldap app Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,10 +1,12 @@
"""passbook LDAP Forms""" """passbook LDAP Forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.admin.forms.source import SOURCE_FORM_FIELDS
from passbook.ldap.models import LDAPSource from passbook.core.forms.policies import GENERAL_FIELDS
from passbook.ldap.models import LDAPGroupMembershipPolicy, LDAPSource
class LDAPSourceForm(forms.ModelForm): class LDAPSourceForm(forms.ModelForm):
@ -23,6 +25,7 @@ class LDAPSourceForm(forms.ModelForm):
'bind_password': forms.TextInput(), 'bind_password': forms.TextInput(),
'domain': forms.TextInput(), 'domain': forms.TextInput(),
'base_dn': forms.TextInput(), 'base_dn': forms.TextInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }
labels = { labels = {
'server_uri': _('Server URI'), 'server_uri': _('Server URI'),
@ -30,58 +33,18 @@ class LDAPSourceForm(forms.ModelForm):
'base_dn': _('Base DN'), 'base_dn': _('Base DN'),
} }
# class GeneralSettingsForm(SettingsForm):
# """general settings form"""
# MODE_AUTHENTICATION_BACKEND = 'auth_backend'
# MODE_CREATE_USERS = 'create_users'
# MODE_CHOICES = (
# (MODE_AUTHENTICATION_BACKEND, _('Authentication Backend')),
# (MODE_CREATE_USERS, _('Create Users'))
# )
# namespace = 'passbook.ldap' class LDAPGroupMembershipPolicyForm(forms.ModelForm):
# settings = ['enabled', 'mode'] """LDAPGroupMembershipPolicy Form"""
# widgets = { class Meta:
# 'enabled': forms.BooleanField(required=False),
# 'mode': forms.ChoiceField(widget=forms.RadioSelect, choices=MODE_CHOICES),
# }
model = LDAPGroupMembershipPolicy
# class ConnectionSettings(SettingsForm): fields = GENERAL_FIELDS + ['dn', ]
# """Connection settings form""" widgets = {
'name': forms.TextInput(),
# namespace = 'passbook.ldap' 'dn': forms.TextInput(),
# settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain'] }
labels = {
# attrs_map = { 'dn': _('DN')
# 'server': {'placeholder': 'dc1.corp.exmaple.com'}, }
# 'bind:user': {'placeholder': 'Administrator'},
# 'domain': {'placeholder': 'corp.example.com'},
# }
# widgets = {
# 'server:tls': forms.BooleanField(required=False, label=_('Server TLS')),
# }
# class AuthenticationBackendSettings(SettingsForm):
# """Authentication backend settings"""
# namespace = 'passbook.ldap'
# settings = ['base']
# attrs_map = {
# 'base': {'placeholder': 'DN in which to search for users'},
# }
# class CreateUsersSettings(SettingsForm):
# """Create users settings"""
# namespace = 'passbook.ldap'
# settings = ['create_base']
# attrs_map = {
# 'create_base': {'placeholder': 'DN in which to create users'},
# }

View File

@ -0,0 +1,28 @@
# Generated by Django 2.1.7 on 2019-03-10 18:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
('passbook_ldap', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LDAPGroupMembershipPolicy',
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')),
('dn', models.TextField()),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_ldap.LDAPSource')),
],
options={
'verbose_name': 'LDAP Group Membership Policy',
'verbose_name_plural': 'LDAP Group Membership Policys',
},
bases=('passbook_core.policy',),
),
]

View File

@ -3,7 +3,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Source from passbook.core.models import Policy, Source, User
class LDAPSource(Source): class LDAPSource(Source):
@ -37,30 +37,19 @@ class LDAPSource(Source):
verbose_name = _('LDAP Source') verbose_name = _('LDAP Source')
verbose_name_plural = _('LDAP Sources') verbose_name_plural = _('LDAP Sources')
class LDAPGroupMembershipPolicy(Policy):
"""Policy to check if a user is in a certain LDAP Group"""
# class LDAPModification(UUIDModel, CreatedUpdatedModel): dn = models.TextField()
# """Store LDAP Data in DB if LDAP Server is unavailable""" source = models.ForeignKey('LDAPSource', on_delete=models.CASCADE)
# ACTION_ADD = 'ADD'
# ACTION_MODIFY = 'MODIFY'
# ACTIONS = ( form = 'passbook.ldap.forms.LDAPGroupMembershipPolicyForm'
# (ACTION_ADD, 'ADD'),
# (ACTION_MODIFY, 'MODIFY'),
# )
# dn = models.CharField(max_length=255) def passes(self, user: User):
# action = models.CharField(max_length=17, choices=ACTIONS, default=ACTION_MODIFY) """Check if user instance passes this policy"""
# data = JSONField() raise NotImplementedError()
# def __str__(self): class Meta:
# return "LDAPModification %d from %s" % (self.pk, self.created)
verbose_name = _('LDAP Group Membership Policy')
# class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel): verbose_name_plural = _('LDAP Group Membership Policys')
# """Model to map an LDAP Group to a passbook group"""
# ldap_dn = models.TextField()
# group = models.ForeignKey(Group, on_delete=models.CASCADE)
# def __str__(self):
# return "LDAPGroupMapping %s -> %s" % (self.ldap_dn, self.group.name)

View File

@ -1,2 +1,2 @@
"""passbook lib""" """passbook lib"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header""" """passbook oauth_client Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,6 +1,7 @@
"""passbook oauth_client forms""" """passbook oauth_client forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.admin.forms.source import SOURCE_FORM_FIELDS
@ -29,6 +30,7 @@ class OAuthSourceForm(forms.ModelForm):
'consumer_key': forms.TextInput(), 'consumer_key': forms.TextInput(),
'consumer_secret': forms.TextInput(), 'consumer_secret': forms.TextInput(),
'provider_type': forms.Select(choices=MANAGER.get_name_tuple()), 'provider_type': forms.Select(choices=MANAGER.get_name_tuple()),
'policies': FilteredSelectMultiple(_('policies'), False)
} }
labels = { labels = {
'request_token_url': _('Request Token URL'), 'request_token_url': _('Request Token URL'),

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header""" """passbook oauth_provider Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -2,6 +2,7 @@
from logging import getLogger from logging import getLogger
from urllib.parse import urlencode from urllib.parse import urlencode
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from oauth2_provider.views.base import AuthorizationView from oauth2_provider.views.base import AuthorizationView
@ -15,7 +16,7 @@ from passbook.oauth_provider.models import OAuth2Provider
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
class PassbookAuthorizationLoadingView(LoadingView): class PassbookAuthorizationLoadingView(LoginRequiredMixin, LoadingView):
"""Show loading view for permission checks""" """Show loading view for permission checks"""
title = _('Checking permissions...') title = _('Checking permissions...')

View File

@ -1,2 +1,2 @@
"""passbook otp Header""" """passbook otp Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,6 +1,7 @@
"""passbook OTP Forms""" """passbook OTP Forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -63,4 +64,5 @@ class OTPFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }

View File

@ -1,2 +1,2 @@
"""passbook password_expiry""" """passbook password_expiry"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,6 +1,7 @@
"""passbook PasswordExpiry Policy forms""" """passbook PasswordExpiry Policy forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.forms.policies import GENERAL_FIELDS from passbook.core.forms.policies import GENERAL_FIELDS
@ -18,6 +19,7 @@ class PasswordExpiryPolicyForm(forms.ModelForm):
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'days': forms.NumberInput(), 'days': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }
labels = { labels = {
'deny_only': _("Only fail the policy, don't set user's password.") 'deny_only': _("Only fail the policy, don't set user's password.")

View File

@ -1,2 +1,2 @@
"""passbook saml_idp Header""" """passbook saml_idp Header"""
__version__ = '0.1.10-beta' __version__ = '0.1.13-beta'

View File

@ -1,6 +1,8 @@
"""passbook SAML IDP Forms""" """passbook SAML IDP Forms"""
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.lib.fields import DynamicArrayField from passbook.lib.fields import DynamicArrayField
from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider, from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider,
@ -32,6 +34,7 @@ class SAMLProviderForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'issuer': forms.TextInput(), 'issuer': forms.TextInput(),
'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False)
} }