Compare commits

...

38 Commits

Author SHA1 Message Date
63aa48d981 bump version: 0.1.16-beta -> 0.1.17-beta 2019-03-12 15:55:18 +01:00
2e0ba05d55 prepare 0.1.17 2019-03-12 15:55:14 +01:00
b2ac57bb67 switch to vertical navigation 2019-03-12 13:35:09 +01:00
4c22e5c2c8 don't use celery heartbeat, use TCP keepalive instead 2019-03-12 13:34:54 +01:00
4a7b0ec8a9 remove Application.user_is_authorized 2019-03-12 10:56:01 +01:00
330118249e bump version: 0.1.15-beta -> 0.1.16-beta 2019-03-11 21:35:11 +01:00
8d4dabde02 finalize RabbitMQ replacement, update debian package, remove redis tgz 2019-03-11 21:35:06 +01:00
cf7323c41b bump version: 0.1.14-beta -> 0.1.15-beta 2019-03-11 21:01:18 +01:00
edd856df7d redis -> rabbitmq 2019-03-11 20:46:19 +01:00
5e35859db6 bump version: 0.1.13-beta -> 0.1.14-beta 2019-03-11 11:44:34 +01:00
acabb2df54 fix unittests 2019-03-11 11:44:12 +01:00
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
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
66 changed files with 905 additions and 334 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.1.9-beta current_version = 0.1.17-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.17-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

64
debian/changelog vendored
View File

@ -1,3 +1,67 @@
passbook (0.1.17) stable; urgency=medium
* bump version: 0.1.15-beta -> 0.1.16-beta
* remove Application.user_is_authorized
* don't use celery heartbeat, use TCP keepalive instead
* switch to vertical navigation
-- Jens Langhammer <jens.langhammer@beryju.org> Tue, 12 Mar 2019 14:54:27 +0000
passbook (0.1.16) stable; urgency=medium
* Replace redis with RabbitMQ
* updated debian package to suggest RabbitMQ
* update helm chart to require RabbitMQ
* fix invalid default config in debian package
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 10:28:36 +0000
passbook (0.1.14) 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
* 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

2
debian/control vendored
View File

@ -8,7 +8,7 @@ Standards-Version: 3.9.6
Package: passbook Package: passbook
Architecture: all Architecture: all
Recommends: mysql-server, redis-server Recommends: mysql-server, rabbitmq-server
Pre-Depends: adduser, libldap2-dev, libsasl2-dev Pre-Depends: adduser, libldap2-dev, libsasl2-dev
Depends: python3 (>= 3.5) | python3.6 | python3.7, python3-pip, dbconfig-pgsql | dbconfig-no-thanks, ${misc:Depends} Depends: python3 (>= 3.5) | python3.6 | python3.7, python3-pip, dbconfig-pgsql | dbconfig-no-thanks, ${misc:Depends}
Description: Authentication Provider/Proxy supporting protocols like SAML, OAuth, LDAP and more. Description: Authentication Provider/Proxy supporting protocols like SAML, OAuth, LDAP and more.

View File

@ -1,4 +1,3 @@
debug: false
http: http:
host: 0.0.0.0 host: 0.0.0.0
port: 8000 port: 8000
@ -8,37 +7,71 @@ log:
console: INFO console: INFO
file: DEBUG file: DEBUG
file: /var/log/passbook/passbook.log file: /var/log/passbook/passbook.log
# Error reporting, disabled by default debug: false
# error_report_enabled: true secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: guest:guest@localhost/passbook
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true
# Set this to the server's external address. passbook:
# This is used to generate external URLs sign_up:
external_url: http://image.example.com # Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true
enabled: true
# This dictates how the Path is generated password_reset:
# can be either of: # Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
# - view_sha512_short enabled: true
# - view_md5 # Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
# - view_sha256 verification:
# - view_sha512 - email
default_return_view: view_sha256 # Text used in title, on login page and multiple other places
branding: passbook
# Set this to true if you only want to use external authentication login:
external_auth_only: false # Override URL used for logo
logo_url: null
# If this is true, images are automatically claimed if the windows user exists # Override URL used for Background on Login page
# in django bg_url: null
auto_claim_enabled: true # Optionally add a subtext, placed below logo on the login page
subtext: null
# LDAP Authentication footer:
# ldap: links:
# enabled: false # Optionally add links to the footer on the login page
# server: # - name: test
# uri: 'ldap://dc1.example.com' # href: https://test
# tls: false # Specify which fields can be used to authenticate. Can be any combination of `username` and `email`
# bind: uid_fields:
# dn: '' - username
# password: '' - email
# search_base: '' session:
# filter: '(sAMAccountName=%(user)s)' remember_age: 2592000 # 60 * 60 * 24 * 30, one month
# require_group: '' # Provider-specific settings
ldap:
# Which field from `uid_fields` maps to which LDAP Attribute
login_field_map:
username: sAMAccountName
email: mail # or userPrincipalName
user_attribute_map:
active_directory:
username: "%(sAMAccountName)s"
email: "%(mail)s"
name: "%(displayName)"
oauth_client:
# List of python packages with sources types to load.
types:
- passbook.oauth_client.source_types.discord
- passbook.oauth_client.source_types.facebook
- passbook.oauth_client.source_types.github
- passbook.oauth_client.source_types.google
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
saml_idp:
# List of python packages with provider types to load.
types:
- passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.aws
- passbook.saml_idp.processors.gitlab
- passbook.saml_idp.processors.nextcloud
- passbook.saml_idp.processors.salesforce
- passbook.saml_idp.processors.shibboleth
- passbook.saml_idp.processors.wordpress_orange

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.1.9-beta" appVersion: "0.1.17-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.17-beta"
icon: https://passbook.beryju.org/images/logo.png icon: https://passbook.beryju.org/images/logo.png

