Compare commits

..

78 Commits

Author SHA1 Message Date
9e46c8bfec bump version: 0.1.22-beta -> 0.1.23-beta 2019-03-18 20:54:31 +01:00
1eaa9b9733 prepare 0.1.23 2019-03-18 20:54:23 +01:00
ee05834b69 Merge branch '28-openid-connect-discovery' into 'master'
set issuer to root address instead of well-known path

Closes #28

See merge request BeryJu.org/passbook!13
2019-03-18 19:52:13 +00:00
fccc8f4959 set issuer to root address instead of well-known path 2019-03-18 20:42:32 +01:00
c721620f96 Merge branch '28-openid-connect-discovery' into 'master'
Resolve "OpenID Connect Discovery support"

Closes #28

See merge request BeryJu.org/passbook!12
2019-03-18 19:42:08 +00:00
c9f73d718e start implementing openid connect discovery 2019-03-18 20:35:11 +01:00
bfa58be721 bump version: 0.1.21-beta -> 0.1.22-beta 2019-03-14 21:22:15 +01:00
4bb602149e perapre 0.1.22 2019-03-14 21:20:55 +01:00
81ab9092fc Fix OAuth Client's disconnect view having invalid URL names 2019-03-14 21:19:14 +01:00
29d5962c4c add Azure AD Source 2019-03-14 21:18:55 +01:00
5c75339946 point to correct icons 2019-03-14 21:18:13 +01:00
4774d9a46c fix delete form not working 2019-03-14 21:17:41 +01:00
dbe16ba4fd fix layout when on mobile viewport and scrolling 2019-03-14 21:17:28 +01:00
6972cf00a0 move icons to single folder, cleanup 2019-03-14 21:17:07 +01:00
0445be9712 fix missing debug template 2019-03-14 21:16:27 +01:00
89dbdd9585 bump version: 0.1.20-beta -> 0.1.21-beta 2019-03-14 18:08:02 +01:00
da88ce7150 prepare 0.1.21 2019-03-14 18:02:13 +01:00
5f50fcfcf5 detect HTTPS from reverse proxy 2019-03-14 18:01:41 +01:00
96be087221 add request debug view 2019-03-14 18:01:27 +01:00
a53a269a8c bump version: 0.1.19-beta -> 0.1.20-beta 2019-03-13 16:51:43 +01:00
59565a5286 prepare 0.1.20 2019-03-13 16:51:38 +01:00
ae3c092238 add user settings for Sources 2019-03-13 16:49:30 +01:00
e98e5e4e3e fix GitHub Pretend again 2019-03-13 15:52:05 +01:00
d50c7ec8d4 bump version: 0.1.18-beta -> 0.1.19-beta 2019-03-13 15:16:48 +01:00
c0fdf377d1 prepare 0.1.18 2019-03-13 15:14:36 +01:00
70c11c8988 fix GitHub Pretend throwing a 500 error 2019-03-13 15:12:13 +01:00
67b19becc1 fix API Call for sentry-client, add missing template 2019-03-13 14:27:34 +01:00
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
2d7e8f1b50 add group administration 2019-03-08 15:49:45 +01:00
155 changed files with 2253 additions and 658 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.7-beta
current_version = 0.1.23-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,6 +15,10 @@ values =
beta
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]

View File

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

View File

@ -1,5 +1,6 @@
"""passbook provider"""
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from allauth_passbook.provider import PassbookProvider
urlpatterns = default_urlpatterns(PassbookProvider)

View File

@ -1,10 +1,10 @@
"""passbook adapter"""
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView)
from allauth_passbook.provider import PassbookProvider

