Compare commits

...

161 Commits

Author SHA1 Message Date
ae64024ef4 bump version: 0.1.17-beta -> 0.1.18-beta 2019-03-13 11:31:05 +01:00
e6571826cb adjust versions for client packages, auto build client-packages 2019-03-12 17:30:57 +01:00
c621e61978 fix included yaml file 2019-03-12 17:19:13 +01:00
3626fa4b98 add sentry client 2019-03-12 17:18:08 +01:00
01b0eb159a fix Server Error when downloading metadata 2019-03-12 17:08:53 +01:00
63aa48d981 bump version: 0.1.16-beta -> 0.1.17-beta 2019-03-12 15:55:18 +01:00
2e0ba05d55 prepare 0.1.17 2019-03-12 15:55:14 +01:00
b2ac57bb67 switch to vertical navigation 2019-03-12 13:35:09 +01:00
4c22e5c2c8 don't use celery heartbeat, use TCP keepalive instead 2019-03-12 13:34:54 +01:00
4a7b0ec8a9 remove Application.user_is_authorized 2019-03-12 10:56:01 +01:00
330118249e bump version: 0.1.15-beta -> 0.1.16-beta 2019-03-11 21:35:11 +01:00
8d4dabde02 finalize RabbitMQ replacement, update debian package, remove redis tgz 2019-03-11 21:35:06 +01:00
cf7323c41b bump version: 0.1.14-beta -> 0.1.15-beta 2019-03-11 21:01:18 +01:00
edd856df7d redis -> rabbitmq 2019-03-11 20:46:19 +01:00
5e35859db6 bump version: 0.1.13-beta -> 0.1.14-beta 2019-03-11 11:44:34 +01:00
acabb2df54 fix unittests 2019-03-11 11:44:12 +01:00
e6376a05f7 bump version: 0.1.12-beta -> 0.1.13-beta 2019-03-11 11:31:12 +01:00
1f45aff7ad prepare 0.1.13 2019-03-11 11:31:06 +01:00
e1f1f617b6 fix UserChangePasswordView not requiring Login 2019-03-11 11:25:59 +01:00
2690675dca allow custom email server for helm installs 2019-03-11 11:03:25 +01:00
7529b51358 Fix DoesNotExist error when running PolicyEngine against None user 2019-03-11 10:52:50 +01:00
c394066d99 bump version: 0.1.11-beta -> 0.1.12-beta 2019-03-11 09:51:00 +01:00
9c585032ef prepare 0.1.12-beta 2019-03-11 09:50:57 +01:00
d408031304 fix OAuth Authorization View not requiring authentication 2019-03-11 09:48:36 +01:00
c47bc11ec0 disable automatic k8s deployment for now 2019-03-11 09:47:06 +01:00
1deb094afe install updated helm release from local folder 2019-03-10 21:47:22 +01:00
501fed1922 rewrite PasswordFactor to use backends setting instead of trying all backends 2019-03-10 21:47:08 +01:00
ad8125ac1c bump version: 0.1.10-beta -> 0.1.11-beta 2019-03-10 19:56:30 +01:00
b42a551fb2 prepare 0.1.11 2019-03-10 19:56:27 +01:00
3256be23df Merge branch '23-groups' into 'master'
Resolve "Group Management"

Closes #23

