Compare commits

...

110 Commits

Author SHA1 Message Date
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
a5dc193cfd bump version: 0.1.8-beta -> 0.1.9-beta 2019-03-10 12:17:48 +01:00
7507ad2620 Merge branch '24-impersonate' into 'master'
Resolve "Impersonate user"

Closes #24

See merge request BeryJu.org/passbook!11
2019-03-10 01:47:29 +00:00
f1291fec8d add impersonation middleware, add to templates 2019-03-10 02:41:31 +01:00
37aeeea239 slightly refactor Factor View, add more unittests 2019-03-10 02:08:09 +01:00
0fa1fc86da add more Verbosity to PolicyEngine, rewrite SAML Authorisation check 2019-03-10 02:07:48 +01:00
c3034ab9ac consistently using PolicyEngine 2019-03-10 02:07:18 +01:00
76694e037a bump version: 0.1.7-beta -> 0.1.8-beta 2019-03-08 21:43:35 +01:00
787db41cc3 prepare for 0.1.7 2019-03-08 21:43:33 +01:00
74da3df7cd bump version: 0.1.6-beta -> 0.1.7-beta 2019-03-08 21:37:59 +01:00
a6e435bd70 prepare debian changelog for 0.1.6 2019-03-08 21:37:55 +01:00
c313b496aa Improve access control for saml 2019-03-08 21:30:16 +01:00
a7eaa74191 fix MATCH_EXACT not working as intended 2019-03-08 21:20:38 +01:00
11ecdc4fcf bump version: 0.1.5-beta -> 0.1.6-beta 2019-03-08 20:39:27 +01:00
2f7781b67a fix captcha factor not loading keys from Factor class 2019-03-08 20:08:28 +01:00
296d4f691a add passing property to PolicyEngine 2019-03-08 19:49:53 +01:00
64033031b1 remove audit's login attempt 2019-03-08 19:45:50 +01:00
9daff7608d fix password not getting set on user import 2019-03-08 19:45:41 +01:00
0a4af80b9b fix static files missing for debian package 2019-03-08 16:41:52 +01:00
a54adb05c4 bump version: 0.1.4-beta -> 0.1.5-beta 2019-03-08 16:03:52 +01:00
43a389e596 Merge branch '22-custom-property-mapping' into 'master'
Resolve "Custom Property Mapping"

Closes #22

See merge request BeryJu.org/passbook!8
2019-03-08 15:03:08 +00:00
2d7e8f1b50 add group administration 2019-03-08 15:49:45 +01:00
cf11f6b121 format data before inserting it 2019-03-08 15:16:25 +01:00
6dcdf7bcce add custom DynamicArrayField to better handle arrays 2019-03-08 15:11:01 +01:00
56d872af15 add PropertyMapping Model, add Subclass for SAML, test with AWS 2019-03-08 12:47:50 +01:00
ca663d16fc fix debian build (again) 2019-03-07 20:58:18 +01:00
e05c18b19b implicitly add kubernetes-healthcheck-host in helm configmap 2019-03-07 17:11:55 +01:00
a7b86e46bc bump version: 0.1.3-beta -> 0.1.4-beta 2019-03-07 16:24:09 +01:00
84f56674c2 prepare 0.1.4 2019-03-07 16:24:07 +01:00
02ab177c6d install python3-venv for debian build 2019-03-07 16:23:42 +01:00
1232c487e9 bump version: 0.1.2-beta -> 0.1.3-beta 2019-03-07 16:13:05 +01:00
ef0a2bfbe8 Merge branch '11-debian-packaging' into 'master'
add debian package files

Closes #11

See merge request BeryJu.org/passbook!7
2019-03-07 15:06:33 +00:00
05242a11ad add debian package files 2019-03-07 16:01:31 +01:00
4593ad7bcc load AWS processor by default on helm 2019-03-07 14:49:06 +01:00
d7fd5a7fa6 Fix redis dependency being too old 2019-03-07 14:39:00 +01:00
4439378fd4 bump version: 0.1.1-beta -> 0.1.2-beta 2019-03-07 14:14:51 +01:00
acf65eafdd make naming of Providers more consistent 2019-03-07 14:14:49 +01:00
c2ebff55ef fix IDP-initiated login not working 2019-03-07 14:10:06 +01:00
99c82676b6 Add some more failsafe for administration 2019-03-07 14:09:52 +01:00
4991e9b825 Merge branch '1-suspicious-request' into 'master'
fix broken E-Mail templatetag

Closes #1

See merge request BeryJu.org/passbook!5
2019-03-03 20:18:23 +00:00
612f95c3ba fix broken E-Mail templatetag 2019-03-03 21:05:17 +01:00
cd91d5ca15 Merge branch '1-suspicious-request' into 'master'
Resolve "Suspicious request detector (many invalid logins from one IP, many attempts on one username, etc)"

Closes #1

See merge request BeryJu.org/passbook!3
2019-03-03 20:04:56 +00:00
cbbbb5dc08 Merge branch '20-sentry' into 'master'
Resolve "Sentry Error Tracking"

Closes #20

See merge request BeryJu.org/passbook!4
2019-03-03 19:58:18 +00:00
c1640b9411 fix prospector/isort errors 2019-03-03 20:54:23 +01:00
a4842c1f95 add sentry configuration 2019-03-03 20:48:31 +01:00
a4707ddc54 fix failing unittests 2019-03-03 20:34:00 +01:00
fb82d56307 create suspicious request detector and policy, add request to policy engine 2019-03-03 20:26:25 +01:00
1a1005f80d remove audit's LoginAttempt 2019-03-03 20:13:54 +01:00
e86cae6cac Merge branch '18-password-expiry' into 'master'
Resolve "Password Expiry"

Closes #18