Binary file not shown.

Binary file not shown.

View File

@ -1,9 +1,9 @@
dependencies: dependencies:
- name: redis - name: rabbitmq
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
version: 5.1.0 version: 4.3.2
- name: postgresql - name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
version: 3.10.1 version: 3.10.1
digest: sha256:04bd136761f070e94a2ff32ff48ff87f5e07fbd451e5fd7f65551e3bd4680e5e digest: sha256:c36e054785f7d706d7d3f525eb1b167dbc89b42f84da7fc167a18bbb6542c999
generated: 2019-02-08T12:08:49.090666+01:00 generated: 2019-03-11T20:36:35.125079+01:00

View File

@ -1,6 +1,6 @@
dependencies: dependencies:
- name: redis - name: rabbitmq
version: 5.1.0 version: 4.3.2
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
- name: postgresql - name: postgresql
version: 3.10.1 version: 3.10.1

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: ''
@ -36,7 +36,7 @@ data:
debug: false debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master" rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }} error_report_enabled: {{ .Values.config.error_reporting }}

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.17-beta
nameOverride: "" nameOverride: ""
@ -14,10 +14,16 @@ 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
postgresqlPassword: foo postgresqlPassword: foo
rabbitmq:
rabbitmq:
password: foo
service: service:
type: ClusterIP type: ClusterIP
@ -31,7 +37,6 @@ ingress:
path: / path: /
hosts: hosts:
- passbook.k8s.local - passbook.k8s.local
- kubernetes-healthcheck-host
defaultHost: passbook.k8s.local defaultHost: passbook.k8s.local
tls: [] tls: []
# - secretName: chart-example-tls # - secretName: chart-example-tls

View File

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

View File

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

View File