See merge request BeryJu.org/passbook!9
2019-03-10 18:49:01 +00:00
f7c0c0146a add LDAP Group Membership Policy 2019-03-10 19:45:16 +01:00
e4baf8c21e Add Group Member policy 2019-03-10 19:32:18 +01:00
364f040b36 always use FilteredSelectMultiple for many-to-many fields 2019-03-10 18:34:09 +01:00
2b8c2b2346 use Django's Admin FilteredSelectMultiple for Group Membership 2019-03-10 18:06:06 +01:00
5f861189e4 Merge branch 'master' into 23-groups
# Conflicts:
#	passbook/admin/templates/administration/base.html
2019-03-10 17:13:29 +01:00
5e11b6687e automatically deploy after release 2019-03-10 17:08:33 +01:00
c4b429825d fix helm labels being on deployments and not pods 2019-03-10 16:39:41 +01:00
eebbae0677 bump version: 0.1.9-beta -> 0.1.10-beta 2019-03-10 15:54:50 +01:00
42b30f4507 prepare 0.1.10 release 2019-03-10 15:53:38 +01:00
0e425418df better show loading state when testing a policy 2019-03-10 15:46:49 +01:00
7fe0300b86 Fix button on policy test page 2019-03-10 15:36:49 +01:00
c012c6be5c fix k8s service routing http traffic to workers 2019-03-10 15:34:24 +01:00
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
250b6691d4 bump version: 0.0.9-alpha -> 0.0.10-alpha 2019-02-26 12:44:02 +01:00
e3b02a6e78 fix isort/pylint issues 2019-02-26 12:43:59 +01:00
e94ef34d8f bump version: 0.0.8-alpha -> 0.0.9-alpha 2019-02-26 12:35:28 +01:00
49e945307a Re-enable OTP Disable View 2019-02-26 12:35:24 +01:00
edfe0e5450 fix broken Docker build and helm package 2019-02-26 12:34:51 +01:00
06b65a7882 add unittests, woo 2019-02-26 10:57:05 +01:00
ff9bc8aa70 Automatically create PasswordFactor on initial setup closes #16 2019-02-26 09:54:51 +01:00
28da67abe6 Improve partially broken Delete Views, show success message on deletion 2019-02-26 09:49:42 +01:00
39d9fe9bf0 add passbook.pretend to use passbook in applications which don't support generic OAuth 2019-02-26 09:10:37 +01:00
750117b0fd Cleanup templates, handle OAuth Provider without application better 2019-02-26 09:09:19 +01:00
983462f80d user/ -> _/user/ to prevent duplicate URLs 2019-02-26 09:08:49 +01:00
4ae31d409b directly use paths instead of including oauth2_provider's 2019-02-26 09:08:22 +01:00
98b414f3e2 add SignUp Confirmation (required by default, can be disabled in invitations) closes #6 2019-02-25 21:03:24 +01:00
a0d42092e3 add Nonce (one-time links), add password reset function (missing e-mail verification), closes #7 2019-02-25 20:46:23 +01:00
f2569b6424 improve placeholder on login template 2019-02-25 19:43:33 +01:00
9d344d887c add more information to administrator Overview 2019-02-25 17:52:51 +01:00
7e9154a0ea bump version: 0.0.7-alpha -> 0.0.8-alpha 2019-02-25 17:39:39 +01:00
e0ef061771 fix pylint errors.... 2019-02-25 17:32:52 +01:00
b8694a7ade fix bandit error (SHA1 has to be used) 2019-02-25 17:23:42 +01:00
10d6a30f2c add experimental HaveIBeenPwned Password Policy 2019-02-25 17:21:56 +01:00
8c94aef6d0 add stub test so coverage doesn't crash 2019-02-25 17:21:06 +01:00
19bd3bfffb fix allauth imports 2019-02-25 17:20:53 +01:00
8611ac624c Make links on admin overview site actually useful 2019-02-25 17:11:52 +01:00
fa93b59a8c switch to toast notifications everywhere 2019-02-25 16:41:53 +01:00
8b66b40f0d move forgot password to PasswordFactor 2019-02-25 16:41:33 +01:00
c2756f15fc Correctly display action on Create/Update templates 2019-02-25 16:40:46 +01:00
408e205c5f add signal for password change, add field for password policies 2019-02-25 15:41:36 +01:00
5f3ab49535 fix bug when Empty username is given to LoginAttempt.attempt 2019-02-25 14:10:29 +01:00
33431ae013 improve OAuth Source Setup process, fix login template, closes #3 2019-02-25 14:10:10 +01:00
b40ac6dc5d more Icons cause everyone loves icons 2019-02-25 13:31:11 +01:00
244 changed files with 5319 additions and 1367 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.7-alpha current_version = 0.1.18-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,18 @@ 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:client-packages/allauth/setup.py]
[bumpversion:file:client-packages/sentry-auth-passbook/setup.py]
[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 +41,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

@ -1,122 +1,124 @@
# Global Variables # Global Variables
before_script: before_script:
- "python3 -m pip install -U virtualenv" - "python3 -m pip install -U virtualenv"
- "virtualenv env" - "virtualenv env"
- "source env/bin/activate" - "source env/bin/activate"
- "pip3 install -U -r requirements-dev.txt" - "pip3 install -U -r requirements-dev.txt"
stages: stages:
- test - test
- build - build
- docs - docs
- deploy
image: python:3.6 image: python:3.6
services: services:
- postgres:latest - postgres:latest
variables: 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 - /client-packages/allauth/.gitlab-ci.yml
isort: isort:
script: script:
- isort -c -sg env - isort -c -sg env
stage: test stage: test
migrations: migrations:
script: script:
- python manage.py migrate - python manage.py migrate
stage: test stage: test
prospector: prospector:
script: script:
- prospector - prospector
stage: test stage: test
pylint: pylint:
script: script:
- pylint passbook - pylint passbook
stage: test stage: test
coverage: coverage:
script: script:
- coverage run manage.py test - coverage run manage.py test
- coverage report - coverage report
stage: test stage: test
bandit: bandit:
script: script:
- bandit -r passbook - bandit -r passbook
stage: test stage: test
package-docker: package-docker:
image: image:
name: gcr.io/kaniko-project/executor:debug name: gcr.io/kaniko-project/executor:debug
entrypoint: [""] entrypoint: [""]
before_script: before_script:
- echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.7-alpha - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.18-beta
stage: build stage: build
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/
package-helm: package-helm:
stage: build stage: build
script: script:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- helm init --client-only - helm init --client-only
- helm package helm/passbook - helm package helm/passbook
- ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz - ./manage.py nexus_upload --method put --url $NEXUS_URL --auth $NEXUS_AUTH --repo helm *.tgz
only: 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: package-client-package-allauth:
# - apt update script:
# - apt install -y build-essential debhelper devscripts equivs python3 python3-pip - cd client-packages/allauth
# - cp debian/control-3.6 debian/control - python setup.py sdist
# - mk-build-deps debian/control - twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
# - apt install ./*build-deps*deb -f -y stage: build
# - "python3 -m pip install -U virtualenv" only:
# - "virtualenv env" refs:
# - "source env/bin/activate" - tags
# - "pip3 install -U -r requirements.txt -r requirements-dev.txt" - /^version/.*$/
# image: debian:buster changes:
# script: - client-packages/allauth/**
# - debuild -us -uc
# - cp ../passbook*.deb . package-client-package-sentry:
# - python manage.py nexus_upload script:
# artifacts: - cd client-packages/sentry-auth-passbook
# paths: - python setup.py sdist
# - passbook-python3.6*deb - twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
# expire_in: 2 days stage: build
# stage: build only:
# only: refs:
# - tags - tags
# - /^debian/.*$r - /^version/.*$/
changes:
- client-packages/sentry-auth-passbook/**
# docs: # docs:
# stage: docs # stage: docs
@ -138,3 +140,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

@ -7,6 +7,7 @@ ignore-paths:
- migrations - migrations
- docs - docs
- node_modules - node_modules
- client-packages
uses: uses:
- django - django

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

View File

@ -3,7 +3,7 @@ from setuptools import setup
setup( setup(
name='django-allauth-passbook', name='django-allauth-passbook',
version='1.0.0', version='0.1.18-beta',
description='passbook support for django-allauth', description='passbook support for django-allauth',
# long_description='\n'.join(read_simple('docs/index.md')[2:]), # long_description='\n'.join(read_simple('docs/index.md')[2:]),
long_description_content_type='text/markdown', long_description_content_type='text/markdown',

View File

@ -0,0 +1,5 @@
*.pyc
*.egg-info/
*.eggs
/dist
/build

View File

@ -0,0 +1,32 @@
sudo: false
language: python
services:
- memcached
- postgresql
- redis-server
python:
- '2.7'
cache:
directories:
- node_modules
- "$HOME/.cache/pip"
deploy:
provider: pypi
user: getsentry
password:
secure: kVmxKHkBWRLYyZme05p+WZSJmb8GjHV9uyuaSCVMRlqWCW+GXRB7P1xXR2jb9URTlNdcs56Ab/UrwzCbMFGC8LmwCeFVgIR/ltytVZG2FgXZPWaeA4dH25qK2oGWgzJ/xeiMpmuJqN9hRl25MX6jG7FZKvrrOkG7+8tpPd1yO+uYWZQbnebZMjcPBqEpn7CC0hR39GSoyVAbydpMe5hwENGQM26CepcicdrelfawItoUrXrkJzBHkIQQTO/xRSbCtRJOtzI5lwtv3GP0hcbOy5tI5dhG/93pLwZRc5+dZaCaP7oaVeOcBjN0zfINRQobt8d6h2Qgvd/YyFkGi0/xKn1zMmKIVLOG6VsYwEAUq8wNOsP4A/jdm4Y0J/1oEZStCkpaGpx85TYi4kq1hWQdyqaVJSPhh4Tk4roIaS2zOYQl+nIpbHqmJ4FJrg1il+TCdjBXobATQ1mKRBUrjD+RDzH/r4ogbd8+UwvvvevpqS2K+/wgT6UD0MzDInv9S29CUQvuFhPoqyJb5XRddHMRE9EEK/2Z8tFN91sDATnqfXHgwnvu00q/nKP5JnijBPzGmx7ydgUViIukklDrlPvo9BbRJz0Vr2vbAvMTrLMLCXqi5CwTm+v+iaOf/YaCziaG2vx0eVASYjpOLCedSgRZBubPM8z4E/HMXhChN7sVDWk=
on:
tags: true
distributions: sdist bdist_wheel
env:
global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
before_install:
- pip install codecov
install:
- make develop
script:
- PYFLAKES_NODOCTEST=1 flake8
- coverage run --source=. -m py.test tests
after_success:
- codecov

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016 Functional Software, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,3 @@
include setup.py package.json webpack.config.js README.rst MANIFEST.in LICENSE AUTHORS
recursive-include sentry_auth_supervisr/templates *
global-exclude *~

View File

@ -0,0 +1,26 @@
.PHONY: clean develop install-tests lint publish test
develop:
pip install "pip>=7"
pip install -e .
make install-tests
install-tests:
pip install .[tests]
lint:
@echo "--> Linting python"
flake8
@echo ""
test:
@echo "--> Running Python tests"
py.test tests || exit 1
@echo ""
publish:
python setup.py sdist bdist_wheel upload
clean:
rm -rf *.egg-info src/*.egg-info
rm -rf dist build

View File

@ -0,0 +1,55 @@
GitHub Auth for Sentry
======================
An SSO provider for Sentry which enables GitHub organization-restricted authentication.
Install
-------
::
$ pip install https://github.com/getsentry/sentry-auth-github/archive/master.zip
Setup
-----
Create a new application under your organization in GitHub. Enter the **Authorization
callback URL** as the prefix to your Sentry installation:
::
https://example.sentry.com
Once done, grab your API keys and drop them in your ``sentry.conf.py``:
.. code-block:: python
GITHUB_APP_ID = ""
GITHUB_API_SECRET = ""
Verified email addresses can optionally be required:
.. code-block:: python
GITHUB_REQUIRE_VERIFIED_EMAIL = True
Optionally you may also specify the domain (for GHE users):
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "api.git.example.com"
If Subdomain isolation is disabled in GHE:
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "git.example.com/api/v3"

View File

@ -0,0 +1,14 @@
from __future__ import absolute_import
# Run tests against sqlite for simplicity
import os
import os.path
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
os.environ.setdefault('DB', 'sqlite')
pytest_plugins = [
'sentry.utils.pytest'
]

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import
from sentry.auth import register
from .provider import PassbookOAuth2Provider
register('passbook', PassbookOAuth2Provider)

View File

@ -0,0 +1,45 @@
from __future__ import absolute_import, print_function
from requests.exceptions import RequestException
from sentry import http
from sentry.utils import json
from .constants import BASE_DOMAIN
class SupervisrApiError(Exception):
def __init__(self, message='', status=0):
super(SupervisrApiError, self).__init__(message)
self.status = status
class SupervisrClient(object):
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.http = http.build_session()
def _request(self, path, access_token):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
}
headers = {
'Authorization': 'Bearer {0}'.format(access_token),
}
try:
req = self.http.get('https://{0}/{1}'.format(BASE_DOMAIN, path.lstrip('/')),
params=params,
headers=headers,
)
except RequestException as e:
raise SupervisrApiError(unicode(e), status=getattr(e, 'status_code', 0))
if req.status_code < 200 or req.status_code >= 300:
raise SupervisrApiError(req.content, status=req.status_code)
return json.loads(req.content)
def get_user(self, access_token):
return self._request('/api/core/v1/accounts/me/?format=openid', access_token)

View File

@ -0,0 +1,14 @@
from __future__ import absolute_import, print_function
from django.conf import settings
CLIENT_ID = getattr(settings, 'PASSBOOK_APP_ID', None)
CLIENT_SECRET = getattr(settings, 'PASSBOOK_API_SECRET', None)
SCOPE = 'openid:userinfo'
BASE_DOMAIN = getattr(settings, 'PASSBOOK_BASE_DOMAIN', 'id.beryju.org')
ACCESS_TOKEN_URL = 'https://{0}/application/oauth/token/'.format(BASE_DOMAIN)
AUTHORIZE_URL = 'https://{0}/application/oauth/authorize/'.format(BASE_DOMAIN)

View File

@ -0,0 +1,62 @@
from __future__ import absolute_import, print_function
from sentry.auth.exceptions import IdentityNotValid
from sentry.auth.providers.oauth2 import (OAuth2Callback, OAuth2Login,
OAuth2Provider)
from .client import PassbookApiError, PassbookClient
from .constants import (ACCESS_TOKEN_URL, AUTHORIZE_URL, CLIENT_ID,
CLIENT_SECRET, SCOPE)
from .views import FetchUser, PassbookConfigureView
class PassbookOAuth2Provider(OAuth2Provider):
access_token_url = ACCESS_TOKEN_URL
authorize_url = AUTHORIZE_URL
name = 'Passbook'
client_id = CLIENT_ID
client_secret = CLIENT_SECRET
def __init__(self, **config):
super(PassbookOAuth2Provider, self).__init__(**config)
def get_configure_view(self):
return PassbookConfigureView.as_view()
def get_auth_pipeline(self):
return [
OAuth2Login(
authorize_url=self.authorize_url,
client_id=self.client_id,
scope=SCOPE,
),
OAuth2Callback(
access_token_url=self.access_token_url,
client_id=self.client_id,
client_secret=self.client_secret,
),
FetchUser(
client_id=self.client_id,
client_secret=self.client_secret,
),
]
def get_refresh_token_url(self):
return ACCESS_TOKEN_URL
def build_identity(self, state):
data = state['data']
user_data = state['user']
return {
'id': user_data['email'],
'email': user_data['email'],
'name': user_data['name'],
'data': self.get_oauth_data(data),
}
def build_config(self, state):
return {}
def refresh_identity(self, auth_identity):
client = PassbookClient(self.client_id, self.client_secret)
access_token = auth_identity.data['access_token']

View File

@ -0,0 +1,75 @@
from __future__ import absolute_import, print_function
from django import forms
from sentry.auth.view import AuthView, ConfigureView
from sentry.models import AuthIdentity
from .client import PassbookClient
def _get_name_from_email(email):
"""
Given an email return a capitalized name. Ex. john.smith@example.com would return John Smith.
"""
name = email.rsplit('@', 1)[0]
name = ' '.join([n_part.capitalize() for n_part in name.split('.')])
return name
class FetchUser(AuthView):
def __init__(self, client_id, client_secret, *args, **kwargs):
self.client = PassbookClient(client_id, client_secret)
super(FetchUser, self).__init__(*args, **kwargs)
def handle(self, request, helper):
access_token = helper.fetch_state('data')['access_token']
user = self.client.get_user(access_token)
# A user hasn't set their name in their Passbook profile so it isn't
# populated in the response
if not user.get('name'):
user['name'] = _get_name_from_email(user['email'])
helper.bind_state('user', user)
return helper.next_step()
class ConfirmEmailForm(forms.Form):
email = forms.EmailField(label='Email')
class ConfirmEmail(AuthView):
def handle(self, request, helper):
user = helper.fetch_state('user')
# TODO(dcramer): this isnt ideal, but our current flow doesnt really
# support this behavior;
try:
auth_identity = AuthIdentity.objects.select_related('user').get(
auth_provider=helper.auth_provider,
ident=user['id'],
)
except AuthIdentity.DoesNotExist:
pass
else:
user['email'] = auth_identity.user.email
if user.get('email'):
return helper.next_step()
form = ConfirmEmailForm(request.POST or None)
if form.is_valid():
user['email'] = form.cleaned_data['email']
helper.bind_state('user', user)
return helper.next_step()
return self.respond('sentry_auth_passbook/enter-email.html', {
'form': form,
})
class PassbookConfigureView(ConfigureView):
def dispatch(self, request, organization, auth_provider):
return self.render('sentry_auth_passbook/configure.html')

View File

@ -0,0 +1,12 @@
[wheel]
universal = 1
[pytest]
python_files = test*.py
addopts = --tb=native -p no:doctest
norecursedirs = bin dist docs htmlcov script hooks node_modules .* {args}
[flake8]
ignore = F999,E501,E128,E124,E402,W503,E731,C901
max-line-length = 100
exclude = .tox,.git,*/migrations/*,node_modules/*,docs/*

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
"""
sentry-auth-passbook
==================
:copyright: (c) 2016 Functional Software, Inc
"""
from setuptools import find_packages, setup
install_requires = [
'sentry>=7.0.0',
]
tests_require = [
'mock',
'flake8>=2.0,<2.1',
]
setup(
name='sentry-auth-passbook',
version='0.1.18-beta',
author='BeryJu.org',
author_email='support@beryju.org',
url='https://passbook.beryju.org',
description='passbook authentication provider for Sentry',
long_description=__doc__,
license='MIT',
packages=find_packages(exclude=['tests']),
zip_safe=False,
install_requires=install_requires,
tests_require=tests_require,
extras_require={'tests': tests_require},
include_package_data=True,
entry_points={
'sentry.apps': [
'auth_passbook = sentry_auth_passbook',
],
},
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
'Topic :: Software Development'
],
)

View File

@ -0,0 +1,6 @@
from sentry.testutils import TestCase
class GitHubOAuth2ProviderTest(TestCase):
def test_simple(self):
pass

View File

@ -0,0 +1,17 @@
from __future__ import absolute_import, print_function
import pytest
from sentry_auth_sentry.views import _get_name_from_email
expected_data = [
('john.smith@example.com', 'John Smith'),
('john@example.com', 'John'),
('XYZ-234=3523@example.com', 'Xyz-234=3523'),
('XYZ.1111@example.com', 'Xyz 1111'),
('JOHN@example.com', 'John'),
]
@pytest.mark.parametrize("email,expected_name", expected_data)
def test_get_name_from_email(email, expected_name):
assert _get_name_from_email(email) == expected_name

89
debian/changelog vendored Normal file
View File

@ -0,0 +1,89 @@
passbook (0.1.17) stable; urgency=medium
* bump version: 0.1.15-beta -> 0.1.16-beta
* remove Application.user_is_authorized
* don't use celery heartbeat, use TCP keepalive instead
* switch to vertical navigation
-- Jens Langhammer <jens.langhammer@beryju.org> Tue, 12 Mar 2019 14:54:27 +0000
passbook (0.1.16) stable; urgency=medium
* Replace redis with RabbitMQ
* updated debian package to suggest RabbitMQ
* update helm chart to require RabbitMQ
* fix invalid default config in debian package
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 10:28:36 +0000
passbook (0.1.14) stable; urgency=medium
* bump version: 0.1.11-beta -> 0.1.12-beta
* Fix DoesNotExist error when running PolicyEngine against None user
* allow custom email server for helm installs
* fix UserChangePasswordView not requiring Login
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 10:28:36 +0000
passbook (0.1.12) stable; urgency=medium
* bump version: 0.1.10-beta -> 0.1.11-beta
* rewrite PasswordFactor to use backends setting instead of trying all backends
* install updated helm release from local folder
* disable automatic k8s deployment for now
* fix OAuth Authorization View not requiring authentication
-- Jens Langhammer <jens.langhammer@beryju.org> Mon, 11 Mar 2019 08:50:29 +0000
passbook (0.1.11) stable; urgency=medium
* add group administration
* bump version: 0.1.9-beta -> 0.1.10-beta
* fix helm labels being on deployments and not pods
* automatically deploy after release
* use Django's Admin FilteredSelectMultiple for Group Membership
* always use FilteredSelectMultiple for many-to-many fields
* Add Group Member policy
* add LDAP Group Membership Policy
-- Jens Langhammer <jens.langhammer@beryju.org> Sun, 10 Mar 2019 18:55:31 +0000
passbook (0.1.10) stable; urgency=high
* bump version: 0.1.7-beta -> 0.1.8-beta
* consistently using PolicyEngine
* add more Verbosity to PolicyEngine, rewrite SAML Authorisation check
* slightly refactor Factor View, add more unittests
* add impersonation middleware, add to templates
* bump version: 0.1.8-beta -> 0.1.9-beta
* fix k8s service routing http traffic to workers
* Fix button on policy test page
* better show loading state when testing a policy
-- Jens Langhammer <jens.langhammer@beryju.org> Sun, 10 Mar 2019 14:52:40 +0000
passbook (0.1.7) stable; urgency=medium
* 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, rabbitmq-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/

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

@ -0,0 +1,77 @@
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
debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: guest:guest@localhost/passbook
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true
passbook:
sign_up:
# Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true
enabled: true
password_reset:
# Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
enabled: true
# Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
verification:
- email
# Text used in title, on login page and multiple other places
branding: passbook
login:
# Override URL used for logo
logo_url: null
# Override URL used for Background on Login page
bg_url: null
# Optionally add a subtext, placed below logo on the login page
subtext: null
footer:
links:
# Optionally add links to the footer on the login page
# - name: test
# href: https://test
# Specify which fields can be used to authenticate. Can be any combination of `username` and `email`
uid_fields:
- username
- email
session:
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
# Provider-specific settings
ldap:
# Which field from `uid_fields` maps to which LDAP Attribute
login_field_map:
username: sAMAccountName
email: mail # or userPrincipalName
user_attribute_map:
active_directory:
username: "%(sAMAccountName)s"
email: "%(mail)s"
name: "%(displayName)"
oauth_client:
# List of python packages with sources types to load.
types:
- passbook.oauth_client.source_types.discord
- passbook.oauth_client.source_types.facebook
- passbook.oauth_client.source_types.github
- passbook.oauth_client.source_types.google
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
saml_idp:
# List of python packages with provider types to load.
types:
- passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.aws
- passbook.saml_idp.processors.gitlab
- passbook.saml_idp.processors.nextcloud
- passbook.saml_idp.processors.salesforce
- passbook.saml_idp.processors.shibboleth
- passbook.saml_idp.processors.wordpress_orange

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.7-alpha" appVersion: "0.1.18-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: 1.0.0 version: "0.1.18-beta"
icon: https://passbook.beryju.org/images/logo.png icon: https://passbook.beryju.org/images/logo.png

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@ -22,7 +22,7 @@ data:
host: 127.0.0.1 host: 127.0.0.1
port: 514 port: 514
email: email:
host: localhost host: {{ .Values.config.email.host }}
port: 25 port: 25
user: '' user: ''
password: '' password: ''
@ -36,7 +36,7 @@ data:
debug: false debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
redis: {{ .Release.Name }}-redis rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }} error_report_enabled: {{ .Values.config.error_reporting }}
@ -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.18-beta
nameOverride: "" nameOverride: ""
@ -14,10 +14,16 @@ config:
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o # secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting # Enable error reporting
error_reporting: true error_reporting: true
email:
host: localhost
postgresql: postgresql:
postgresqlDatabase: passbook postgresqlDatabase: passbook
postgresqlPassword: foo postgresqlPassword: foo
rabbitmq:
rabbitmq:
password: foo
service: service:
type: ClusterIP type: ClusterIP
@ -31,7 +37,6 @@ ingress:
path: / path: /
hosts: hosts:
- passbook.k8s.local - passbook.k8s.local
- kubernetes-healthcheck-host
defaultHost: passbook.k8s.local defaultHost: passbook.k8s.local
tls: [] tls: []
# - secretName: chart-example-tls # - secretName: chart-example-tls

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.7-alpha' __version__ = '0.1.18-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.7-alpha' __version__ = '0.1.18-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

@ -9,33 +9,37 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% 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>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Provider' %}</th> <th>{% trans 'Provider' %}</th>
<th></th> <th>{% trans 'Provider Type' %}</th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
{% for application in object_list %} <tbody>
<tr> {% for application in object_list %}
<td>{{ application.name }}</td> <tr>
<td>{{ application.provider }}</td> <td>{{ application.name }}</td>
<td> <td>{{ application.get_provider }}</td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <td>{{ application.get_provider|verbose_name }}</td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> <td>
</td> <a class="btn btn-default btn-sm"
</tr> href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
{% endfor %} <a class="btn btn-default btn-sm"
</tbody> href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -8,80 +8,80 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Audit Log" %}</h1> <h1><span class="pficon-catalog"></span> {% trans "Audit Log" %}</h1>
<div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view"> <div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
{% for entry in object_list %} {% for entry in object_list %}
<div class="list-group-item"> <div class="list-group-item">
<div class="list-view-pf-main-info"> <div class="list-view-pf-main-info">
<div class="list-view-pf-left"> <div class="list-view-pf-left">
<span class="fa fa-plane list-view-pf-icon-sm"></span> <span class="fa fa-plane list-view-pf-icon-sm"></span>
</div>
<div class="list-view-pf-body">
<div class="list-view-pf-description">
<div class="list-group-item-heading">
{{ entry.action }}
</div>
<div class="list-group-item-text">
{{ entry.context }}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="fa fa-clock-o"></span>
<strong>{{ entry.created }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
</div>
</div>
</div> </div>
<div class="list-view-pf-body">
<div class="list-view-pf-description">
<div class="list-group-item-heading">
{{ entry.action }}
</div>
<div class="list-group-item-text">
{{ entry.context }}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="fa fa-clock-o"></span>
<strong>{{ entry.created }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
</div>
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
// Row Checkbox Selection // Row Checkbox Selection
$("#pf-list-standard input[type='checkbox']").change(function (e) { $("#pf-list-standard input[type='checkbox']").change(function (e) {
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
$(this).closest('.list-group-item').addClass("active"); $(this).closest('.list-group-item').addClass("active");
} else { } else {
$(this).closest('.list-group-item').removeClass("active"); $(this).closest('.list-group-item').removeClass("active");
} }
}); });
// toggle dropdown menu // toggle dropdown menu
$('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () { $('#pf-list-standard .list-view-pf-actions').on('show.bs.dropdown', function () {
var $this = $(this); var $this = $(this);
var $dropdown = $this.find('.dropdown'); var $dropdown = $this.find('.dropdown');
var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true); var space = $(window).height() - $dropdown[0].getBoundingClientRect().top - $this.find('.dropdown-menu').outerHeight(true);
$dropdown.toggleClass('dropup', space < 10); $dropdown.toggleClass('dropup', space < 10);
}); });
// allow users to select multiple list items with shift key // allow users to select multiple list items with shift key
$('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) { $('#pf-list-standard .list-group').on('click', '.list-view-pf-checkbox>input', function (event) {
var $list = $('.list-group'); var $list = $('.list-group');
var prevIndex = $list.data('preIndex'); var prevIndex = $list.data('preIndex');
var $listItems = $list.children('.list-group-item'); var $listItems = $list.children('.list-group-item');
var $currentItem = $(this).closest('.list-group-item'); var $currentItem = $(this).closest('.list-group-item');
if (event.shiftKey && prevIndex > -1 && this.checked) { if (event.shiftKey && prevIndex > -1 && this.checked) {
var currentIndex = $listItems.index($currentItem); var currentIndex = $listItems.index($currentItem);
var $selectScope = currentIndex - prevIndex > 0 var $selectScope = currentIndex - prevIndex > 0
? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack()) ? $currentItem.prevAll().not($listItems.eq(prevIndex).prevAll().addBack())
: $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack()); : $listItems.eq(prevIndex).prevAll().not($currentItem.prevAll().addBack());
$selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true); $selectScope.addClass('active').find('.list-view-pf-checkbox').children('input').prop('checked', true);
} }
$list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1); $list.data('preIndex', this.checked ? $listItems.index($currentItem) : -1);
}); });
}); });
</script> </script>
{% include 'partials/pagination.html' %} {% include 'partials/pagination.html' %}
</div> </div>
{% endblock %} {% endblock %}

View File

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

View File

@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Factors" %}</h1> <h1><span class="pficon-plugged"></span> {% trans "Factors" %}</h1>
<span>{% trans "Factors required for a user to successfully authenticate." %}</span> <span>{% trans "Factors required for a user to successfully authenticate." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
@ -20,7 +20,8 @@
</button> </button>
<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" href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:factor-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -39,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

@ -9,32 +9,35 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% 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>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>{% trans 'Expiry' %}</th> <th>{% trans 'Expiry' %}</th>
<th>{% trans 'Link' %}</th> <th>{% trans 'Link' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for invitation in object_list %} {% for invitation in object_list %}
<tr> <tr>
<td>{{ invitation.expires|default:"Never" }}</td> <td>{{ invitation.expires|default:"Never" }}</td>
<td><pre>{{ invitation.link }}</pre></td> <td>
<td> <pre>{{ invitation.link }}</pre>
<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>
</td> <td>
</tr> <a class="btn btn-default btn-sm"
{% endfor %} href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,11 +7,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}</a> <a href="{% url 'passbook_admin:applications' %}">
<span class="pficon-applications"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ application_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:applications' %}">
<span class="pficon pficon-ok"></span>{{ application_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -19,11 +26,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}</a> <a href="{% url 'passbook_admin:sources' %}">
<span class="pficon-resource-pool"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ source_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:sources' %}">
<span class="pficon pficon-ok"></span>{{ source_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -31,11 +45,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}</a> <a href="{% url 'passbook_admin:providers' %}">
<span class="pficon-integration"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ provider_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:providers' %}">
{% 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>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -43,11 +68,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}</a> <a href="{% url 'passbook_admin:factors' %}">
<span class="pficon-plugged"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ factor_count }}</a></span> <span class="card-pf-aggregate-status-notification">
{% if factor_count < 1 %}
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
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>
</p> </p>
</div> </div>
</div> </div>
@ -55,11 +91,22 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}</a> <a href="{% url 'passbook_admin:policies' %}">
<span class="pficon-infrastructure"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Policies' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ invitation_count }}</a></span> <span class="card-pf-aggregate-status-notification">
{% if policies_without_attachment > 0 %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'Policies without attachment exist.' %}"></span>
{{ policy_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ policy_count }}
{% endif %}
</span>
</p> </p>
</div> </div>
</div> </div>
@ -67,11 +114,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Policies' %}</a> <a href="{% url 'passbook_admin:invitations' %}">
<span class="pficon-migration"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ policy_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:invitations' %}">
<span class="pficon pficon-ok"></span>{{ invitation_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -79,11 +133,61 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}</a> <a href="{% url 'passbook_admin:users' %}">
<span class="pficon-users"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}
</a>
</h2> </h2>
<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"><a href="#"><span class="pficon pficon-ok"></span>{{ user_count }}</a></span> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:users' %}">
<span class="pficon pficon-ok"></span>{{ user_count }}
</a>
</span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#">
<span class="pficon-bundle"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %}
</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#">
{{ version }}
</a>
</span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#">
<span class="pficon-server"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Worker(s)' %}
</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#">
{% 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 }}
{% endif %}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>

View File

@ -9,42 +9,54 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Policies" %}</h1> <h1><span class="pficon-infrastructure"></span> {% trans "Policies" %}</h1>
<span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span> <span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<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" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
{% endfor %} href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
</ul> {% endfor %}
</div> </ul>
<hr> </div>
<table class="table table-striped table-bordered"> <hr>
<thead> <table class="table table-striped table-bordered">
<tr> <thead>
<th>{% trans 'Name' %}</th> <tr>
<th>{% trans 'Class' %}</th> <th></th>
<th></th> <th>{% trans 'Name' %}</th>
</tr> <th>{% trans 'Type' %}</th>
</thead> <th></th>
<tbody> </tr>
{% for policy in object_list %} </thead>
<tr> <tbody>
<td>{{ policy.name }}</td> {% for policy in object_list %}
<td>{{ policy|fieldtype }}</td> <tr {% if not policy.policymodel_set.exists %} class="warning" {% endif %}>
<td> <th>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> {% if not policy.policymodel_set.exists %}
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-test' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a> <span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right" title="{% trans 'Warning: Policy is not assigned.' %}"></span>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-delete' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> {% else %}
</td> <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>
</tr> {% endif %}
{% endfor %} </th>
</tbody> <td>{{ policy.name }}</td>
</table> <td>{{ policy|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-test' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:policy-delete' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,3 +5,22 @@
{% block above_form %} {% block above_form %}
<h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1> <h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
{% block action %}
{% trans 'Test' %}
{% endblock %}
{% block beneath_form %}
<p class="loading" style="display: none;">
<span class="spinner spinner-xs spinner-inline"></span> {% trans 'Processing, please wait...' %}
</p>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script>
$('form').on('submit', function () {
$('p.loading').show();
})
</script>
{% endblock %}

View File

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

@ -10,45 +10,57 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Providers" %}</h1> <h1><span class="pficon-integration"></span> {% trans "Providers" %}</h1>
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span> <span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<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" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
{% endfor %} href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ name }}</a></li>
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for provider in object_list %}
<tr>
<td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %} {% endfor %}
</td> </ul>
</tr> </div>
{% endfor %} <hr>
</tbody> <table class="table table-striped table-bordered">
</table> <thead>
<tr>
<th></th>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for provider in object_list %}
<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|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Sources" %}</h1> <h1><span class="pficon-resource-pool"></span> {% trans "Sources" %}</h1>
<span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span> <span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span>
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
@ -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>
@ -27,6 +27,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Class' %}</th>
<th>{% trans 'Additional Info' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -35,6 +36,7 @@
<tr> <tr>
<td>{{ source.name }}</td> <td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td> <td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -5,34 +5,38 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Users" %}</h1> <h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
<hr> <hr>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<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> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for user in object_list %}
{% for user in object_list %} <tr>
<tr> <td>{{ user.username }}</td>
<td>{{ user.username }}</td> <td>{{ user.name|default:'-' }}</td>
<td>{{ user.first_name|default:'-' }}</td> <td>{{ user.is_active }}</td>
<td>{{ user.last_name|default:'-' }}</td> <td>{{ user.last_login }}</td>
<td>{{ user.is_active }}</td> <td>
<td>{{ user.last_login }}</td> <a class="btn btn-default btn-sm"
<td> href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <a class="btn btn-default btn-sm"
<a class="btn btn-default btn-sm" 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>
</td> <a class="btn btn-default btn-sm"
</tr> href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
{% endfor %} <a class="btn btn-default btn-sm"
</tbody> href="{% url 'passbook_core:overview' %}?__impersonate={{ user.pk }}">{% trans 'Impersonate' %}</a>
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,7 +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 %}
{% blocktrans with type=form|form_verbose_name %}Create {{ type }}{% endblocktrans %}
{% 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">
@ -11,8 +26,15 @@
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% include 'partials/form.html' with form=form %} {% include 'partials/form.html' with form=form %}
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a> <a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-primary" value="{% trans 'Create' %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
{% block beneath_form %}
{% endblock %}
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
{{ block.super }}
{{ form.media.js }}
{% endblock %}

View File

@ -1,7 +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 %}
{% blocktrans with type=form|form_verbose_name %}Update {{ type }}{% endblocktrans %}
{% 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/',
@ -56,6 +65,13 @@ urlpatterns = [
users.UserUpdateView.as_view(), name='user-update'), users.UserUpdateView.as_view(), name='user-update'),
path('users/<int:pk>/delete/', path('users/<int:pk>/delete/',
users.UserDeleteView.as_view(), name='user-delete'), users.UserDeleteView.as_view(), name='user-delete'),
path('users/<int:pk>/reset/',
users.UserPasswordResetView.as_view(), name='user-password-reset'),
# Groups
path('group/', groups.GroupListView.as_view(), name='group'),
path('group/create/', groups.GroupCreateView.as_view(), name='group-create'),
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'),
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
# Audit Log # Audit Log
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
# Groups # Groups

View File

@ -1,4 +1,5 @@
"""passbook Application administration""" """passbook Application administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -13,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):
@ -28,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"""
@ -45,5 +51,10 @@ class ApplicationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView)
model = Application model = Application
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully updated Application') success_message = _('Successfully deleted Application')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,4 +1,5 @@
"""passbook Factor administration""" """passbook Factor administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -33,25 +34,24 @@ 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')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
source_type = self.request.GET.get('type') factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
kwargs['type'] = model._meta.verbose_name kwargs['type'] = model._meta.verbose_name
return kwargs return kwargs
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get('type') factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView): class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update factor""" """Update factor"""
@ -61,11 +61,12 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
success_message = _('Successfully updated Factor') success_message = _('Successfully updated Factor')
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get('type') form_class_path = self.get_object().form
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type) form_class = path_to_class(form_class_path)
if not model: return form_class
raise Http404
return path_to_class(model.form) def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete factor""" """Delete factor"""
@ -73,7 +74,11 @@ class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Factor model = Factor
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully updated Factor') success_message = _('Successfully deleted Factor')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

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

View File

@ -1,4 +1,5 @@
"""passbook Invitation administration""" """passbook Invitation administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -26,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
@ -42,4 +47,8 @@ class InvitationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Invitation model = Invitation
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:invitations') success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully updated Invitation') success_message = _('Successfully deleted Invitation')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -2,6 +2,8 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.celery import CELERY_APP
from passbook.core.models import (Application, Factor, Invitation, Policy, from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User) Provider, Source, User)
@ -19,4 +21,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs['source_count'] = len(Source.objects.all()) kwargs['source_count'] = len(Source.objects.all())
kwargs['factor_count'] = len(Factor.objects.all()) kwargs['factor_count'] = len(Factor.objects.all())
kwargs['invitation_count'] = len(Invitation.objects.all()) kwargs['invitation_count'] = len(Invitation.objects.all())
kwargs['version'] = __version__
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')
@ -68,11 +69,15 @@ class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Policy model = Policy
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully updated Policy') success_message = _('Successfully deleted Policy')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class PolicyTestView(AdminRequiredMixin, DetailView, FormView): class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
"""View to test policy(s)""" """View to test policy(s)"""
@ -96,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

@ -1,4 +1,5 @@
"""passbook Provider administration""" """passbook Provider administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -28,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')
@ -64,7 +65,11 @@ class ProviderDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
model = Provider model = Provider
template_name = 'generic/delete.html' template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully updated Provider') success_message = _('Successfully deleted Provider')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,4 +1,5 @@
"""passbook Source administration""" """passbook Source administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -33,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')
@ -66,9 +67,13 @@ class SourceDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete source""" """Delete source"""
model = Source model = Source
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully updated Source') success_message = _('Successfully deleted Source')
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,12 +1,15 @@
"""passbook User administration""" """passbook User administration"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
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 User
class UserListView(AdminRequiredMixin, ListView): class UserListView(AdminRequiredMixin, ListView):
@ -20,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')
@ -31,6 +34,24 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete user""" """Delete user"""
model = User model = User
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy('passbook_admin:users')
success_message = _('Successfully updated User') success_message = _('Successfully deleted User')
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class UserPasswordResetView(AdminRequiredMixin, View):
"""Get Password reset link for user"""
# pylint: disable=invalid-name
def get(self, request, pk):
"""Create nonce for user and return link"""
user = get_object_or_404(User, pk=pk)
nonce = Nonce.objects.create(user=user)
link = request.build_absolute_uri(reverse(
'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid}))
messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link}))
return redirect('passbook_admin:users')

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.7-alpha' __version__ = '0.1.18-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.7-alpha' __version__ = '0.1.18-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__)
@ -51,7 +49,10 @@ class AuditEntry(UUIDModel):
def create(action, request, **kwargs): def create(action, request, **kwargs):
"""Create AuditEntry from arguments""" """Create AuditEntry from arguments"""
client_ip, _ = get_client_ip(request) client_ip, _ = get_client_ip(request)
user = request.user if not hasattr(request, 'user'):
user = None
else:
user = request.user
if isinstance(user, AnonymousUser): if isinstance(user, AnonymousUser):
user = kwargs.get('user', None) user = kwargs.get('user', None)
entry = AuditEntry.objects.create( entry = AuditEntry.objects.create(
@ -60,7 +61,7 @@ class AuditEntry(UUIDModel):
# User 255.255.255.255 as fallback if IP cannot be determined # User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255', request_ip=client_ip or '255.255.255.255',
context=kwargs) context=kwargs)
LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip) LOGGER.debug("Logged %s from %s (%s)", action, user, client_ip)
return entry return entry
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -72,41 +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"""
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.7-alpha' __version__ = '0.1.18-beta'

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