See merge request BeryJu.org/passbook!2
2019-03-03 16:53:31 +00:00
0b282f45e0 fix pylint messages 2019-03-03 17:45:20 +01:00
791e88ffc1 Fix negate on FieldMatcherPolicy 2019-03-03 17:21:58 +01:00
7bd3c4bccf Better handle Policy.action and Policy.negate 2019-03-03 17:12:53 +01:00
722e2e4050 Show warning when un-attached policies exist 2019-03-03 17:12:35 +01:00
c7fc444c95 add password policy 2019-03-03 17:12:05 +01:00
20ad062814 Log SAML Authorization actions 2019-03-03 00:34:34 +01:00
fcb5d36e07 cleanup SAML urls 2019-03-03 00:07:40 +01:00
9b131b619f Show warning message when no Factor exists 2019-03-02 23:54:40 +01:00
54427f7c68 use HTML5 autocomplete values to better handle password managers 2019-03-02 23:19:58 +01:00
35eef9c28d improve worker warning 2019-03-02 22:41:25 +01:00
e88a82553d use separate Form for Admin user editing (allow is_staff and is_active) 2019-03-02 22:41:14 +01:00
01a9520140 add import_users script to import users from CSV with already hashed passwords 2019-03-02 22:40:47 +01:00
46667615c3 switch releases to beta 2019-02-27 17:47:41 +01:00
c6721a83a4 bump version: 0.1.1-alpha -> 0.1.1-beta 2019-02-27 17:45:10 +01:00
46866e8ef0 bump version: 0.1.0-beta -> 0.1.1-alpha 2019-02-27 17:43:28 +01:00
4a49681127 Fix docker build failing 2019-02-27 17:43:24 +01:00
4c3fced4e9 bump version: 0.1.0-alpha -> 0.1.0-beta 2019-02-27 16:45:52 +01:00
172347d90f bump version: 0.0.13-alpha -> 0.1.0-alpha 2019-02-27 16:42:52 +01:00
f54520b5cf bump version: 0.0.12-alpha -> 0.0.13-alpha 2019-02-27 16:06:28 +01:00
d7c4697625 Only use one create template, get title from Form's Model 2019-02-27 16:06:20 +01:00
5584f5bda8 switch to PolicyEngine everywhere 2019-02-27 15:49:20 +01:00
2ce6f5a714 improve error display on forms 2019-02-27 15:49:05 +01:00
c66945623a Improve admin interface more (back links, better headlines) 2019-02-27 15:48:33 +01:00
cbae05c74c show more useful information on admin overview 2019-02-27 15:45:42 +01:00
5b771da972 switch from first_name and last_name to name 2019-02-27 15:09:05 +01:00
2db1738e4a make Admin UI more consistent, better show when provider has no application assigned 2019-02-27 14:47:11 +01:00
95de6a14fd bump version: 0.0.11-alpha -> 0.0.12-alpha 2019-02-27 13:18:28 +01:00
17132ebc19 Verify OAuth Username vuln and fix closes #9 2019-02-27 13:18:16 +01:00
289be46388 fix SAML Views not having LoginRequiredMixin 2019-02-27 12:36:18 +01:00
6c300b7b31 autofocus password field 2019-02-27 12:35:57 +01:00
b726583084 Keep GET parameters throughout entire login process 2019-02-27 12:35:48 +01:00
48055d1cfd fix CSRF Bug in SAML 2019-02-27 11:20:52 +01:00
436070f5bd fix redis connection issues in k8s 2019-02-27 09:59:01 +01:00
3ee79818db explicit version in helm values 2019-02-27 09:33:26 +01:00
e7a02104db fix display on mobile 2019-02-27 09:33:12 +01:00
556740d7bc add PasswordPolicyForm back in 2019-02-26 15:41:11 +01:00
421f51770c implement password policy checking on signup and password change closes #8 2019-02-26 15:40:58 +01:00
96f7e70f9e enable always_eager when unittesting 2019-02-26 14:24:50 +01:00
ad96f7dbb8 add E-Mail support via celery task, untested, closes #17 2019-02-26 14:10:53 +01:00
e7fb48eba2 bump version: 0.0.10-alpha -> 0.0.11-alpha 2019-02-26 13:06:26 +01:00
b19b5b644d remove hardcoded passwords 2019-02-26 13:06:22 +01:00
179 changed files with 3117 additions and 716 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.10-alpha current_version = 0.1.12-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>.*)
@ -9,11 +9,14 @@ tag_name = version/{new_version}
[bumpversion:part:release] [bumpversion:part:release]
optional_value = stable optional_value = stable
first_value = beta
values = values =
alpha alpha
beta beta
stable stable
[bumpversion:file:helm/passbook/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml] [bumpversion:file:helm/passbook/Chart.yaml]
[bumpversion:file:.gitlab-ci.yml] [bumpversion:file:.gitlab-ci.yml]
@ -34,6 +37,10 @@ values =
[bumpversion:file:passbook/lib/__init__.py] [bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/hibp_policy/__init__.py]
[bumpversion:file:passbook/password_expiry_policy/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py] [bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py] [bumpversion:file:passbook/audit/__init__.py]

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
@ -16,7 +17,6 @@ variables:
POSTGRES_DB: passbook POSTGRES_DB: passbook
POSTGRES_USER: passbook POSTGRES_USER: passbook
POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77' POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77'
SUPERVISR_ENV: ci
include: include:
- /allauth/.gitlab-ci.yml - /allauth/.gitlab-ci.yml
@ -54,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.0.10-alpha - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.12-beta
stage: build stage: build
only: only:
- tags - tags
@ -69,54 +69,30 @@ package-helm:
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/
# package-3.5: package-debian:
# before_script: before_script:
# - apt update - apt update
# - apt install -y build-essential debhelper devscripts equivs python3 python3-pip - apt install -y --no-install-recommends build-essential debhelper devscripts equivs python3 python3-dev python3-pip libsasl2-dev libldap2-dev
# - cp debian/control-3.5 debian/control - mk-build-deps debian/control
# - mk-build-deps debian/control - apt install ./*build-deps*deb -f -y
# - apt install ./*build-deps*deb -f -y - python3 -m pip install -U virtualenv pip
# - "python3 -m pip install -U virtualenv" - virtualenv env
# - "virtualenv env" - source env/bin/activate
# - "source env/bin/activate" - pip3 install -U -r requirements.txt -r requirements-dev.txt
# - "pip3 install -U -r requirements.txt -r requirements-dev.txt" - ./manage.py collectstatic --no-input
# image: debian image: ubuntu:18.04
# script: script:
# - debuild -us -uc - debuild -us -uc
# - cp ../passbook*.deb . - cp ../passbook*.deb .
# - python manage.py nexus_upload - ./manage.py nexus_upload --method post --url $NEXUS_URL --auth $NEXUS_AUTH --repo apt passbook*deb
# artifacts: artifacts:
# paths: paths:
# - passbook-python3.5*deb - passbook*deb
# expire_in: 2 days expire_in: 2 days
# stage: build stage: build
# only: only:
# - tags - tags
# - /^debian/.*$/ - /^version/.*$/
# package-3.6:
# before_script:
# - apt update
# - apt install -y build-essential debhelper devscripts equivs python3 python3-pip
# - cp debian/control-3.6 debian/control
# - mk-build-deps debian/control
# - apt install ./*build-deps*deb -f -y
# - "python3 -m pip install -U virtualenv"
# - "virtualenv env"
# - "source env/bin/activate"
# - "pip3 install -U -r requirements.txt -r requirements-dev.txt"
# image: debian:buster
# script:
# - debuild -us -uc
# - cp ../passbook*.deb .
# - python manage.py nexus_upload
# artifacts:
# paths:
# - passbook-python3.6*deb
# expire_in: 2 days
# stage: build
# only:
# - tags
# - /^debian/.*$r
# docs: # docs:
# stage: docs # stage: docs
@ -138,3 +114,16 @@ package-helm:
# - 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

View File

@ -6,10 +6,13 @@ COPY ./requirements.txt /app/
WORKDIR /app/ WORKDIR /app/
RUN mkdir /app/static/ && \ RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
mkdir /app/static/ && \
pip install -r requirements.txt && \ pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
FROM python:3.6-slim-stretch FROM python:3.6-slim-stretch
@ -20,9 +23,12 @@ COPY --from=build /app/static /app/static/
WORKDIR /app/ WORKDIR /app/
RUN pip install -r requirements.txt && \ RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
adduser --system --home /app/ passbook && \ adduser --system --home /app/ passbook && \
chown -R passbook /app/ chown -R passbook /app/ && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
USER passbook USER passbook

62
debian/changelog vendored Normal file
View File

@ -0,0 +1,62 @@
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
* bump version: 0.1.3-beta -> 0.1.4-beta
* implicitly add kubernetes-healthcheck-host in helm configmap
* fix debian build (again)
* add PropertyMapping Model, add Subclass for SAML, test with AWS
* add custom DynamicArrayField to better handle arrays
* format data before inserting it
* bump version: 0.1.4-beta -> 0.1.5-beta
* fix static files missing for debian package
* fix password not getting set on user import
* remove audit's login attempt
* add passing property to PolicyEngine
* fix captcha factor not loading keys from Factor class
* bump version: 0.1.5-beta -> 0.1.6-beta
* fix MATCH_EXACT not working as intended
* Improve access control for saml
-- Jens Langhammer <jens.langhammer@beryju.org> Fri, 08 Mar 2019 20:37:05 +0000
passbook (0.1.4) stable; urgency=medium
* initial debian package release
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 06 Mar 2019 18:22:41 +0000

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
10

20
debian/config vendored Normal file
View File

@ -0,0 +1,20 @@
#!/bin/sh
# config maintainer script for passbook
set -e
# source debconf stuff
. /usr/share/debconf/confmodule
dbc_first_version=1.0.0
dbc_dbuser=passbook
dbc_dbname=passbook
# source dbconfig-common shell library, and call the hook function
if [ -f /usr/share/dbconfig-common/dpkg/config.pgsql ]; then
. /usr/share/dbconfig-common/dpkg/config.pgsql
dbc_go passbook "$@"
fi
#DEBHELPER#
exit 0

14
debian/control vendored Normal file
View File

@ -0,0 +1,14 @@
Source: passbook
Section: admin
Priority: optional
Maintainer: BeryJu.org <support@beryju.org>
Uploaders: Jens Langhammer <jens@beryju.org>, BeryJu.org <support@beryju.org>
Build-Depends: debhelper (>= 10), dh-systemd (>= 1.5), dh-exec, wget, dh-exec, python3 (>= 3.5) | python3.6 | python3.7
Standards-Version: 3.9.6
Package: passbook
Architecture: all
Recommends: mysql-server, redis-server
Pre-Depends: adduser, libldap2-dev, libsasl2-dev
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.

22
debian/copyright vendored Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019 BeryJu.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
debian/dirs vendored Normal file
View File

@ -0,0 +1,4 @@
etc/passbook/
etc/passbook/config.d/
var/log/passbook/
usr/share/passbook/

44
debian/etc/passbook/config.yml vendored Normal file
View File

@ -0,0 +1,44 @@
debug: false
http:
host: 0.0.0.0
port: 8000
secret_key_file: /etc/passbook/secret_key
log:
level:
console: INFO
file: DEBUG
file: /var/log/passbook/passbook.log
# Error reporting, disabled by default
# error_report_enabled: true
# Set this to the server's external address.
# This is used to generate external URLs
external_url: http://image.example.com
# This dictates how the Path is generated
# can be either of:
# - view_sha512_short
# - view_md5
# - view_sha256
# - view_sha512
default_return_view: view_sha256
# Set this to true if you only want to use external authentication
external_auth_only: false
# If this is true, images are automatically claimed if the windows user exists
# in django
auto_claim_enabled: true
# LDAP Authentication
# ldap:
# enabled: false
# server:
# uri: 'ldap://dc1.example.com'
# tls: false
# bind:
# dn: ''
# password: ''
# search_base: ''
# filter: '(sAMAccountName=%(user)s)'
# require_group: ''

2
debian/gbp.conf vendored Normal file
View File

@ -0,0 +1,2 @@
[buildpackage]
export-dir=../build-area

8
debian/install vendored Normal file
View File

@ -0,0 +1,8 @@
passbook /usr/share/passbook/
static /usr/share/passbook/
manage.py /usr/share/passbook/
passbook.sh /usr/share/passbook/
vendor /usr/share/passbook/
debian/etc/passbook /etc/
debian/templates/database.yml /usr/share/passbook/

