Compare commits

...

16 Commits

Author SHA1 Message Date
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
eebbae0677 bump version: 0.1.9-beta -> 0.1.10-beta 2019-03-10 15:54:50 +01:00
42b30f4507 prepare 0.1.10 release 2019-03-10 15:53:38 +01:00
0e425418df better show loading state when testing a policy 2019-03-10 15:46:49 +01:00
7fe0300b86 Fix button on policy test page 2019-03-10 15:36:49 +01:00
c012c6be5c fix k8s service routing http traffic to workers 2019-03-10 15:34:24 +01:00
2d7e8f1b50 add group administration 2019-03-08 15:49:45 +01:00
46 changed files with 559 additions and 184 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.1.9-beta current_version = 0.1.11-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.9-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.11-beta
stage: build stage: build
only: only:
- tags - tags
@ -113,3 +114,18 @@ 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 --client-only
- helm repo add beryju.org https://pkg.beryju.org/repository/helm/
- helm repo update
- helm upgrade passbook-prod beryju.org/passbook --devel

27
debian/changelog vendored
View File

@ -1,3 +1,30 @@
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
* bump version: 0.1.7-beta -> 0.1.8-beta
* consistently using PolicyEngine
* add more Verbosity to PolicyEngine, rewrite SAML Authorisation check
* slightly refactor Factor View, add more unittests
* add impersonation middleware, add to templates
* bump version: 0.1.8-beta -> 0.1.9-beta
* fix k8s service routing http traffic to workers
* Fix button on policy test page
* better show loading state when testing a policy
-- Jens Langhammer <jens.langhammer@beryju.org> Sun, 10 Mar 2019 14:52:40 +0000
passbook (0.1.7) stable; urgency=medium passbook (0.1.7) stable; urgency=medium
* bump version: 0.1.3-beta -> 0.1.4-beta * bump version: 0.1.3-beta -> 0.1.4-beta

View File

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

View File

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

@ -17,3 +17,4 @@ spec:
selector: selector:
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

View File

@ -18,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.9-beta tag: 0.1.11-beta
nameOverride: "" nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-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

@ -5,3 +5,22 @@
{% block above_form %} {% block above_form %}
<h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1> <h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %}
{% trans 'Test' %}
{% endblock %}
{% block beneath_form %}
<p class="loading" style="display: none;">
<span class="spinner spinner-xs spinner-inline"></span> {% trans 'Processing, please wait...' %}
</p>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script>
$('form').on('submit', function () {
$('p.loading').show();
})
</script>
{% 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 %}
@ -19,6 +29,8 @@
<input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
{% block beneath_form %}
{% endblock %}
</div> </div>
{% endblock %} {% endblock %}

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.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-beta'

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,5 +1,7 @@
"""passbook administration forms""" """passbook administration 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.models import DummyFactor, PasswordFactor from passbook.core.models import DummyFactor, PasswordFactor
from passbook.lib.fields import DynamicArrayField from passbook.lib.fields import DynamicArrayField
@ -16,6 +18,7 @@ class PasswordFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
} }
field_classes = { field_classes = {
'backends': DynamicArrayField 'backends': DynamicArrayField
@ -31,4 +34,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

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

@ -60,6 +60,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

@ -1,2 +1,2 @@
"""passbook hibp_policy""" """passbook hibp_policy"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-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.9-beta' __version__ = '0.1.11-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.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header""" """passbook oauth_client Header"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-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.9-beta' __version__ = '0.1.11-beta'

View File

@ -1,2 +1,2 @@
"""passbook otp Header""" """passbook otp Header"""
__version__ = '0.1.9-beta' __version__ = '0.1.11-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.9-beta' __version__ = '0.1.11-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.9-beta' __version__ = '0.1.11-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)
} }