View File

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='django-allauth-passbook',
version='1.0.0',
version='0.1.23-beta',
description='passbook support for django-allauth',
# long_description='\n'.join(read_simple('docs/index.md')[2:]),
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 PassbookApiError(Exception):
def __init__(self, message='', status=0):
super(PassbookApiError, self).__init__(message)
self.status = status
class PassbookClient(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 PassbookApiError(unicode(e), status=getattr(e, 'status_code', 0))
if req.status_code < 200 or req.status_code >= 300:
raise PassbookApiError(req.content, status=req.status_code)
return json.loads(req.content)
def get_user(self, access_token):
return self._request('/api/v1/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.23-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

114
debian/changelog vendored
View File

@ -1,4 +1,116 @@
passbook (0.1.6) stable; urgency=medium
passbook (0.1.23) stable; urgency=medium
* add support for OpenID-Connect Discovery
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 18 Mar 2019 20:19:27 +0000
passbook (0.1.22) stable; urgency=medium
* bump version: 0.1.20-beta -> 0.1.21-beta
* fix missing debug template
* move icons to single folder, cleanup
* fix layout when on mobile viewport and scrolling
* fix delete form not working
* point to correct icons
* add Azure AD Source
* Fix OAuth Client's disconnect view having invalid URL names
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 14 Mar 2019 20:19:27 +0000
passbook (0.1.21) stable; urgency=medium
* bump version: 0.1.19-beta -> 0.1.20-beta
* add request debug view
* detect HTTPS from reverse proxy
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 14 Mar 2019 17:01:49 +0000
passbook (0.1.20) stable; urgency=medium
* bump version: 0.1.18-beta -> 0.1.19-beta
* fix GitHub Pretend again
* add user settings for Sources
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 13 Mar 2019 15:49:44 +0000
passbook (0.1.18) stable; urgency=medium
* bump version: 0.1.16-beta -> 0.1.17-beta
* fix Server Error when downloading metadata
* add sentry client
* fix included yaml file
* adjust versions for client packages, auto build client-packages
* bump version: 0.1.17-beta -> 0.1.18-beta
* fix API Call for sentry-client, add missing template
* fix GitHub Pretend throwing a 500 error
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 13 Mar 2019 14:14:10 +0000
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

2
debian/control vendored
View File

@ -8,7 +8,7 @@ Standards-Version: 3.9.6
Package: passbook
Architecture: all
Recommends: mysql-server, redis-server
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.

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@ -22,7 +22,7 @@ data:
host: 127.0.0.1
port: 514
email:
host: localhost
host: {{ .Values.config.email.host }}
port: 25
user: ''
password: ''
@ -36,7 +36,7 @@ data:
debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master"
rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }}
@ -123,6 +123,7 @@ data:
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
- passbook.oauth_client.source_types.azure_ad
saml_idp:
signing: true
autosubmit: false

View File

@ -18,6 +18,7 @@ spec:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
spec:
volumes:
- name: config-volume

View File

@ -17,3 +17,4 @@ spec:
selector:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web

View File

@ -18,6 +18,7 @@ spec:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: worker
spec:
volumes:
- name: config-volume

View File

@ -5,7 +5,7 @@
replicaCount: 1
image:
tag: 0.1.7-beta
tag: 0.1.23-beta
nameOverride: ""
@ -14,10 +14,16 @@ config:
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
error_reporting: true
email:
host: localhost
postgresql:
postgresqlDatabase: passbook
postgresqlPassword: foo
postgresqlDatabase: passbook
postgresqlPassword: foo
rabbitmq:
rabbitmq:
password: foo
service:
type: ClusterIP
@ -31,7 +37,6 @@ ingress:
path: /
hosts:
- passbook.k8s.local
- kubernetes-healthcheck-host
defaultHost: passbook.k8s.local
tls: []
# - secretName: chart-example-tls

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

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

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

View File