0
debian/links vendored Normal file
View File

14
debian/passbook-worker.service vendored Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=passbook - Authentication Provider/Proxy (Background worker)
After=network.target
Requires=network.target
[Service]
User=passbook
Group=passbook
WorkingDirectory=/usr/share/passbook
Type=simple
ExecStart=/usr/share/passbook/passbook.sh worker
[Install]
WantedBy=multi-user.target

14
debian/passbook.service vendored Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=passbook - Authentication Provider/Proxy
After=network.target
Requires=network.target
[Service]
User=passbook
Group=passbook
WorkingDirectory=/usr/share/passbook
Type=simple
ExecStart=/usr/share/passbook/passbook.sh web
[Install]
WantedBy=multi-user.target

36
debian/postinst vendored Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
set -e
. /usr/share/debconf/confmodule
. /usr/share/dbconfig-common/dpkg/postinst.pgsql
# you can set the default database encoding to something else
dbc_pgsql_createdb_encoding="UTF8"
dbc_generate_include=template:/etc/passbook/config.d/database.yml
dbc_generate_include_args="-o template_infile=/usr/share/passbook/database.yml"
dbc_go passbook "$@"
if [ -z "`getent group passbook`" ]; then
addgroup --quiet --system passbook
fi
if [ -z "`getent passwd passbook`" ]; then
echo " * Creating user and group passbook..."
adduser --quiet --system --home /usr/share/passbook --shell /bin/false --ingroup passbook --disabled-password --disabled-login --gecos "passbook User" passbook >> /var/log/passbook/passbook.log 2>&1
fi
echo " * Updating binary packages (psycopg2)"
python3 -m pip install --target=/usr/share/passbook/vendor/ --no-cache-dir --upgrade --force-reinstall psycopg2 >> /var/log/passbook/passbook.log 2>&1
if [ ! -f '/etc/passbook/secret_key' ]; then
echo " * Generating Secret Key"
python3 -c 'import random; result = "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)]); print(result)' > /etc/passbook/secret_key 2> /dev/null
fi
chown -R passbook: /usr/share/passbook/
chown -R passbook: /etc/passbook/
chown -R passbook: /var/log/passbook/
chmod 440 /etc/passbook/secret_key
echo " * Running Database Migration"
/usr/share/passbook/passbook.sh migrate
echo " * A superuser can be created with this command '/usr/share/passbook/passbook.sh createsuperuser'"
echo " * You should probably also adjust your settings in '/etc/passbook/config.yml'"
#DEBHELPER#

24
debian/postrm vendored Normal file
View File

@ -0,0 +1,24 @@
#!/bin/sh
set -e
if [ -f /usr/share/debconf/confmodule ]; then
. /usr/share/debconf/confmodule
fi
if [ -f /usr/share/dbconfig-common/dpkg/postrm.pgsql ]; then
. /usr/share/dbconfig-common/dpkg/postrm.pgsql
dbc_go passbook "$@"
fi
if [ "$1" = "purge" ]; then
if which ucf >/dev/null 2>&1; then
ucf --purge /etc/passbook/config.d/database.yml
ucfr --purge passbook /etc/passbook/config.d/database.yml
fi
rm -rf /etc/passbook/
rm -rf /usr/share/passbook/
fi
#DEBHELPER#

10
debian/prerm vendored Normal file
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
. /usr/share/dbconfig-common/dpkg/prerm.pgsql
dbc_go passbook "$@"
#DEBHELPER#

27
debian/rules vendored Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/make -f
# Uncomment this to turn on verbose mode.
# export DH_VERBOSE=1
%:
dh $@ --with=systemd
build-arch:
python3 -m pip install setuptools
python3 -m pip install --target=vendor/ -r requirements.txt
override_dh_strip:
dh_strip --exclude=psycopg2
override_dh_shlibdeps:
dh_shlibdeps --exclude=psycopg2
override_dh_installinit:
dh_installinit --name=passbook
dh_installinit --name=passbook-worker
dh_systemd_enable --name=passbook
dh_systemd_enable --name=passbook-worker
dh_systemd_start
# override_dh_usrlocal to do nothing
override_dh_usrlocal:

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (native)

8
debian/templates/database.yml vendored Normal file
View File

@ -0,0 +1,8 @@
databases:
default:
engine: django.db.backends.postgresql
name: _DBC_DBNAME_
user: _DBC_DBUSER_
password: _DBC_DBPASS_
host: _DBC_DBSERVER_
port: _DBC_DBPORT_

View File

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

View File

@ -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: {{ .Release.Name }}-redis redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master"
# 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 }}
@ -50,6 +50,7 @@ data:
{{- range .Values.ingress.hosts }} {{- range .Values.ingress.hosts }}
- {{ . | quote }} - {{ . | quote }}
{{- end }} {{- end }}
- kubernetes-healthcheck-host
passbook: passbook:
sign_up: sign_up:
@ -105,10 +106,9 @@ data:
email: mail # or userPrincipalName email: mail # or userPrincipalName
user_attribute_map: user_attribute_map:
active_directory: active_directory:
sAMAccountName: username username: "%(sAMAccountName)s"
mail: email email: "%(mail)s"
given_name: first_name name: "%(displayName)"
name: last_name
# # Create new users in LDAP upon sign-up # # Create new users in LDAP upon sign-up
# create_users: true # create_users: true
# # Reset LDAP password when user reset their password # # Reset LDAP password when user reset their password
@ -131,6 +131,7 @@ data:
# List of python packages with provider types to load. # List of python packages with provider types to load.
types: types:
- passbook.saml_idp.processors.generic - passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.aws
- passbook.saml_idp.processors.gitlab - passbook.saml_idp.processors.gitlab
- passbook.saml_idp.processors.nextcloud - passbook.saml_idp.processors.nextcloud
- passbook.saml_idp.processors.salesforce - passbook.saml_idp.processors.salesforce

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: latest tag: 0.1.12-beta
nameOverride: "" nameOverride: ""