@ -4,46 +4,4 @@
{% load is_active %} {% load is_active %}
{% block nav_secondary %} {% block nav_secondary %}
<ul class="nav navbar-nav navbar-persistent">
<li class="{% is_active 'passbook_admin:overview' %}">
<a href="{% url 'passbook_admin:overview' %}">{% trans 'Overview' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
<a href="{% url 'passbook_admin:applications' %}">{% trans 'Applications' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
<a href="{% url 'passbook_admin:sources' %}">{% trans 'Sources' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
<a href="{% url 'passbook_admin:property-mappings' %}">{% trans 'Property Mappings' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
<a href="{% url 'passbook_admin:policies' %}">{% trans 'Policies' %}</a>
</li>
<li
class="{% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
<a href="{% url 'passbook_admin:invitations' %}">{% trans 'Invitations' %}</a>
</li>
<li class="{% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">{% trans 'Users' %}</a>
</li>
<li class="{% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">{% trans 'Audit Log' %}</a>
</li>
<li class="{% is_active_app 'admin' %}">
<a href="{% url 'admin:index' %}">{% trans 'Django' %}</a>
</li>
</ul>
{% endblock %} {% endblock %}

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.17-beta'

View File

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

View File

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

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.1.9-beta' __version__ = '0.1.17-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
@ -152,11 +152,6 @@ class Application(PolicyModel):
objects = InheritanceManager() objects = InheritanceManager()
def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application"""
from passbook.core.policies import PolicyEngine
return PolicyEngine(self.policies.all()).for_user(user).build().result
def get_provider(self): def get_provider(self):
"""Get casted provider instance""" """Get casted provider instance"""
if not self.provider: if not self.provider:
@ -393,6 +388,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

@ -7,7 +7,6 @@ raven
markdown markdown
colorlog colorlog
celery celery
redis
psycopg2 psycopg2
idna<2.8,>=2.5 idna<2.8,>=2.5
cherrypy cherrypy

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',
@ -184,8 +184,10 @@ CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULE = {} CELERY_BEAT_SCHEDULE = {}
CELERY_CREATE_MISSING_QUEUES = True CELERY_CREATE_MISSING_QUEUES = True
CELERY_TASK_DEFAULT_QUEUE = 'passbook' CELERY_TASK_DEFAULT_QUEUE = 'passbook'
CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis') CELERY_BROKER_URL = 'amqp://%s' % CONFIG.get('rabbitmq')
CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis') CELERY_RESULT_BACKEND = 'rpc://'
CELERY_ACKS_LATE = True
CELERY_BROKER_HEARTBEAT = 0
# Raven settings # Raven settings
RAVEN_CONFIG = { RAVEN_CONFIG = {

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 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="270px" height="10px" viewBox="0 0 270 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg> width="270px" height="20px" viewBox="0 0 270 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -8,38 +8,40 @@
<div class="toast-notifications-list-pf"> <div class="toast-notifications-list-pf">
{% include 'partials/messages.html' %} {% include 'partials/messages.html' %}
</div> </div>
<nav class="navbar navbar-default navbar-pf" role="navigation"> <nav class="navbar navbar-pf-vertical">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse-1"> <button type="button" class="navbar-toggle">
<span class="sr-only">{% trans 'Toggle navigation' %}</span> <span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
<img src="{% static 'img/brand.svg' %}" alt="passbook" /> <img class="navbar-brand-icon" src="{% static 'img/logo.png' %}" alt="" />
<img class="navbar-brand-name" src="{% static 'img/brand.svg' %}" alt="passbook" />
</a> </a>
</div> </div>
<div class="collapse navbar-collapse navbar-collapse-1"> <nav class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-utility"> <ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
<li class="dropdown"> <li class="dropdown">
<button class="btn btn-link nav-item-iconic" id="horizontalDropdownMenu1" data-toggle="dropdown" <button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true"> aria-haspopup="true" aria-expanded="true">
<span title="Help" class="fa pficon-help dropdown-title"></span> <span title="Help" class="fa pficon-help dropdown-title"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="horizontalDropdownMenu1"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %} {% comment %} <li><a href="#0">Help</a></li> {% endcomment %}
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li> <li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
<button class="btn btn-link dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown"
<span class="pficon pficon-user"></span> aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="dropdown-title"> <span class="dropdown-title">
{{ user.username }} <b class="caret"></b> {{ user.username }} <span class="caret"></span>
</span> </span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li> <li>
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a> <a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a>
</li> </li>
@ -53,21 +55,129 @@
</ul> </ul>
</li> </li>
</ul> </ul>
{% is_active_app 'passbook_admin' as is_admin %} </nav>
<ul class="nav navbar-nav navbar-primary {% if is_admin == 'active' %}persistent-secondary{% endif %}">
<li class="{% is_active_url 'passbook_core:overview' %}">
<a href="{% url 'passbook_core:overview' %}">{% trans 'Overview' %}</a>
</li>
{% if user.is_superuser %}
<li class="{% is_active_app 'passbook_admin' %}">
<a href="{% url 'passbook_admin:overview' %}">{% trans 'Administration' %}</a>
{% block nav_secondary %}
{% endblock %}
</li>
{% endif %}
</ul>
</div>
</nav> </nav>
<div class="nav-pf-vertical nav-pf-vertical-with-sub-menus hide-nav-pf">
<ul class="list-group">
<li class="list-group-item {% is_active_url 'passbook_core:overview' %}">
<a href="{% url 'passbook_core:overview' %}">
<span class="fa fa-dashboard" data-toggle="tooltip" title="{% trans 'Overview' %}"></span>
<span class="list-group-item-value">{% trans 'Overview' %}</span>
</a>
</li>
{% is_active_app 'passbook_admin' as is_admin %}
{% if user.is_superuser %}
<li class="list-group-item {% is_active_app 'passbook_admin' %} secondary-nav-item-pf">
<a href="{% url 'passbook_admin:overview' %}">
<span class="pficon pficon-user" data-toggle="tooltip" title=""
data-original-title="{% trans 'Administration' %}"></span>
<span class="list-group-item-value dropdown-title">{% trans 'Administration' %}</span>
</a>
<div id="user-secondary" class="nav-pf-secondary-nav">
<div class="nav-item-pf-header">
<a href="#0" class="secondary-collapse-toggle-pf" data-toggle="collapse-secondary-nav"></a>
<span>{% trans 'Administration' %}</span>
</div>
<ul class="list-group">
<li class="list-group-item {% is_active 'passbook_admin:overview' %}">
<a href="{% url 'passbook_admin:overview' %}">
<span class="list-group-item-value">
{% trans 'Overview' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
<a href="{% url 'passbook_admin:applications' %}">
<span class="list-group-item-value">
{% trans 'Applications' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
<a href="{% url 'passbook_admin:sources' %}">
<span class="list-group-item-value">
{% trans 'Sources' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:providers' %}">
<span class="list-group-item-value">
{% trans 'Providers' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
<a href="{% url 'passbook_admin:property-mappings' %}">
<span class="list-group-item-value">
{% trans 'Property Mappings' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">
<span class="list-group-item-value">
{% trans 'Factors' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
<a href="{% url 'passbook_admin:policies' %}">
<span class="list-group-item-value">
{% trans 'Policies' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
<a href="{% url 'passbook_admin:invitations' %}">
<span class="list-group-item-value">
{% trans 'Invitations' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">
<span class="list-group-item-value">
{% trans 'Users' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
<a href="{% url 'passbook_admin:groups' %}">
<span class="list-group-item-value">
{% trans 'Groups' %}
</span>
</a>
</li>
<li class="list-group-item {% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">
<span class="list-group-item-value">
{% trans 'Audit Log' %}
</span>
</a>
</li>
<li class="list-group-item {% is_active_app 'admin' %}">
<a href="{% url 'admin:index' %}">
<span class="list-group-item-value">
{% trans 'Django' %}
</span>
</a>
</li>
</ul>
</div>
</li>
{% endif %}
</ul>
</div>
<div class="container-fluid container-cards-pf"> <div class="container-fluid container-cards-pf">
{% block content %} {% block content %}
{% endblock %} {% endblock %}

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,7 +1,10 @@
"""passbook util view tests""" """passbook util view tests"""
import string
from random import SystemRandom
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from passbook.core.models import User
from passbook.core.views.utils import LoadingView, PermissionDeniedView from passbook.core.views.utils import LoadingView, PermissionDeniedView
@ -9,6 +12,11 @@ class TestUtilViews(TestCase):
"""Test Utility Views""" """Test Utility Views"""
def setUp(self): def setUp(self):
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
self.factory = RequestFactory() self.factory = RequestFactory()
def test_loading_view(self): def test_loading_view(self):
@ -21,5 +29,6 @@ class TestUtilViews(TestCase):
def test_permission_denied_view(self): def test_permission_denied_view(self):
"""Test PermissionDeniedView""" """Test PermissionDeniedView"""
request = self.factory.get('something') request = self.factory.get('something')
request.user = self.user
response = PermissionDeniedView.as_view()(request) response = PermissionDeniedView.as_view()(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -5,6 +5,7 @@ from django.contrib import messages
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -28,4 +29,6 @@ class AccessMixin:
def user_has_access(self, application, user): def user_has_access(self, application, user):
"""Check if user has access to application.""" """Check if user has access to application."""
LOGGER.debug("Checking permissions of %s on application %s...", user, application) LOGGER.debug("Checking permissions of %s on application %s...", user, application)
return application.user_is_authorized(user) policy_engine = PolicyEngine(application.policies.all())
policy_engine.for_user(user).with_request(self.request).build()
return policy_engine.result

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

View File

@ -29,7 +29,7 @@ web:
debug: false debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
redis: localhost rabbitmq: guest:guest@localhost/passbook
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true error_report_enabled: true
secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s
@ -62,11 +62,6 @@ passbook:
uid_fields: uid_fields:
- username - username
- email - email
# Factors to load
factors:
- passbook.core.auth.factors.backend
- passbook.core.auth.factors.dummy
- passbook.captcha_factor.factor
session: session:
remember_age: 2592000 # 60 * 60 * 24 * 30, one month remember_age: 2592000 # 60 * 60 * 24 * 30, one month
# Provider-specific settings # Provider-specific settings

View File

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

View File

@ -2,6 +2,8 @@
from logging import getLogger from logging import getLogger
from urllib.parse import urlencode from urllib.parse import urlencode
from django.contrib import messages
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 +17,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...')
@ -48,7 +50,10 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView):
provider.save() provider.save()
self._application = application self._application = application
# Check permissions # Check permissions
if not self.user_has_access(self._application, request.user): passing, policy_meaages = self.user_has_access(self._application, request.user)
if not passing:
for policy_meaage in policy_meaages:
messages.error(request, policy_meaage)
return redirect('passbook_oauth_provider:oauth2-permission-denied') return redirect('passbook_oauth_provider:oauth2-permission-denied')
actual_response = super().dispatch(request, *args, **kwargs) actual_response = super().dispatch(request, *args, **kwargs)
if actual_response.status_code == 400: if actual_response.status_code == 400:

View File

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