@ -0,0 +1,31 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="pficon-applications"></span> {% trans "Request" %}</h1>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Key' %}</th>
<th>{% trans 'Value' %}</th>
</tr>
</thead>
<tbody>
{% for key, value in request_dict.items %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

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

View File

@ -1,83 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load static %}
{% load utils %}
{% block head %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-treeview.min.css'%}">
{% endblock %}
{% block scripts %}
{{ block.super }}
<script src="{% static 'js/bootstrap-treeview.min.js' %}"></script>
<script>
var cleanupData = function (obj) {
return {
text: obj.name,
href: '?group=' + obj.uuid,
nodes: obj.children.map(cleanupData),
};
}
$(function() {
var apiUrl = "{% url 'passbook_admin:group-list' %}?format=json";
$.ajax({
url: apiUrl,
}).done(function(data) {
$('#treeview1').treeview({
collapseIcon: "fa fa-angle-down",
data: data.map(cleanupData),
expandIcon: "fa fa-angle-right",
nodeIcon: "fa pficon-users",
showBorder: true,
enableLinks: true,
onNodeSelected: function (event, node) {
window.location.href = node.href;
}
});
});
});
</script>
{% endblock %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="col-md-3">
<div id="treeview1" class="treeview">
</div>
</div>
<div class="col-md-9">
<h1>{% trans "Invitations" %}</h1>
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary">
{% trans 'Create...' %}
</a>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Expiry' %}</th>
<th>{% trans 'Link' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for invitation in object_list %}
<tr>
<td>{{ invitation.expires|default:"Never" }}</td>
<td>
<pre>{{ invitation.link }}</pre>
</td>
<td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{%
trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -5,3 +5,22 @@
{% block above_form %}
<h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1>
{% 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

@ -31,6 +31,8 @@
href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_core:overview' %}?__impersonate={{ user.pk }}">{% trans 'Impersonate' %}</a>
</td>
</tr>
{% endfor %}

View File

@ -2,10 +2,20 @@
{% load i18n %}
{% 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 %}
@ -19,6 +29,8 @@
<input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form>
</div>
{% block beneath_form %}
{% endblock %}
</div>
{% endblock %}

View File

@ -1,7 +1,7 @@
"""passbook URL Configuration"""
from django.urls import include, path
from passbook.admin.views import (applications, audit, factors, groups,
from passbook.admin.views import (applications, audit, debug, factors, groups,
invitations, overview, policy,
property_mapping, providers, sources, users)
@ -67,10 +67,17 @@ urlpatterns = [
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
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
# Groups
path('groups/', groups.GroupListView.as_view(), name='groups'),
# API
path('api/', include('passbook.admin.api.urls'))
path('api/', include('passbook.admin.api.urls')),
# Debug
path('debug/request/', debug.DebugRequestView.as_view(), name='debug-request'),
]

View File

@ -0,0 +1,17 @@
"""passbook administration debug views"""
from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin
class DebugRequestView(AdminRequiredMixin, TemplateView):
"""Show debug info about request"""
template_name = 'administration/debug/request.html'
def get_context_data(self, **kwargs):
kwargs['request_dict'] = {}
for key in dir(self.request):
kwargs['request_dict'][key] = getattr(self.request, key)
return super().get_context_data(**kwargs)

View File

@ -1,12 +1,57 @@
"""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.core.forms.groups import GroupForm
from passbook.core.models import Group
class GroupListView(AdminRequiredMixin, ListView):
"""Show list of all invitations"""
"""Show list of all groups"""
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

@ -11,6 +11,7 @@ from django.views.generic.detail import DetailView
from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy
from passbook.core.policies import PolicyEngine
from passbook.lib.utils.reflection import path_to_class
@ -100,7 +101,9 @@ class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
def form_valid(self, form):
policy = self.get_object()
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:
messages.success(self.request, _('User successfully passed policy.'))
else:

View File

@ -1,2 +1,2 @@
"""passbook api"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

View File

@ -1,2 +1,2 @@
"""passbook core"""
__version__ = '0.1.7-beta'
__version__ = '0.1.23-beta'

View File

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

View File

@ -39,35 +39,41 @@ class AuthenticationView(UserPassesTestMixin, View):
# Allow only not authenticated users to login
def test_func(self):
return self.request.user.is_authenticated is False
return AuthenticationView.SESSION_PENDING_USER in self.request.session
def handle_no_permission(self):
# Function from UserPassesTestMixin
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
return _redirect_with_qs('passbook_core:overview', self.request.GET)
if self.request.user.is_authenticated:
return _redirect_with_qs('passbook_core:overview', self.request.GET)
return _redirect_with_qs('passbook_core:auth-login', self.request.GET)
def get_pending_factors(self):
"""Loading pending factors from Database or load from session variable"""
# Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS]
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
pending_factors = []
for factor in _all_factors:
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(self.pending_user).with_request(self.request).build()
if policy_engine.passing:
pending_factors.append((factor.uuid.hex, factor.type))
return pending_factors
def dispatch(self, request, *args, **kwargs):
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
# Extract pending user from session (only remember uid)
if AuthenticationView.SESSION_PENDING_USER in request.session:
self.pending_user = get_object_or_404(
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
else:
# No Pending user, redirect to login screen
return _redirect_with_qs('passbook_core:auth-login', request.GET)
# Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
else:
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
self.pending_factors = []
for factor in _all_factors:
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(self.pending_user).with_request(request).build()
if policy_engine.passing:
self.pending_factors.append((factor.uuid.hex, factor.type))
self.pending_user = get_object_or_404(
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
self.pending_factors = self.get_pending_factors()
# Read and instantiate factor from session
factor_uuid, factor_class = None, None
if AuthenticationView.SESSION_FACTOR not in request.session:
@ -107,11 +113,11 @@ class AuthenticationView(UserPassesTestMixin, View):
next_factor = None
if self.pending_factors:
next_factor = self.pending_factors.pop()
# Save updated pening_factor list to session
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor is %s", next_factor)
# return _redirect_with_qs('passbook_core:auth-process', kwargs={'factor': next_factor})
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
# User passed all factors
LOGGER.debug("User passed all factors, logging in")
@ -126,7 +132,6 @@ class AuthenticationView(UserPassesTestMixin, View):
def _user_passed(self):
"""User Successfully passed all factors"""
# user = authenticate(request=self.request, )
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in user %s", self.pending_user)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
# Generated by Django 2.1.7 on 2019-03-10 18:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0019_auto_20190310_1615'),
]
operations = [
migrations.CreateModel(
name='GroupMembershipPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Group')),
],
options={
'verbose_name': 'Group Membership Policy',
'verbose_name_plural': 'Group Membership Policies',
},
bases=('passbook_core.policy',),
),
]

View File

@ -8,7 +8,7 @@ from typing import Tuple, Union
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.fields import ArrayField, HStoreField
from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
@ -31,7 +31,7 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children')
extra_data = models.TextField(blank=True)
tags = HStoreField(default=dict)
def __str__(self):
return "Group %s" % self.name
@ -152,11 +152,6 @@ class Application(PolicyModel):
objects = InheritanceManager()
def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application"""
from passbook.core.policies import PolicyEngine
return PolicyEngine(self.policies.all()).for_user(user).build().result
def get_provider(self):
"""Get casted provider instance"""
if not self.provider:
@ -191,6 +186,12 @@ class Source(PolicyModel):
"""Return additional Info, such as a callback URL. Show in the administration interface."""
return None
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
return False
def __str__(self):
return self.name
@ -393,6 +394,21 @@ class DebugPolicy(Policy):
verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies')
class GroupMembershipPolicy(Policy):
"""Policy to check if the user is member in a certain group"""
group = models.ForeignKey('Group', on_delete=models.CASCADE)
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]:
return self.group.user_set.filter(pk=user.pk).exists()
class Meta:
verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies')
class Invitation(UUIDModel):
"""Single-use invitation link"""

View File

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

View File

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

View File

@ -35,6 +35,7 @@ SECRET_KEY = CONFIG.get('secret_key')
DEBUG = CONFIG.get('debug')
INTERNAL_IPS = ['127.0.0.1']
ALLOWED_HOSTS = CONFIG.get('domains', [])
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
LOGIN_URL = 'passbook_core:auth-login'
# CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view'
@ -47,8 +48,7 @@ SESSION_COOKIE_NAME = 'passbook_session'
LANGUAGE_COOKIE_NAME = 'passbook_language'
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'passbook.oauth_client.backends.AuthorizedServiceBackend'
'django.contrib.auth.backends.ModelBackend'
]
# Application definition
@ -60,6 +60,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.postgres',
'rest_framework',
'drf_yasg',
'raven.contrib.django.raven_compat',
@ -184,8 +185,10 @@ CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULE = {}
CELERY_CREATE_MISSING_QUEUES = True
CELERY_TASK_DEFAULT_QUEUE = 'passbook'
CELERY_BROKER_URL = 'redis://%s' % CONFIG.get('redis')
CELERY_RESULT_BACKEND = 'redis://%s' % CONFIG.get('redis')
CELERY_BROKER_URL = 'amqp://%s' % CONFIG.get('rabbitmq')
CELERY_RESULT_BACKEND = 'rpc://'
CELERY_ACKS_LATE = True
CELERY_BROKER_HEARTBEAT = 0
# Raven settings
RAVEN_CONFIG = {

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="20px" height="20px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FBBB00;}
.st1{fill:#518EF8;}
.st2{fill:#28B446;}
.st3{fill:#F14336;}
</style>
<path class="st0" d="M4.4,12.1l-0.7,2.6l-2.5,0.1C0.4,13.3,0,11.7,0,10c0-1.7,0.4-3.2,1.1-4.6h0l2.3,0.4l1,2.3
C4.2,8.7,4.1,9.3,4.1,10C4.1,10.7,4.2,11.4,4.4,12.1z"/>
<path class="st1" d="M19.8,8.1C19.9,8.7,20,9.4,20,10c0,0.7-0.1,1.4-0.2,2.1c-0.5,2.3-1.8,4.3-3.5,5.7l0,0l-2.9-0.1L13,15.1
c1.2-0.7,2.1-1.8,2.6-3h-5.3v-4h5.4H19.8L19.8,8.1z"/>
<path class="st2" d="M16.3,17.8L16.3,17.8C14.5,19.2,12.4,20,10,20c-3.8,0-7.1-2.1-8.8-5.3l3.2-2.7c0.8,2.3,3,3.9,5.6,3.9
c1.1,0,2.1-0.3,3-0.8L16.3,17.8z"/>
<path class="st3" d="M16.4,2.3L13.1,5c-0.9-0.6-2-0.9-3.1-0.9c-2.6,0-4.8,1.7-5.6,4L1.1,5.4h0C2.8,2.2,6.1,0,10,0
C12.4,0,14.7,0.9,16.4,2.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 750 B

View File

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

View File

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 788 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 783 B

After

Width:  |  Height:  |  Size: 783 B

View File

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 688 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

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