7
passbook.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# Check if this file is a symlink, if so, read real base dir
BASE_DIR=$(dirname $(readlink -f ${BASH_SOURCE[0]}))
cd $BASE_DIR
PYTHONPATH="${BASE_DIR}/vendor/" python3 manage.py $@

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -11,7 +11,7 @@ class UserSerializer(ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['is_superuser', 'username', 'first_name', 'last_name', 'email', 'date_joined', fields = ['is_superuser', 'username', 'name', 'email', 'date_joined',
'uuid'] 'uuid']

View File

@ -0,0 +1,17 @@
"""passbook administrative user forms"""
from django import forms
from passbook.core.models import User
class UserForm(forms.ModelForm):
"""Update User Details"""
class Meta:
model = User
fields = ['username', 'name', 'email', 'is_staff', 'is_active']
widgets = {
'name': forms.TextInput
}

View File

@ -0,0 +1,25 @@
"""passbook admin Middleware to impersonate users"""
from passbook.core.models import User
def impersonate(get_response):
"""Middleware to impersonate users"""
def middleware(request):
"""Middleware to impersonate users"""
# User is superuser and has __impersonate ID set
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = request.GET["__impersonate"]
# user wants to stop impersonation
elif "__unimpersonate" in request.GET and 'impersonate_id' in request.session:
del request.session['impersonate_id']
# Actually impersonate user
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(pk=request.session['impersonate_id'])
response = get_response(request)
return response
return middleware

View File

@ -0,0 +1,5 @@
"""passbook admin settings"""
MIDDLEWARE = [
'passbook.admin.middleware.impersonate',
]

View File

@ -12,7 +12,7 @@
<h1><span class="pficon-applications"></span> {% trans "Applications" %}</h1> <h1><span class="pficon-applications"></span> {% trans "Applications" %}</h1>
<span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span> <span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span>
<hr> <hr>
<a href="{% url 'passbook_admin:application-create' %}" class="btn btn-primary"> <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
<hr> <hr>
@ -21,6 +21,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Provider' %}</th> <th>{% trans 'Provider' %}</th>
<th>{% trans 'Provider Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -28,7 +29,8 @@
{% for application in object_list %} {% for application in object_list %}
<tr> <tr>
<td>{{ application.name }}</td> <td>{{ application.name }}</td>
<td>{{ application.provider }}</td> <td>{{ application.get_provider }}</td>
<td>{{ application.get_provider|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

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

View File

@ -21,7 +21,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:factor-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -40,7 +40,7 @@
{% for factor in object_list %} {% for factor in object_list %}
<tr> <tr>
<td>{{ factor.name }} ({{ factor.slug }})</td> <td>{{ factor.name }} ({{ factor.slug }})</td>
<td>{{ factor.type }}</td> <td>{{ factor|verbose_name }}</td>
<td>{{ factor.order }}</td> <td>{{ factor.order }}</td>
<td>{{ factor.enabled }}</td> <td>{{ factor.enabled }}</td>
<td> <td>

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

@ -12,7 +12,7 @@
<h1><span class="pficon-migration"></span> {% trans "Invitations" %}</h1> <h1><span class="pficon-migration"></span> {% trans "Invitations" %}</h1>
<span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span> <span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span>
<hr> <hr>
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary"> <a href="{% url 'passbook_admin:invitation-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
<hr> <hr>

View File

@ -54,7 +54,11 @@
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:providers' %}"> <a href="{% url 'passbook_admin:providers' %}">
<span class="pficon pficon-ok"></span>{{ provider_count }} {% if providers_without_application.exists %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: At least one Provider has no application assigned.' %}"></span> {{ provider_count }}
{% else %}
<span class="pficon pficon-ok"></span> {{ provider_count }}
{% endif %}
</a> </a>
</span> </span>
</p> </p>
@ -72,9 +76,13 @@
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:factors' %}"> {% if factor_count < 1 %}
<span class="pficon pficon-ok"></span>{{ factor_count }} <span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
</a> title="{% trans 'No Factors configured. No Users will be able to login.' %}"></span>
{{ factor_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ factor_count }}
{% endif %}
</span> </span>
</p> </p>
</div> </div>
@ -91,9 +99,13 @@
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:policies' %}"> {% if policies_without_attachment > 0 %}
<span class="pficon pficon-ok"></span>{{ policy_count }} <span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
</a> title="{% trans 'Policies without attachment exist.' %}"></span>
{{ policy_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ policy_count }}
{% endif %}
</span> </span>
</p> </p>
</div> </div>
@ -168,7 +180,12 @@
<p class="card-pf-aggregate-status-notifications"> <p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"> <span class="card-pf-aggregate-status-notification">
<a href="#"> <a href="#">
{% if worker_count < 1%}
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No workers connected. Policies will not work and you may expect other issues.' %}"></span> {{ worker_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ worker_count }} <span class="pficon pficon-ok"></span>{{ worker_count }}
{% endif %}
</a> </a>
</span> </span>
</p> </p>

View File

@ -20,7 +20,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -28,16 +28,24 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th></th>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for policy in object_list %} {% for policy in object_list %}
<tr> <tr {% if not policy.policymodel_set.exists %} class="warning" {% endif %}>
<th>
{% if not policy.policymodel_set.exists %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Policy is not assigned.' %}"></span>
{% else %}
<span class="pficon-ok" data-toggle="tooltip" data-placement="right" title="{% blocktrans with objects=policy.policymodel_set.all|join:', ' %}Assigned to objects {{ objects }}{% endblocktrans %}"></span>
{% endif %}
</th>
<td>{{ policy.name }}</td> <td>{{ policy.name }}</td>
<td>{{ policy|fieldtype }}</td> <td>{{ policy|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

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

@ -0,0 +1,52 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="fa fa-table"></span> {% trans "Property Mappings" %}</h1>
<span>{% trans "Property Mappings allow you expose provider-specific attributes." %}</span>
<hr>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for property_mapping in object_list %}
<tr>
<td>{{ property_mapping.name }} ({{ property_mapping.slug }})</td>
<td>{{ property_mapping|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:property-mapping-update' pk=property_mapping.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:property-mapping-delete' pk=property_mapping.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -21,7 +21,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -29,16 +29,24 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th></th>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Type' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for provider in object_list %} {% for provider in object_list %}
<tr> <tr {% if not provider.application %} class="warning" {% endif %}>
<th>
{% if not provider.application %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Provider has no application assigned.' %}"></span>
{% else %}
<span class="pficon-ok" data-toggle="tooltip" data-placement="right" title="{% blocktrans with app=provider.application %}Assigned to Application {{ app }}{% endblocktrans %}"></span>
{% endif %}
</th>
<td>{{ provider.name }}</td> <td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td> <td>{{ provider|verbose_name }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -17,7 +17,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li> href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -11,8 +11,7 @@
<thead> <thead>
<tr> <tr>
<th>{% trans 'Username' %}</th> <th>{% trans 'Username' %}</th>
<th>{% trans 'First Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Last Name' %}</th>
<th>{% trans 'Active' %}</th> <th>{% trans 'Active' %}</th>
<th>{% trans 'Last Login' %}</th> <th>{% trans 'Last Login' %}</th>
<th></th> <th></th>
@ -22,8 +21,7 @@
{% for user in object_list %} {% for user in object_list %}
<tr> <tr>
<td>{{ user.username }}</td> <td>{{ user.username }}</td>
<td>{{ user.first_name|default:'-' }}</td> <td>{{ user.name|default:'-' }}</td>
<td>{{ user.last_name|default:'-' }}</td>
<td>{{ user.is_active }}</td> <td>{{ user.is_active }}</td>
<td>{{ user.last_login }}</td> <td>{{ user.last_login }}</td>
<td> <td>
@ -33,6 +31,8 @@
href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a> href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_core:overview' %}?__impersonate={{ user.pk }}">{% trans 'Impersonate' %}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -1,11 +1,12 @@
{% extends "generic/form.html" %} {% extends "generic/form.html" %}
{% load utils %}
{% load i18n %} {% load i18n %}
{% block above_form %} {% block above_form %}
<h1>{% trans 'Create' %}</h1> <h1>{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %} {% block action %}
{% trans 'Create' %} {% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}
{% endblock %} {% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "generic/create.html" %}
{% load i18n %}
{% block title %}
{% blocktrans with type=request.GET.type %}Create {{ type }}{% endblocktrans %}
{% endblock %}
{% block above_form %}
<h1>{% blocktrans with type=request.GET.type %}Create {{ type }}{% endblocktrans %}</h1>
{% endblock %}

View File

@ -2,6 +2,21 @@
{% load i18n %} {% load i18n %}
{% load utils %} {% load utils %}
{% load static %}
{% block head %}
{{ block.super }}
{{ 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 %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@ -14,5 +29,12 @@
<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 %}
{% block scripts %}
{{ block.super }}
{{ form.media.js }}
{% endblock %}

View File

@ -1,11 +1,12 @@
{% extends "generic/form.html" %} {% extends "generic/form.html" %}
{% load utils %}
{% load i18n %} {% load i18n %}
{% block above_form %} {% block above_form %}
<h1>{% trans 'Update' %}</h1> <h1>{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %} {% block action %}
{% trans 'Update' %} {% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}
{% endblock %} {% endblock %}

View File

@ -2,8 +2,8 @@
from django.urls import include, path from django.urls import include, path
from passbook.admin.views import (applications, audit, factors, groups, from passbook.admin.views import (applications, audit, factors, groups,
invitations, overview, policy, providers, invitations, overview, policy,
sources, users) property_mapping, providers, sources, users)
urlpatterns = [ urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'), path('', overview.AdministrationOverviewView.as_view(), name='overview'),
@ -43,6 +43,15 @@ urlpatterns = [
factors.FactorUpdateView.as_view(), name='factor-update'), factors.FactorUpdateView.as_view(), name='factor-update'),
path('factors/<uuid:pk>/delete/', path('factors/<uuid:pk>/delete/',
factors.FactorDeleteView.as_view(), name='factor-delete'), factors.FactorDeleteView.as_view(), name='factor-delete'),
# Factors
path('property-mappings/', property_mapping.PropertyMappingListView.as_view(),
name='property-mappings'),
path('property-mappings/create/',
property_mapping.PropertyMappingCreateView.as_view(), name='property-mapping-create'),
path('property-mappings/<uuid:pk>/update/',
property_mapping.PropertyMappingUpdateView.as_view(), name='property-mapping-update'),
path('property-mappings/<uuid:pk>/delete/',
property_mapping.PropertyMappingDeleteView.as_view(), name='property-mapping-delete'),
# Invitations # Invitations
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'), path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
path('invitations/create/', path('invitations/create/',
@ -58,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

@ -14,6 +14,7 @@ class ApplicationListView(AdminRequiredMixin, ListView):
"""Show list of all applications""" """Show list of all applications"""
model = Application model = Application
ordering = 'name'
template_name = 'administration/application/list.html' template_name = 'administration/application/list.html'
def get_queryset(self): def get_queryset(self):
@ -29,6 +30,10 @@ class ApplicationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView)
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully created Application') success_message = _('Successfully created Application')
def get_context_data(self, **kwargs):
kwargs['type'] = 'Application'
return super().get_context_data(**kwargs)
class ApplicationUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): class ApplicationUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update application""" """Update application"""

View File

@ -34,7 +34,7 @@ class FactorListView(AdminRequiredMixin, ListView):
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Factor""" """Create new Factor"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully created Factor') success_message = _('Successfully created Factor')

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

@ -27,6 +27,10 @@ class InvitationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
success_message = _('Successfully created Invitation') success_message = _('Successfully created Invitation')
form_class = InvitationForm form_class = InvitationForm
def get_context_data(self, **kwargs):
kwargs['type'] = 'Invitation'
return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
obj = form.save(commit=False) obj = form.save(commit=False)
obj.created_by = self.request.user obj.created_by = self.request.user

View File

@ -23,4 +23,6 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs['invitation_count'] = len(Invitation.objects.all()) kwargs['invitation_count'] = len(Invitation.objects.all())
kwargs['version'] = __version__ kwargs['version'] = __version__
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5)) kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -11,6 +11,7 @@ from django.views.generic.detail import DetailView
from passbook.admin.forms.policies import PolicyTestForm from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.core.policies import PolicyEngine
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
@ -32,7 +33,7 @@ class PolicyListView(AdminRequiredMixin, ListView):
class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Policy""" """Create new Policy"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully created Policy') success_message = _('Successfully created Policy')
@ -100,7 +101,9 @@ class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
def form_valid(self, form): def form_valid(self, form):
policy = self.get_object() policy = self.get_object()
user = form.cleaned_data.get('user') user = form.cleaned_data.get('user')
result = policy.passes(user) policy_engine = PolicyEngine([policy])
policy_engine.for_user(user).with_request(self.request).build()
result = policy_engine.passing
if result: if result:
messages.success(self.request, _('User successfully passed policy.')) messages.success(self.request, _('User successfully passed policy.'))
else: else:

View File

@ -0,0 +1,90 @@
"""passbook PropertyMapping administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
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.core.models import PropertyMapping
from passbook.lib.utils.reflection import path_to_class
def all_subclasses(cls):
"""Recursively return all subclassess of cls"""
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
class PropertyMappingListView(AdminRequiredMixin, ListView):
"""Show list of all property_mappings"""
model = PropertyMapping
template_name = 'administration/property_mapping/list.html'
ordering = 'name'
def get_context_data(self, **kwargs):
kwargs['types'] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new PropertyMapping"""
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully created Property Mapping')
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
property_mapping_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type)
kwargs['type'] = model._meta.verbose_name
return kwargs
def get_form_class(self):
property_mapping_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type)
if not model:
raise Http404
return path_to_class(model.form)
class PropertyMappingUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update property_mapping"""
model = PropertyMapping
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully updated Property Mapping')
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PropertyMappingDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete property_mapping"""
model = PropertyMapping
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully deleted Property Mapping')
def get_object(self, queryset=None):
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -29,7 +29,7 @@ class ProviderListView(AdminRequiredMixin, ListView):
class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Provider""" """Create new Provider"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully created Provider') success_message = _('Successfully created Provider')

View File

@ -34,7 +34,7 @@ class SourceListView(AdminRequiredMixin, ListView):
class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView): class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Source""" """Create new Source"""
template_name = 'generic/create_inheritance.html' template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully created Source') success_message = _('Successfully created Source')

View File

@ -7,8 +7,8 @@ from django.utils.translation import ugettext as _
from django.views import View from django.views import View
from django.views.generic import DeleteView, ListView, UpdateView from django.views.generic import DeleteView, ListView, UpdateView
from passbook.admin.forms.users import UserForm
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.users import UserDetailForm
from passbook.core.models import Nonce, User from passbook.core.models import Nonce, User
@ -23,7 +23,7 @@ class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update user""" """Update user"""
model = User model = User
form_class = UserDetailForm form_class = UserForm
template_name = 'generic/update.html' template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy('passbook_admin:users')

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -14,8 +14,8 @@ class OpenIDUserInfoView(ScopedResourceMixin, View):
payload = { payload = {
'sub': request.user.uuid.int, 'sub': request.user.uuid.int,
'name': request.user.get_full_name(), 'name': request.user.get_full_name(),
'given_name': request.user.first_name, 'given_name': request.user.name,
'family_name': request.user.last_name, 'family_name': '',
'preferred_username': request.user.username, 'preferred_username': request.user.username,
'email': request.user.email, 'email': request.user.email,
} }

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -0,0 +1,16 @@
# Generated by Django 2.1.7 on 2019-03-08 14:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20190221_1240'),
]
operations = [
migrations.DeleteModel(
name='LoginAttempt',
),
]

View File

@ -1,5 +1,4 @@
"""passbook audit models""" """passbook audit models"""
from datetime import timedelta
from logging import getLogger from logging import getLogger
from django.conf import settings from django.conf import settings
@ -7,11 +6,10 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ipware import get_client_ip from ipware import get_client_ip
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import UUIDModel
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -75,43 +73,3 @@ class AuditEntry(UUIDModel):
verbose_name = _('Audit Entry') verbose_name = _('Audit Entry')
verbose_name_plural = _('Audit Entries') verbose_name_plural = _('Audit Entries')
class LoginAttempt(CreatedUpdatedModel):
"""Track failed login-attempts"""
target_uid = models.CharField(max_length=254)
request_ip = models.GenericIPAddressField()
attempts = models.IntegerField(default=1)
@staticmethod
def attempt(target_uid, request):
"""Helper function to create attempt or count up existing one"""
if not target_uid:
return
client_ip, _ = get_client_ip(request)
# Since we can only use 254 chars for target_uid, truncate target_uid.
target_uid = target_uid[:254]
time_threshold = timezone.now() - timedelta(minutes=10)
existing_attempts = LoginAttempt.objects.filter(
target_uid=target_uid,
request_ip=client_ip,
last_updated__gt=time_threshold).order_by('created')
if existing_attempts.exists():
attempt = existing_attempts.first()
attempt.attempts += 1
attempt.save()
LOGGER.debug("Increased attempts on %s", attempt)
else:
attempt = LoginAttempt.objects.create(
target_uid=target_uid,
request_ip=client_ip)
LOGGER.debug("Created new attempt %s", attempt)
def __str__(self):
return "LoginAttempt to %s from %s (x%d)" % (self.target_uid,
self.request_ip, self.attempts)
class Meta:
unique_together = (('target_uid', 'request_ip', 'created'),)

View File

@ -1 +0,0 @@
django-ipware

View File

@ -1,9 +1,8 @@
"""passbook audit signal listener""" """passbook audit signal listener"""
from django.contrib.auth.signals import (user_logged_in, user_logged_out, from django.contrib.auth.signals import user_logged_in, user_logged_out
user_login_failed)
from django.dispatch import receiver from django.dispatch import receiver
from passbook.audit.models import AuditEntry, LoginAttempt from passbook.audit.models import AuditEntry
from passbook.core.signals import (invitation_created, invitation_used, from passbook.core.signals import (invitation_created, invitation_used,
user_signed_up) user_signed_up)
@ -34,8 +33,3 @@ def on_invitation_used(sender, request, invitation, **kwargs):
"""Log Invitation usage""" """Log Invitation usage"""
AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request, AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request,
invitation_uuid=invitation.uuid.hex) invitation_uuid=invitation.uuid.hex)
@receiver(user_login_failed)
def on_user_login_failed(sender, request, credentials, **kwargs):
"""Log failed login attempt"""
LoginAttempt.attempt(target_uid=credentials.get('username'), request=request)

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -13,3 +13,10 @@ class CaptchaFactor(FormView, AuthenticationFactor):
def form_valid(self, form): def form_valid(self, form):
return self.authenticator.user_ok() return self.authenticator.user_ok()
def get_form(self, form_class=None):
form = CaptchaForm(**self.get_form_kwargs())
form.fields['captcha'].public_key = '6Lfi1w8TAAAAAELH-YiWp0OFItmMzvjGmw2xkvUN'
form.fields['captcha'].private_key = '6Lfi1w8TAAAAAMQI3f86tGMvd1QkcqqVQyBWI23D'
form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key
return form

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.0.10-alpha' __version__ = '0.1.12-beta'

View File

@ -1,11 +1,13 @@
"""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 from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
@ -13,11 +15,42 @@ from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.view import AuthenticationView from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import PasswordFactorForm 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.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"""
@ -32,7 +65,16 @@ class PasswordFactor(FormView, AuthenticationFactor):
if 'password-forgotten' in request.GET: if 'password-forgotten' in request.GET:
nonce = Nonce.objects.create(user=self.pending_user) nonce = Nonce.objects.create(user=self.pending_user)
LOGGER.debug("DEBUG %s", str(nonce.uuid)) LOGGER.debug("DEBUG %s", str(nonce.uuid))
# TODO: Send email to user # Send mail to user
send_email.delay(self.pending_user.email, _('Forgotten password'),
'email/account_password_reset.html', {
'url': self.request.build_absolute_uri(
reverse('passbook_core:auth-password-reset',
kwargs={
'nonce': nonce.uuid
})
)
})
self.authenticator.cleanup() self.authenticator.cleanup()
messages.success(request, _('Check your E-Mails for a password reset link.')) messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login') return redirect('passbook_core:auth-login')
@ -47,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

@ -4,15 +4,23 @@ from logging import getLogger
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from passbook.core.models import Factor, User from passbook.core.models import Factor, User
from passbook.core.policies import PolicyEngine
from passbook.core.views.utils import PermissionDeniedView from passbook.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute from passbook.lib.utils.urls import is_url_absolute
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
def _redirect_with_qs(view, get_query_set=None):
"""Wrapper to redirect whilst keeping GET Parameters"""
target = reverse(view)
if get_query_set:
target += '?' + urlencode({key: value for key, value in get_query_set.items()})
return redirect(target)
class AuthenticationView(UserPassesTestMixin, View): class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator""" """Wizard-like Multi-factor authenticator"""
@ -31,33 +39,41 @@ class AuthenticationView(UserPassesTestMixin, View):
# Allow only not authenticated users to login # Allow only not authenticated users to login
def test_func(self): def test_func(self):
return self.request.user.is_authenticated is False return AuthenticationView.SESSION_PENDING_USER in self.request.session
def handle_no_permission(self): def handle_no_permission(self):
# Function from UserPassesTestMixin # Function from UserPassesTestMixin
if 'next' in self.request.GET: if 'next' in self.request.GET:
return redirect(self.request.GET.get('next')) return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview')) if self.request.user.is_authenticated:
return _redirect_with_qs('passbook_core:overview', self.request.GET)
return _redirect_with_qs('passbook_core:auth-login', self.request.GET)
def get_pending_factors(self):
"""Loading pending factors from Database or load from session variable"""
# Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS]
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
pending_factors = []
for factor in _all_factors:
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(self.pending_user).with_request(self.request).build()
if policy_engine.passing:
pending_factors.append((factor.uuid.hex, factor.type))
return pending_factors
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
# Extract pending user from session (only remember uid) # Extract pending user from session (only remember uid)
if AuthenticationView.SESSION_PENDING_USER in request.session: self.pending_user = get_object_or_404(
self.pending_user = get_object_or_404( User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER]) self.pending_factors = self.get_pending_factors()
else:
# No Pending user, redirect to login screen
return redirect(reverse('passbook_core:auth-login'))
# Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
else:
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
self.pending_factors = []
for factor in _all_factors:
if factor.passes(self.pending_user):
self.pending_factors.append((factor.uuid.hex, factor.type))
# Read and instantiate factor from session # Read and instantiate factor from session
factor_uuid, factor_class = None, None factor_uuid, factor_class = None, None
if AuthenticationView.SESSION_FACTOR not in request.session: if AuthenticationView.SESSION_FACTOR not in request.session:
@ -97,12 +113,12 @@ class AuthenticationView(UserPassesTestMixin, View):
next_factor = None next_factor = None
if self.pending_factors: if self.pending_factors:
next_factor = self.pending_factors.pop() next_factor = self.pending_factors.pop()
# Save updated pening_factor list to session
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \ self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
self.pending_factors self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor is %s", next_factor) LOGGER.debug("Rendering Factor is %s", next_factor)
# return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor})) return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
return redirect(reverse('passbook_core:auth-process'))
# User passed all factors # User passed all factors
LOGGER.debug("User passed all factors, logging in") LOGGER.debug("User passed all factors, logging in")
return self._user_passed() return self._user_passed()
@ -112,20 +128,19 @@ class AuthenticationView(UserPassesTestMixin, View):
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
self.cleanup() self.cleanup()
return redirect(reverse('passbook_core:auth-denied')) return _redirect_with_qs('passbook_core:auth-denied', self.request.GET)
def _user_passed(self): def _user_passed(self):
"""User Successfully passed all factors""" """User Successfully passed all factors"""
# user = authenticate(request=self.request, )
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND] backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend) login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in user %s", self.pending_user) LOGGER.debug("Logged in user %s", self.pending_user)
# Cleanup # Cleanup
self.cleanup() self.cleanup()
next_param = self.request.GET.get('next', None) next_param = self.request.GET.get('next', None)
if next_param and is_url_absolute(next_param): if next_param and not is_url_absolute(next_param):
return redirect(next_param) return redirect(next_param)
return redirect(reverse('passbook_core:overview')) return _redirect_with_qs('passbook_core:overview')
def cleanup(self): def cleanup(self):
"""Remove temporary data from session""" """Remove temporary data from session"""

View File

@ -5,9 +5,8 @@ import os
import celery import celery
from django.conf import settings from django.conf import settings
from raven import Client
# from raven import Client from raven.contrib.celery import register_logger_signal, register_signal
# from raven.contrib.celery import register_logger_signal, register_signal
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")
@ -18,16 +17,17 @@ LOGGER = logging.getLogger(__name__)
class Celery(celery.Celery): class Celery(celery.Celery):
"""Custom Celery class with Raven configured""" """Custom Celery class with Raven configured"""
# def on_configure(self): # pylint: disable=method-hidden
# """Update raven client""" def on_configure(self):
# try: """Update raven client"""
# client = Client(settings.RAVEN_CONFIG.get('dsn')) try:
# # register a custom filter to filter out duplicate logs client = Client(settings.RAVEN_CONFIG.get('dsn'))
# register_logger_signal(client) # register a custom filter to filter out duplicate logs
# # hook into the Celery error handler register_logger_signal(client)
# register_signal(client) # hook into the Celery error handler
# except RecursionError: # This error happens when pdoc is running register_signal(client)
# pass except RecursionError: # This error happens when pdoc is running
pass
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -0,0 +1,10 @@
"""passbook core exceptions"""
class PasswordPolicyInvalid(Exception):
"""Exception raised when a Password Policy fails"""
messages = []
def __init__(self, *messages):
super().__init__()
self.messages = messages

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

@ -38,10 +38,8 @@ class SignUpForm(forms.Form):
"""SignUp Form""" """SignUp Form"""
title = _('Sign Up') title = _('Sign Up')
first_name = forms.CharField(label=_('First Name'), name = forms.CharField(label=_('Name'),
widget=forms.TextInput(attrs={'placeholder': _('First Name')})) widget=forms.TextInput(attrs={'placeholder': _('Name')}))
last_name = forms.CharField(label=_('Last Name'),
widget=forms.TextInput(attrs={'placeholder': _('Last Name')}))
username = forms.CharField(label=_('Username'), username = forms.CharField(label=_('Username'),
widget=forms.TextInput(attrs={'placeholder': _('Username')})) widget=forms.TextInput(attrs={'placeholder': _('Username')}))
email = forms.EmailField(label=_('E-Mail'), email = forms.EmailField(label=_('E-Mail'),
@ -83,12 +81,14 @@ class SignUpForm(forms.Form):
password_repeat = self.cleaned_data.get('password_repeat') password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat: if password != password_repeat:
raise ValidationError(_("Passwords don't match")) raise ValidationError(_("Passwords don't match"))
# TODO: Password policy? Via Plugin? via Policy?
# return check_password(self)
return self.cleaned_data.get('password_repeat') return self.cleaned_data.get('password_repeat')
class PasswordFactorForm(forms.Form): class PasswordFactorForm(forms.Form):
"""Password authentication form""" """Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')})) password = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': _('Password'),
'autofocus': 'autofocus',
'autocomplete': 'current-password'
}))

View File

@ -1,10 +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.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"""
@ -15,6 +25,9 @@ class PasswordFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'backends': FilteredSelectMultiple(_('backends'), False,
choices=get_authentication_backends())
} }
class DummyFactorForm(forms.ModelForm): class DummyFactorForm(forms.ModelForm):
@ -27,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

@ -3,7 +3,9 @@
from django import forms 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, WebhookPolicy from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ] GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
@ -50,3 +52,36 @@ class DebugPolicyForm(forms.ModelForm):
labels = { labels = {
'result': _('Allow user') 'result': _('Allow user')
} }
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + ['group', ]
widgets = {
'name': forms.TextInput(),
}
class PasswordPolicyForm(forms.ModelForm):
"""PasswordPolicy Form"""
class Meta:
model = PasswordPolicy
fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase',
'amount_symbols', 'length_min', 'symbol_charset',
'error_message']
widgets = {
'name': forms.TextInput(),
'symbol_charset': forms.TextInput(),
'error_message': forms.TextInput(),
}
labels = {
'amount_uppercase': _('Minimum amount of Uppercase Characters'),
'amount_lowercase': _('Minimum amount of Lowercase Characters'),
'amount_symbols': _('Minimum amount of Symbols Characters'),
'length_min': _('Minimum Length'),
}

View File

@ -13,16 +13,23 @@ class UserDetailForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'first_name', 'last_name', 'email'] fields = ['username', 'name', 'email']
widgets = {
'name': forms.TextInput
}
class PasswordChangeForm(forms.Form): class PasswordChangeForm(forms.Form):
"""Form to update password""" """Form to update password"""
password = forms.CharField(label=_('Password'), password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput(attrs={'placeholder': _('New Password')})) widget=forms.PasswordInput(attrs={
'placeholder': _('New Password'),
'autocomplete': 'new-password'
}))
password_repeat = forms.CharField(label=_('Repeat Password'), password_repeat = forms.CharField(label=_('Repeat Password'),
widget=forms.PasswordInput(attrs={ widget=forms.PasswordInput(attrs={
'placeholder': _('Repeat Password') 'placeholder': _('Repeat Password'),
'autocomplete': 'new-password'
})) }))
def clean_password_repeat(self): def clean_password_repeat(self):
@ -31,5 +38,4 @@ class PasswordChangeForm(forms.Form):
password_repeat = self.cleaned_data.get('password_repeat') password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat: if password != password_repeat:
raise ValidationError(_("Passwords don't match")) raise ValidationError(_("Passwords don't match"))
# TODO: Password policy check
return self.cleaned_data.get('password_repeat') return self.cleaned_data.get('password_repeat')

View File

@ -0,0 +1,45 @@
"""passbook import_users management command"""
from csv import DictReader
from logging import getLogger
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from passbook.core.models import User
LOGGER = getLogger(__name__)
class Command(BaseCommand):
"""Import users from CSV file"""
def add_arguments(self, parser):
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Create Users from CSV file"""
for file in options.get('file'):
with open(file, 'r') as _file:
reader = DictReader(_file)
for user in reader:
LOGGER.debug('User %s', user.get('username'))
try:
# only import users with valid email addresses
if user.get('email'):
validator = EmailValidator()
validator(user.get('email'))
# use combination of username and email to check for existing user
if User.objects.filter(
username=user.get('username'),
email=user.get('email')).exists():
LOGGER.debug('User %s exists already, skipping', user.get('username'))
# Create user
User.objects.create(
username=user.get('username'),
email=user.get('email'),
name=user.get('name'),
password=user.get('password'))
LOGGER.debug('Created User %s', user.get('username'))
except ValidationError as exc:
LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
continue

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,19 @@
# Generated by Django 2.1.7 on 2019-02-26 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0014_auto_20190226_0850'),
]
operations = [
migrations.AddField(
model_name='passwordpolicy',
name='error_message',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.1.7 on 2019-02-27 13:55
from django.db import migrations, models
def migrate_names(apps, schema_editor):
"""migrate first_name and last_name to name"""
User = apps.get_model("passbook_core", "User")
for user in User.objects.all():
user.name = '%s %s' % (user.first_name, user.last_name)
user.save()
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0015_passwordpolicy_error_message'),
]
operations = [
migrations.AddField(
model_name='user',
name='name',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.RunPython(migrate_names),
migrations.AlterField(
model_name='user',
name='name',
field=models.TextField(),
preserve_default=False,
),
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='user_field',
field=models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 2.1.7 on 2019-03-08 10:40
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0016_auto_20190227_1355'),
]
operations = [
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-03-08 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0017_propertymapping'),
]
operations = [
migrations.AddField(
model_name='provider',
name='property_mappings',
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
),
]

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

@ -4,10 +4,11 @@ from datetime import timedelta
from logging import getLogger from logging import getLogger
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
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
@ -30,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
@ -43,19 +44,24 @@ class User(AbstractUser):
"""Custom User model to allow easier adding o f user-based settings""" """Custom User model to allow easier adding o f user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False) uuid = models.UUIDField(default=uuid4, editable=False)
name = models.TextField()
sources = models.ManyToManyField('Source', through='UserSourceConnection') sources = models.ManyToManyField('Source', through='UserSourceConnection')
applications = models.ManyToManyField('Application') applications = models.ManyToManyField('Application')
groups = models.ManyToManyField('Group') groups = models.ManyToManyField('Group')
password_change_date = models.DateTimeField(auto_now_add=True) password_change_date = models.DateTimeField(auto_now_add=True)
def set_password(self, password): def set_password(self, password):
password_changed.send(sender=self, user=self, password=password) if self.pk:
password_changed.send(sender=self, user=self, password=password)
self.password_change_date = now() self.password_change_date = now()
return super().set_password(password) return super().set_password(password)
class Provider(models.Model): class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True)
objects = InheritanceManager() objects = InheritanceManager()
# This class defines no field for easier inheritance # This class defines no field for easier inheritance
@ -69,13 +75,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField('Policy', blank=True) policies = models.ManyToManyField('Policy', blank=True)
def passes(self, user: User) -> bool:
"""Return true if user passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
class Factor(PolicyModel): class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
@ -156,7 +155,13 @@ class Application(PolicyModel):
def user_is_authorized(self, user: User) -> bool: def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application""" """Check if user is authorized to use this application"""
from passbook.core.policies import PolicyEngine from passbook.core.policies import PolicyEngine
return PolicyEngine(self.policies.all()).for_user(user).result return PolicyEngine(self.policies.all()).for_user(user).build().result
def get_provider(self):
"""Get casted provider instance"""
if not self.provider:
return None
return Provider.objects.get_subclass(pk=self.provider.pk)
def __str__(self): def __str__(self):
return self.name return self.name
@ -222,7 +227,7 @@ class Policy(UUIDModel, CreatedUpdatedModel):
return self.name return self.name
return "%s action %s" % (self.name, self.action) return "%s action %s" % (self.name, self.action)
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
"""Check if user instance passes this policy""" """Check if user instance passes this policy"""
raise NotImplementedError() raise NotImplementedError()
@ -246,8 +251,7 @@ class FieldMatcherPolicy(Policy):
USER_FIELDS = ( USER_FIELDS = (
('username', _('Username'),), ('username', _('Username'),),
('first_name', _('First Name'),), ('name', _('Name'),),
('last_name', _('Last Name'),),
('email', _('E-Mail'),), ('email', _('E-Mail'),),
('is_staff', _('Is staff'),), ('is_staff', _('Is staff'),),
('is_active', _('Is active'),), ('is_active', _('Is active'),),
@ -267,7 +271,7 @@ class FieldMatcherPolicy(Policy):
description = "%s: %s" % (self.name, description) description = "%s: %s" % (self.name, description)
return description return description
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
"""Check if user instance passes this role""" """Check if user instance passes this role"""
if not hasattr(user, self.user_field): if not hasattr(user, self.user_field):
raise ValueError("Field does not exist") raise ValueError("Field does not exist")
@ -284,8 +288,9 @@ class FieldMatcherPolicy(Policy):
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP: if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value) pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value)) passes = bool(pattern.match(user_field_value))
if self.negate: if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
passes = not passes passes = user_field_value == self.value
LOGGER.debug("User got '%r'", passes) LOGGER.debug("User got '%r'", passes)
return passes return passes
@ -302,10 +307,11 @@ class PasswordPolicy(Policy):
amount_symbols = models.IntegerField(default=0) amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0) length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
form = 'passbook.core.forms.policies.PasswordPolicyForm' form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, user: User) -> bool: def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
# Only check if password is being set # Only check if password is being set
if not hasattr(user, '__password__'): if not hasattr(user, '__password__'):
return True return True
@ -320,6 +326,8 @@ class PasswordPolicy(Policy):
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols) filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password)) result = bool(re.compile(filter_regex).match(password))
LOGGER.debug("User got %r", result) LOGGER.debug("User got %r", result)
if not result:
return result, self.error_message
return result return result
class Meta: class Meta:
@ -378,13 +386,28 @@ class DebugPolicy(Policy):
wait = SystemRandom().randrange(self.wait_min, self.wait_max) wait = SystemRandom().randrange(self.wait_min, self.wait_max)
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait) LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
sleep(wait) sleep(wait)
return self.result return self.result, 'Debugging'
class Meta: class Meta:
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"""
@ -408,7 +431,7 @@ class Invitation(UUIDModel):
verbose_name_plural = _('Invitations') verbose_name_plural = _('Invitations')
class Nonce(UUIDModel): class Nonce(UUIDModel):
"""One-time link for password resets/signup-confirmations""" """One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration) expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE) user = models.ForeignKey('User', on_delete=models.CASCADE)
@ -420,3 +443,19 @@ class Nonce(UUIDModel):
verbose_name = _('Nonce') verbose_name = _('Nonce')
verbose_name_plural = _('Nonces') verbose_name_plural = _('Nonces')
class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
name = models.TextField()
form = ''
objects = InheritanceManager()
def __str__(self):
return "Property Mapping %s" % self.name
class Meta:
verbose_name = _('Property Mapping')
verbose_name_plural = _('Property Mappings')

View File

@ -2,6 +2,7 @@
from logging import getLogger from logging import getLogger
from celery import group from celery import group
from ipware import get_client_ip
from passbook.core.celery import CELERY_APP from passbook.core.celery import CELERY_APP
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
@ -17,32 +18,72 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
setattr(user_obj, key, value) setattr(user_obj, key, value)
LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name, LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
policy_obj.pk.hex, user_obj) policy_obj.pk.hex, user_obj)
return policy_obj.passes(user_obj) policy_result = policy_obj.passes(user_obj)
# Handle policy result correctly if result, message or just result
message = None
if isinstance(policy_result, (tuple, list)):
policy_result, message = policy_result
# Invert result if policy.negate is set
if policy_obj.negate:
policy_result = not policy_result
LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
return policy_obj.action, policy_result, message
class PolicyEngine: class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result""" """Orchestrate policy checking, launch tasks and return result"""
policies = None policies = None
_group = None _group = None
_request = None
_user = None
def __init__(self, policies): def __init__(self, policies):
self.policies = policies self.policies = policies
self._request = None
self._user = None
def for_user(self, user): def for_user(self, user):
"""Check policies for user""" """Check policies for user"""
self._user = user
return self
def with_request(self, request):
"""Set request"""
self._request = request
return self
def build(self):
"""Build task group"""
if not self._user:
raise ValueError("User not set.")
signatures = [] signatures = []
kwargs = { kwargs = {
'__password__': getattr(user, '__password__', None) '__password__': getattr(self._user, '__password__', None),
} }
if self._request:
kwargs['remote_ip'], _ = get_client_ip(self._request)
if not kwargs['remote_ip']:
kwargs['remote_ip'] = '255.255.255.255'
for policy in self.policies: for policy in self.policies:
signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs)) signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs))
self._group = group(signatures)() self._group = group(signatures)()
return self return self
@property @property
def result(self): def result(self):
"""Get policy-checking result""" """Get policy-checking result"""
for policy_result in self._group.get(): messages = []
if policy_result is False: for policy_action, policy_result, policy_message in self._group.get():
return False passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
return True (policy_action == Policy.ACTION_DENY and not policy_result)
LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing)
if policy_message:
messages.append(policy_message)
if not passing:
return False, messages
return True, messages
@property
def passing(self):
"""Only get true/false if user passes"""
return self.result[0]

View File

@ -1,12 +1,13 @@
django>=2.0 django>=2.0
django-model-utils django-model-utils
django-ipware
djangorestframework djangorestframework
PyYAML PyYAML
raven raven
markdown markdown
colorlog colorlog
celery celery
redis<3.0 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,8 +59,10 @@ 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',
'passbook.core.apps.PassbookCoreConfig', 'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig', 'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
@ -75,6 +76,8 @@ INSTALLED_APPS = [
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', 'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
'passbook.hibp_policy.apps.PassbookHIBPConfig', 'passbook.hibp_policy.apps.PassbookHIBPConfig',
'passbook.pretend.apps.PassbookPretendConfig', 'passbook.pretend.apps.PassbookPretendConfig',
'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig',
'passbook.suspicious_policy.apps.PassbookSuspiciousPolicyConfig',
] ]
# Message Tag fix for bootstrap CSS Classes # Message Tag fix for bootstrap CSS Classes
@ -103,6 +106,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
] ]
ROOT_URLCONF = 'passbook.core.urls' ROOT_URLCONF = 'passbook.core.urls'
@ -183,6 +187,14 @@ CELERY_TASK_DEFAULT_QUEUE = 'passbook'
CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis') CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis')
CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis') CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis')
# Raven settings
RAVEN_CONFIG = {
'dsn': ('https://55b5dd780bc14f4c96bba69b7a9abbcc:449af483bd0745'
'0d83be640d834e5458@sentry.services.beryju.org/8'),
'release': VERSION,
'environment': 'dev' if DEBUG else 'production',
}
# CherryPY settings # CherryPY settings
with CONFIG.cd('web'): with CONFIG.cd('web'):
CHERRYPY_SERVER = { CHERRYPY_SERVER = {
@ -291,6 +303,7 @@ TEST_OUTPUT_FILE_NAME = 'unittest.xml'
if any('test' in arg for arg in sys.argv): if any('test' in arg for arg in sys.argv):
LOGGING = None LOGGING = None
TEST = True TEST = True
CELERY_TASK_ALWAYS_EAGER = True
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS'] _DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
# Load subapps's INSTALLED_APPS # Load subapps's INSTALLED_APPS

View File

@ -1,12 +1,26 @@
"""passbook core signals""" """passbook core signals"""
from django.core.signals import Signal from django.core.signals import Signal
from django.dispatch import receiver
# from django.db.models.signals import post_save, pre_delete from passbook.core.exceptions import PasswordPolicyInvalid
# from django.dispatch import receiver
# from passbook.core.models import Invitation, User
user_signed_up = Signal(providing_args=['request', 'user']) user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation']) invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user']) invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password']) password_changed = Signal(providing_args=['user', 'password'])
@receiver(password_changed)
# pylint: disable=unused-argument
def password_policy_checker(sender, password, **kwargs):
"""Run password through all password policies which are applied to the user"""
from passbook.core.models import PasswordFactor
from passbook.core.policies import PolicyEngine
setattr(sender, '__password__', password)
_all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
for factor in _all_factors:
policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses())
policy_engine.for_user(sender).build()
passing, messages = policy_engine.result
if not passing:
raise PasswordPolicyInvalid(*messages)

View File

@ -0,0 +1,197 @@
.dynamic-array-widget .array-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.dynamic-array-widget .remove_sign {
width: 10px;
height: 2px;
background: #a41515;
border-radius: 1px;
}
.dynamic-array-widget .remove {
height: 15px;
display: flex;
align-items: center;
margin-left: 5px;
}
.dynamic-array-widget .remove:hover {
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

@ -16,3 +16,33 @@ const typeHandler = function (e) {
$source.on('input', typeHandler) // register for oninput $source.on('input', typeHandler) // register for oninput
$source.on('propertychange', typeHandler) // for IE8 $source.on('propertychange', typeHandler) // for IE8
window.addEventListener('load', function () {
function addRemoveEventListener(widgetElement) {
widgetElement.querySelectorAll('.array-remove').forEach(function (element) {
element.addEventListener('click', function () {
this.parentNode.parentNode.remove();
});
});
}
document.querySelectorAll('.dynamic-array-widget').forEach(function (widgetElement) {
addRemoveEventListener(widgetElement);
widgetElement.querySelector('.add-array-item').addEventListener('click', function () {
var first = widgetElement.querySelector('.array-item');
var newElement = first.cloneNode(true);
var id_parts = newElement.querySelector('input').getAttribute('id').split('_');
var id = id_parts.slice(0, -1).join('_') + '_' + String(parseInt(id_parts.slice(-1)[0]) + 1);
newElement.querySelector('input').setAttribute('id', id);
newElement.querySelector('input').value = '';
addRemoveEventListener(newElement);
first.parentElement.insertBefore(newElement, first.parentNode.lastChild);
});
});
});

17
passbook/core/tasks.py Normal file
View File

@ -0,0 +1,17 @@
"""passbook core tasks"""
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from passbook.core.celery import CELERY_APP
from passbook.lib.config import CONFIG
@CELERY_APP.task()
def send_email(to_address, subject, template, context):
"""Send Email to user(s)"""
html_content = render_to_string(template, context=context)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address])
msg.attach_alternative(html_content, "text/html")
msg.send()

View File

@ -4,37 +4,51 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8"> <head>
<title> <meta charset="UTF-8">
{% block title %} <meta name="viewport" content="width=device-width, initial-scale=1">
{% title %} <title>
{% endblock %} {% block title %}
</title> {% title %}
<link rel="icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
<style>
.login-pf {
background-attachment: fixed;
scroll-behavior: smooth;
}
</style>
{% block head %}
{% endblock %} {% endblock %}
</head> </title>
<body {% if is_login %} class="login-pf" {% endif %}> <link rel="icon" type="image/png" href="{% static 'img/logo.png' %}">
{% block body %} <link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
{% endblock %} <link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<script src="{% static 'js/jquery.min.js' %}"></script> <link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
<script src="{% static 'js/bootstrap.min.js' %}"></script> <link rel="stylesheet" type="text/css" href="{% static 'css/passbook.css' %}">
<script src="{% static 'js/patternfly.min.js' %}"></script> <style>
<script src="{% static 'js/passbook.js' %}"></script> .login-pf {
{% block scripts %} background-attachment: fixed;
{% endblock %} scroll-behavior: smooth;
<div class="modals"> background-size: cover;
{% include 'partials/about_modal.html' %} }
</div> </style>
</body> {% block head %}
{% endblock %}
</head>
<body {% if is_login %} class="login-pf" {% endif %}>
{% if 'impersonate_id' in request.session %}
<div class="experimental-pf-bar">
<span id="experimentalBar" class="experimental-pf-text">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</span>
</div>
{% endif %}
{% block body %}
{% endblock %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script>
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %}
{% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body>
</html> </html>

View File

@ -0,0 +1,84 @@
{% extends 'email/base.html' %}
{% load inline %}
{% load i18n %}
{% block pre_header %}
{% trans "We're thrilled to have you here! Get ready to dive into your new account." %}
{% endblock %}
{% block content %}
<!-- HERO -->
<tr>
<td bgcolor="#3625b7" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<tr>
<td bgcolor="#566572" align="center" valign="top"
style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #8F9BA3; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 32px; font-weight: 400; margin: 0; color: #E9ECEF;">{% trans 'Welcome!' %}
</h1>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
<table border="0" cellpadding="0" cellspacing="0" width="480">
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 20px 30px 40px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "We're excited to have you get started. First, you need to confirm your account. Just press the button below."%}
</p>
</td>
</tr>
<!-- BULLETPROOF BUTTON -->
<tr>
<td bgcolor="#566572" align="left">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#566572" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3625b7"><a
href="{{ url }}" target="_blank"
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3625b7; display: inline-block;">{% trans 'Confirm Account' %}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 0px 30px 0px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "If that doesn't work, copy and paste the following link in your browser:" %}</p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 20px 30px 20px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;"><a href="{{ url }}" target="_blank" style="color: #3625b7;">{{ url }}</a></p>
</td>
</tr>
<!-- COPY -->
<tr>
<td bgcolor="#566572" align="left"
style="padding: 0px 30px 20px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
<p style="margin: 0;">
{% trans "If you have any questions, just reply to this email—we're always happy to help out." %}
</p>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More