Compare commits

...

142 Commits

Author SHA1 Message Date
11b5860d4a new release: 0.1.35-beta 2019-04-29 20:47:32 +02:00
9bdbff4cda fix gitlab-ci using incorrect variables for docker access 2019-04-29 20:42:24 +02:00
e0d597eeac show cache on admin overview, add modal to clear cache, re-add logging to policy 2019-04-29 20:37:44 +02:00
f576985cc9 remove django guardian and check application access via PolicyEngine 2019-04-29 20:07:18 +02:00
22a6aef60b fix wrong import 2019-04-29 20:06:52 +02:00
ec0a6e7854 new release: 0.1.34-beta 2019-04-29 19:22:20 +02:00
6904608e6f fix clicking on administration always loading admin interface 2019-04-29 19:22:14 +02:00
cb3732cb2b filter out connection errors for sentry 2019-04-29 19:16:49 +02:00
57de6cbafc repo cleanup, switch to new docker registry 2019-04-29 17:05:39 +02:00
b1dda764a9 Merge branch 'master' of git.beryju.org:BeryJu.org/passbook
# Conflicts:
#	debian/etc/passbook/config.yml
#	helm/passbook/templates/passbook-configmap.yaml
2019-04-18 11:34:13 +02:00
5ec2102487 Merge branch '39-saml-audience-as-database-field' into 'master'
Resolve "SAML: Audience as Database field"

Closes #39

See merge request BeryJu.org/passbook!25
2019-04-18 09:31:56 +00:00
9f8fb7378a add audience field, switch base processor, better handle processor loading failure 2019-04-18 11:30:21 +02:00
98cd646044 remove unneeded processors 2019-04-18 11:09:12 +02:00
0cba1b4c45 fix SAML RancherProcessor not being loaded by default 2019-04-18 11:01:44 +02:00
53918462b6 new release: 0.1.33-beta 2019-04-18 10:28:12 +02:00
8a7e74b523 better release commit messages 2019-04-18 10:28:09 +02:00
4dc7065e97 prepare 0.1.33 2019-04-18 10:25:22 +02:00
3c93bb9f9f remove included CI file 2019-04-18 10:18:47 +02:00
8143fae2d6 switch to custom CI base image with dependencies installed 2019-04-18 10:15:44 +02:00
3cfe45d3cb fix broken icon for helm chart 2019-04-17 17:53:56 +02:00
8e5c3f2f31 remove unused import 2019-04-17 17:53:45 +02:00
5a3b2fdd49 add Rancher SAML processor 2019-04-17 17:53:15 +02:00
e47b9f0d57 fix SAML Request not being parsed all the time 2019-04-17 17:53:05 +02:00
146dd747f1 fix EntityID being None in SAML Metadata 2019-04-17 17:52:24 +02:00
f2ce56063b bump version: 0.1.31-beta -> 0.1.32-beta 2019-04-17 14:27:41 +02:00
b26f378e4c prepare 0.1.32 2019-04-17 14:27:03 +02:00
9072b836c6 automatically add response_type if not given in OAuth Request 2019-04-17 14:25:51 +02:00
2fa57d064e bump version: 0.1.30-beta -> 0.1.31-beta 2019-04-13 17:58:09 +02:00
146705c60a prepare 0.1.31-beta 2019-04-13 17:58:03 +02:00
5029a99df6 Merge branch '37-guardian' into 'master'
Resolve "Guardian"

Closes #37

See merge request BeryJu.org/passbook!23
2019-04-13 15:56:54 +00:00
e7129d18f6 fix inconsistent migrations 2019-04-13 17:52:11 +02:00
d2bf9f81d6 remove raven middleware 2019-04-13 17:46:51 +02:00
30acf0660b Merge branch 'master' into 37-guardian 2019-04-13 17:43:02 +02:00
dda41af5c8 remove logging to increase speed, add more caching to policy and rewriter 2019-04-13 17:22:03 +02:00
9b5b03647b move actual proxying logic to separate class 2019-04-13 16:05:11 +02:00
940b3eb943 move logging to separate thread 2019-04-13 16:04:48 +02:00
16eb629b71 only enable sentry when not DEBUG 2019-04-11 15:30:42 +02:00
755045b226 try to fix app_gw being null 2019-04-11 15:30:07 +02:00
61478db94e use global urllib Pools 2019-04-11 15:29:35 +02:00
f69f959bdb allow setting authentication_header to empty string (disabling the header) 2019-04-11 15:29:01 +02:00
146edb45d4 bump version: 0.1.29-beta -> 0.1.30-beta 2019-04-11 14:22:34 +02:00
045a802365 don't use context manager in web command 2019-04-11 14:22:32 +02:00
c90d8ddcff bump version: 0.1.28-beta -> 0.1.29-beta 2019-04-11 14:03:08 +02:00
3ff2ec929f prepare 0.1.29 2019-04-11 14:03:05 +02:00
a3ef26b7ad Run collectstatic before coverage, use autoreload on celery worker 2019-04-11 13:54:11 +02:00
19cd1624c1 replace cherrypy with daphne 2019-04-11 13:43:49 +02:00
366ef352c6 switch to whitenoise for static files 2019-04-11 13:43:08 +02:00
a9031a6abc Add libpq-dev dependency so psycopg2 build works 2019-04-11 12:44:26 +02:00
a1a5223b58 bump version: 0.1.27-beta -> 0.1.28-beta 2019-04-11 10:48:31 +02:00
c723b0233f prepare 0.1.28 2019-04-11 10:48:28 +02:00
b369eb28f1 set default log level to warn, fix clean_nonces not working 2019-04-11 10:43:13 +02:00
9b8f390e31 Merge branch '38-websocket-proxying' into 'master'
Resolve "Websocket Proxying"

Closes #38

See merge request BeryJu.org/passbook!24
2019-04-10 20:42:24 +00:00
11630c9a74 switch kubernetes deployment to daphne server 2019-04-10 22:38:25 +02:00
c9ac10f6f6 Implement websocket proxy 2019-04-10 19:03:42 +02:00
04d613cb28 Move code from django-revproxy to app_gw to fix cookie bug 2019-04-10 19:03:22 +02:00
40866f9ecd Choose upstream more cleverly 2019-04-10 18:49:33 +02:00
d8585eb872 trigger autoreload from config files 2019-04-10 18:48:55 +02:00
15aaeda475 remove unused import 2019-04-10 18:47:21 +02:00
8536ef9e23 Add guardian for Application permissions 2019-04-10 18:46:33 +02:00
35b6bb6b3f fix failing CI 2019-04-09 17:26:53 +02:00
eaa573c715 fully remove raven and switch WSGI and logging to sentry_sdk 2019-04-05 16:11:53 +02:00
660972e303 add ability to have non-expiring nonces, clean up expired nonces 2019-04-04 21:49:10 +02:00
a21012bf0c switch from raven to sentry_sdk 2019-04-04 21:48:50 +02:00
8dbafa4bda fix allauth client's formatting 2019-04-04 21:47:28 +02:00
80049413f0 bump version: 0.1.26-beta -> 0.1.27-beta 2019-03-22 14:51:13 +01:00
2739442d4a prepare 0.1.27 2019-03-22 14:51:09 +01:00
c679f0a67c bump version: 0.1.25-beta -> 0.1.26-beta 2019-03-22 12:48:00 +01:00
d9a952dd03 prepare 0.1.26 2019-03-22 12:47:57 +01:00
9a1a0f0aa8 Merge branch '35-better-error-templates' into 'master'
Resolve "Better Error templates"

Closes #35

See merge request BeryJu.org/passbook!20
2019-03-22 11:19:42 +00:00
4d6bb60134 add custom template views 2019-03-22 12:16:30 +01:00
80e6d59382 Merge branch '34-parsed-url-cached' into 'master'
Resolve "InvalidUpstream: Upstream URL scheme must be either 'http' or 'https' (https://ory1-esxi-prod-1.ory1.beryju.org)."

Closes #34

See merge request BeryJu.org/passbook!19
2019-03-22 09:58:44 +00:00
81ac951872 validate upstream in form 2019-03-22 10:55:26 +01:00
f33e553cfd always parse url instead of once 2019-03-22 10:55:04 +01:00
9b0240dc26 bump version: 0.1.24-beta -> 0.1.25-beta 2019-03-21 16:50:00 +01:00
c327310392 prepare 0.1.24-beta release 2019-03-21 16:49:57 +01:00
457375287c Merge branch '30-application-security-gateway' into 'master'
Resolve "Application Security Gateway (Reverse Proxy)"

Closes #30

See merge request BeryJu.org/passbook!17
2019-03-21 15:41:34 +00:00
7e87bfef5b validate server_name in form 2019-03-21 16:36:38 +01:00
a7af5268de Invalidate cache when ApplicationGateway instance is saved 2019-03-21 16:27:37 +01:00
6d916029bb implement actual Rewriting logic 2019-03-21 16:22:07 +01:00
81fdcbadad add compiled regex to RewriteRule 2019-03-21 16:21:51 +01:00
ec1e25fe71 cleanup property_mapping list 2019-03-21 16:21:11 +01:00
b5306e4a94 Redirect to login on reverse proxy 2019-03-21 15:15:01 +01:00
801b8a1e59 prevent ZeroDivisionError 2019-03-21 15:05:04 +01:00
3a52059793 cleanup post-migration mess 2019-03-21 15:02:33 +01:00
10b7d99b37 Merge branch 'master' into 30-application-security-gateway
# Conflicts:
#	passbook/core/policies.py
#	passbook/core/settings.py
2019-03-21 14:58:10 +01:00
6be8d0cbb2 Better handle policy timeouts 2019-03-21 14:53:57 +01:00
5b8e3689ec Check for policies in app_gw 2019-03-21 14:53:47 +01:00
25a5d8f5da Don't use LoginRequired for PermissionDenied View 2019-03-21 14:53:38 +01:00
883d439544 add timeout field to policy to prevent stuck policies 2019-03-21 14:48:51 +01:00
1c3b5889e5 Merge branch '33-cache-policy-results' into 'master'
Resolve "Cache Policy Results"

Closes #33

See merge request BeryJu.org/passbook!18
2019-03-21 10:40:36 +00:00
87012b65e1 add redis as service in CI for unittests 2019-03-21 11:35:40 +01:00
29913773a7 invalidate cache when policy is saved 2019-03-21 11:29:11 +01:00
0bc6a4fed4 explicitly use redis db 2019-03-21 11:28:57 +01:00
4645d8353f utilise cache in PolicyEngine 2019-03-21 11:08:32 +01:00
260c5555fa add redis dependency back in for caching 2019-03-21 11:08:08 +01:00
6f7b917c38 bump version: 0.1.23-beta -> 0.1.24-beta 2019-03-20 23:00:33 +01:00
1456ee6d3e prepare 0.1.24 2019-03-20 23:00:22 +01:00
ae3d3d0295 fix TypeError: can only concatenate list (not "str") to list 2019-03-20 22:50:09 +01:00
c23ceacd0b initial implementation of reverse proxy, using django-revproxy from within a middleware
add new config entry "primary_domain" which is used to set the cookie domain
2019-03-20 22:42:47 +01:00
5155204283 Merge branch '32-automatically-set-owner-field-when-creating-oauth-provider' into 'master'
Resolve "Automatically set owner field when creating OAuth Provider"

Closes #32

See merge request BeryJu.org/passbook!16
2019-03-20 21:20:54 +00:00
5509ec9b0f Merge branch '29-oauth-provider-add-extra-info-button-to-show-urls' into 'master'
Resolve "OAuth Provider: Add extra info button to show URLs"

Closes #29

See merge request BeryJu.org/passbook!15
2019-03-20 21:17:36 +00:00
d6f9b2e47d remove user field from form. Closes #32 2019-03-20 20:09:27 +01:00
67aa4aef11 add modal for OAuth Providers showing the URLs 2019-03-20 20:03:28 +01:00
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
211 changed files with 3009 additions and 912 deletions

View File

@ -1,10 +1,10 @@
[bumpversion]
current_version = 0.1.15-beta
current_version = 0.1.35-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
serialize = {major}.{minor}.{patch}-{release}
message = bump version: {current_version} -> {new_version}
message = new release: {new_version}
tag_name = version/{new_version}
[bumpversion:part: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]
@ -49,3 +53,7 @@ values =
[bumpversion:file:passbook/otp/__init__.py]
[bumpversion:file:passbook/app_gw/__init__.py]
[bumpversion:file:passbook/suspicious_policy/__init__.py]

View File

@ -9,3 +9,6 @@ insert_final_newline = true
[html]
indent_size = 2
[yaml]
indent_size = 2

View File

@ -1,25 +1,33 @@
# Global Variables
before_script:
- "python3 -m pip install -U virtualenv"
- "virtualenv env"
- "source env/bin/activate"
- "pip3 install -U -r requirements-dev.txt"
stages:
- build-buildimage
- test
- build
- docs
- deploy
image: python:3.6
image: docker.beryju.org/passbook/build-base:latest
services:
- postgres:latest
- redis:latest
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77'
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
include:
- /allauth/.gitlab-ci.yml
create-build-image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.build-base --destination docker.beryju.org/passbook/build-base:latest --destination docker.beryju.org/passbook/build-base:0.1.35-beta
stage: build-buildimage
only:
refs:
- tags
- /^version/.*$/
isort:
script:
@ -39,6 +47,7 @@ pylint:
stage: test
coverage:
script:
- python manage.py collectstatic --no-input
- coverage run manage.py test
- coverage report
stage: test
@ -52,9 +61,9 @@ package-docker:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_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.15-beta
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.1.35-beta
stage: build
only:
- tags
@ -65,65 +74,36 @@ package-helm:
- 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
artifacts:
paths:
- passbook-*.tgz
expire_in: 2 days
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
package-client-package-allauth:
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
- cd client-packages/allauth
- python setup.py sdist
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
stage: build
only:
- tags
- /^version/.*$/
refs:
- tags
- /^version/.*$/
changes:
- client-packages/allauth/**
# docs:
# stage: docs
# only:
# - master
# - tags
# - /^debian/.*$/
# environment:
# name: docs
# url: "https://passbook.beryju.org/docs/"
# script:
# - apt update
# - apt install -y rsync
# - "mkdir ~/.ssh"
# - "cp .gitlab/known_hosts ~/.ssh/"
# - "pip3 install -U -r requirements-docs.txt"
# - "eval $(ssh-agent -s)"
# - "echo \"${CI_SSH_PRIVATE}\" | ssh-add -"
# - 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
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/**

View File

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

View File

@ -4,6 +4,9 @@
"[html]": {
"editor.tabSize": 2
},
"[yml]": {
"editor.tabSize": 2
},
"cSpell.words": [
"SAML",
"passbook"

View File

@ -6,7 +6,7 @@ COPY ./requirements.txt /app/
WORKDIR /app/
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \
mkdir /app/static/ && \
pip install -r requirements.txt && \
pip install psycopg2 && \
@ -23,7 +23,7 @@ COPY --from=build /app/static /app/static/
WORKDIR /app/
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \
pip install -r requirements.txt && \
pip install psycopg2 && \
adduser --system --home /app/ passbook && \

12
Dockerfile.build-base Normal file
View File

@ -0,0 +1,12 @@
FROM python:3.6
COPY ./passbook/ /app/passbook
COPY ./client-packages/ /app/client-packages
COPY ./requirements.txt /app/
COPY ./requirements-dev.txt /app/
WORKDIR /app/
RUN apt-get update && apt-get install libssl-dev libffi-dev libpq-dev -y && \
pip install -U -r requirements-dev.txt && \
rm -rf /app/*

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 BeryJu.org
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

View File

@ -1,27 +0,0 @@
# Global Variables
before_script:
- cd allauth/
- "python3 -m pip install -U virtualenv"
- "virtualenv env"
- "source env/bin/activate"
- "pip3 install -U -r requirements-dev.txt"
stages:
- test-allauth
image: python:3.6
isort:
script:
- isort -c -sg env
stage: test-allauth
prospector:
script:
- prospector
stage: test-allauth
pylint:
script:
- pylint passbook
stage: test-allauth
bandit:
script:
- bandit -r allauth_passbook
stage: test-allauth

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.35-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.35-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

71
debian/changelog vendored
View File

@ -1,71 +0,0 @@
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
View File

@ -1 +0,0 @@
10

20
debian/config vendored
View File

@ -1,20 +0,0 @@
#!/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
View File

@ -1,14 +0,0 @@
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
View File

@ -1,22 +0,0 @@
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
View File

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

View File

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

2
debian/gbp.conf vendored
View File

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

8
debian/install vendored
View File

@ -1,8 +0,0 @@
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/

View File

@ -1,14 +0,0 @@
[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

View File

@ -1,14 +0,0 @@
[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
View File

@ -1,36 +0,0 @@
#!/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
View File

@ -1,24 +0,0 @@
#!/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
View File

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

27
debian/rules vendored
View File

@ -1,27 +0,0 @@
#!/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:

View File

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

View File

@ -1,8 +0,0 @@
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
appVersion: "0.1.15-beta"
appVersion: "0.1.35-beta"
description: A Helm chart for passbook.
name: passbook
version: "0.1.15-beta"
icon: https://passbook.beryju.org/images/logo.png
version: "0.1.35-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -0,0 +1 @@
# passbook

View File

@ -0,0 +1,98 @@
---
categories:
- Authentication
- SSO
questions:
- default: "true"
variable: config.error_reporting
type: boolean
description: "Enable error-reporting to sentry.services.beryju.org"
group: "passbook Configuration"
label: "Error Reporting"
####################################################################
### PostgreSQL
####################################################################
- variable: postgresql.enabled
default: true
description: "Deploy a database server as part of this deployment, or set to false and configure an external database connection."
type: boolean
required: true
label: Install PostgreSQL
show_subquestion_if: true
group: "Database Settings"
subquestions:
- variable: postgresql.postgresqlDatabase
default: "passbook"
description: "Database name to create"
type: string
label: PostgreSQL Database
- variable: postgresql.postgresqlUsername
default: "passbook"
description: "Database user to create"
type: string
label: PostgreSQL User
- variable: postgresql.postgresqlPassword
default: ""
description: "password will be auto-generated if not specified"
type: password
label: PostgreSQL Password
- variable: externalDatabase.host
default: ""
description: "Host of the external database"
type: string
label: External Database Host
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.user
default: ""
description: "Existing username in the external DB"
type: string
label: External Database username
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.password
default: ""
description: "External database password"
type: password
label: External Database password
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.database
default: ""
description: "Name of the existing database"
type: string
label: External Database
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.port
default: "3306"
description: "External database port number"
type: string
label: External Database Port
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: postgresql.persistence.enabled
default: false
description: "Enable persistent volume for PostgreSQL"
type: boolean
required: true
label: PostgreSQL Persistent Volume Enabled
show_if: "postgresql.enabled=true"
show_subquestion_if: true
group: "Database Settings"
subquestions:
- variable: postgresql.master.persistence.size
default: "8Gi"
description: "PostgreSQL Persistent Volume Size"
type: string
label: PostgreSQL Volume Size
- variable: postgresql.master.persistence.storageClass
default: ""
description: "If undefined or null, uses the default StorageClass. Default to null"
type: storageclass
label: Default StorageClass for PostgreSQL
- variable: postgresql.master.persistence.existingClaim
default: ""
description: "If not empty, uses the specified existing PVC instead of creating new one"
type: string
label: Existing Persistent Volume Claim for PostgreSQL

View File

@ -5,5 +5,8 @@ dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/
version: 3.10.1
digest: sha256:c36e054785f7d706d7d3f525eb1b167dbc89b42f84da7fc167a18bbb6542c999
generated: 2019-03-11T20:36:35.125079+01:00
- name: redis
repository: https://kubernetes-charts.storage.googleapis.com/
version: 5.1.0
digest: sha256:8bf68bc928a2e3c0f05139635be05fa0840554c7bde4cecd624fac78fb5fa5a3
generated: 2019-03-21T11:06:51.553379+01:00

View File

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

View File

@ -15,8 +15,8 @@ data:
port: ''
log:
level:
console: DEBUG
file: DEBUG
console: WARNING
file: WARNING
file: /dev/null
syslog:
host: 127.0.0.1
@ -36,7 +36,8 @@ data:
debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq-master"
rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master/0"
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }}
@ -46,6 +47,7 @@ data:
secret_key: {{ randAlphaNum 50 }}
{{- end }}
primary_domain: {{ .Values.primary_domain }}
domains:
{{- range .Values.ingress.hosts }}
- {{ . | quote }}
@ -123,6 +125,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
@ -131,9 +134,4 @@ data:
# 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

@ -26,7 +26,7 @@ spec:
name: {{ include "passbook.fullname" . }}-config
containers:
- name: {{ .Chart.Name }}
image: "docker.pkg.beryju.org/passbook:{{ .Values.image.tag }}"
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c"]
args: ["./manage.py migrate && ./manage.py web"]

View File

@ -26,7 +26,7 @@ spec:
name: {{ include "passbook.fullname" . }}-config
containers:
- name: {{ .Chart.Name }}
image: "docker.pkg.beryju.org/passbook:{{ .Values.image.tag }}"
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["./manage.py", "worker"]
ports:

View File

@ -5,7 +5,7 @@
replicaCount: 1
image:
tag: 0.1.15-beta
tag: 0.1.35-beta
nameOverride: ""

View File

@ -1,7 +0,0 @@
#!/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"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

View File

@ -4,49 +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:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
<a href="{% url 'passbook_admin:groups' %}">{% trans 'Groups' %}</a>
</li>
<li class="{% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">{% trans 'Audit Log' %}</a>
</li>
<li class="{% is_active_app 'admin' %}">
<a href="{% url 'admin:index' %}">{% trans 'Django' %}</a>
</li>
</ul>
{% 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

@ -152,10 +152,8 @@
<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>
<span class="pficon-bundle"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %}
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
@ -192,5 +190,59 @@
</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">
<span class="pficon-server"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Cached Policies' %}
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#" data-toggle="modal" data-target="#clearCacheMOdal">
{% if cached_policies < 1 %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No policies cached. Users may experience slow response times.' %}"></span> {{ cached_policies }}
{% else %}
<span class="pficon pficon-ok"></span>{{ cached_policies }}
{% endif %}
</a>
</span>
</p>
</div>
</div>
</div>
</div>
<div class="modal fade" id="clearCacheMOdal" tabindex="-1" role="dialog" aria-labelledby="clearCacheMOdalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<h4 class="modal-title" id="clearCacheMOdalLabel">{% trans 'Clear Cache' %}</h4>
</div>
<div class="modal-body">
<form method="post" id="clearForm">
{% csrf_token %}
<input type="hidden" name="clear">
<p>
{% blocktrans %}
Are you sure you want to clear the cache? This includes all user sessions and all cached Policy results.
{% endblocktrans %}
</p>
<h3>
{% blocktrans %}
This will also log you out.
{% endblocktrans %}
</h3>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button form="clearForm" type="submit" type="button" class="btn btn-danger">{% trans 'Clear' %}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -36,7 +36,7 @@
<tbody>
{% for property_mapping in object_list %}
<tr>
<td>{{ property_mapping.name }} ({{ property_mapping.slug }})</td>
<td>{{ property_mapping.name }}</td>
<td>{{ property_mapping|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"

View File

@ -57,6 +57,10 @@
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
{% get_htmls provider as htmls %}
{% for html in htmls %}
{{ html|safe }}
{% endfor %}
</td>
</tr>
{% endfor %}

View File

@ -5,6 +5,8 @@ from logging import getLogger
from django import template
from django.db.models import Model
from passbook.lib.utils.template import render_to_string
register = template.Library()
LOGGER = getLogger(__name__)
@ -29,3 +31,24 @@ def get_links(model_instance):
pass
return links
@register.simple_tag(takes_context=True)
def get_htmls(context, model_instance):
"""Find all html_ methods on an object instance, run them and return as dict"""
prefix = 'html_'
htmls = []
if not isinstance(model_instance, Model):
LOGGER.warning("Model %s is not instance of Model", model_instance)
return htmls
try:
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod):
if name.startswith(prefix):
template, _context = method(context.get('request'))
htmls.append(render_to_string(template, _context))
except NotImplementedError:
pass
return htmls

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)
@ -77,5 +77,7 @@ urlpatterns = [
# 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,4 +1,6 @@
"""passbook administration overview"""
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin
@ -13,6 +15,13 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
template_name = 'administration/overview.html'
def post(self, *args, **kwargs):
"""Handle post (clear cache from modal)"""
if 'clear' in self.request.POST:
cache.clear()
return redirect(reverse('passbook_core:auth-login'))
return self.get(*args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['application_count'] = len(Application.objects.all())
kwargs['policy_count'] = len(Policy.objects.all())
@ -25,4 +34,6 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
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))
kwargs['cached_policies'] = len(cache.keys('policy_*'))
print(cache.keys('*'))
return super().get_context_data(**kwargs)

View File

@ -1,2 +1,2 @@
"""passbook api"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

BIN
passbook/app_gw/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,2 @@
"""passbook Application Security Gateway Header"""
__version__ = '0.1.35-beta'

5
passbook/app_gw/admin.py Normal file
View File

@ -0,0 +1,5 @@
"""passbook Application Security Gateway model admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_app_gw')

16
passbook/app_gw/apps.py Normal file
View File

@ -0,0 +1,16 @@
"""passbook Application Security Gateway app"""
from importlib import import_module
from django.apps import AppConfig
class PassbookApplicationApplicationGatewayConfig(AppConfig):
"""passbook app_gw app"""
name = 'passbook.app_gw'
label = 'passbook_app_gw'
verbose_name = 'passbook Application Security Gateway'
mountpoint = 'app_gw/'
def ready(self):
import_module('passbook.app_gw.signals')

66
passbook/app_gw/forms.py Normal file
View File

@ -0,0 +1,66 @@
"""passbook Application Security Gateway Forms"""
from urllib.parse import urlparse
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms import ValidationError
from django.utils.translation import gettext as _
from passbook.app_gw.models import ApplicationGatewayProvider, RewriteRule
from passbook.lib.fields import DynamicArrayField
class ApplicationGatewayProviderForm(forms.ModelForm):
"""Security Gateway Provider form"""
def clean_server_name(self):
"""Check if server_name is in DB already, since
Postgres ArrayField doesn't suppport keys."""
current = self.cleaned_data.get('server_name')
if ApplicationGatewayProvider.objects \
.filter(server_name__overlap=current) \
.exclude(pk=self.instance.pk).exists():
raise ValidationError(_("Server Name already in use."))
return current
def clean_upstream(self):
"""Check that upstream begins with http(s)"""
for upstream in self.cleaned_data.get('upstream'):
_parsed_url = urlparse(upstream)
if _parsed_url.scheme not in ('http', 'https'):
raise ValidationError(_("URL Scheme must be either http or https"))
return self.cleaned_data.get('upstream')
class Meta:
model = ApplicationGatewayProvider
fields = ['server_name', 'upstream', 'enabled', 'authentication_header',
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
widgets = {
'authentication_header': forms.TextInput(),
'default_content_type': forms.TextInput(),
'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False)
}
field_classes = {
'server_name': DynamicArrayField,
'upstream': DynamicArrayField
}
labels = {
'upstream_ssl_verification': _('Verify upstream SSL Certificates?'),
'property_mappings': _('Rewrite Rules')
}
class RewriteRuleForm(forms.ModelForm):
"""Rewrite Rule Form"""
class Meta:
model = RewriteRule
fields = ['name', 'match', 'halt', 'replacement', 'redirect', 'conditions']
widgets = {
'name': forms.TextInput(),
'match': forms.TextInput(attrs={'data-is-monospace': True}),
'replacement': forms.TextInput(attrs={'data-is-monospace': True}),
'conditions': FilteredSelectMultiple(_('Conditions'), False)
}

View File

@ -0,0 +1,33 @@
"""passbook app_gw middleware"""
from django.views.generic import RedirectView
from passbook.app_gw.proxy.handler import RequestHandler
from passbook.lib.config import CONFIG
class ApplicationGatewayMiddleware:
"""Check if request should be proxied or handeled normally"""
_app_gw_cache = {}
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Rudimentary cache
host_header = request.META.get('HTTP_HOST')
if host_header not in self._app_gw_cache:
self._app_gw_cache[host_header] = RequestHandler.find_app_gw_for_request(request)
if self._app_gw_cache[host_header]:
return self.dispatch(request, self._app_gw_cache[host_header])
return self.get_response(request)
def dispatch(self, request, app_gw):
"""Build proxied request and pass to upstream"""
handler = RequestHandler(app_gw, request)
if not handler.check_permission():
to_url = 'https://%s/?next=%s' % (CONFIG.get('domains')[0], request.get_full_path())
return RedirectView.as_view(url=to_url)(request)
return handler.get_response()

BIN
passbook/app_gw/migrations/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,50 @@
# Generated by Django 2.1.7 on 2019-03-20 21:38
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
]
operations = [
migrations.CreateModel(
name='ApplicationGatewayProvider',
fields=[
('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')),
('server_name', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
('upstream', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
('enabled', models.BooleanField(default=True)),
('authentication_header', models.TextField(default='X-Remote-User')),
('default_content_type', models.TextField(default='application/octet-stream')),
('upstream_ssl_verification', models.BooleanField(default=True)),
],
options={
'verbose_name': 'Application Gateway Provider',
'verbose_name_plural': 'Application Gateway Providers',
},
bases=('passbook_core.provider',),
),
migrations.CreateModel(
name='RewriteRule',
fields=[
('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')),
('match', models.TextField()),
('halt', models.BooleanField(default=False)),
('replacement', models.TextField()),
('redirect', models.CharField(choices=[('internal', 'Internal'), (301, 'Moved Permanently'), (302, 'Found')], max_length=50)),
('conditions', models.ManyToManyField(to='passbook_core.Policy')),
],
options={
'verbose_name': 'Rewrite Rule',
'verbose_name_plural': 'Rewrite Rules',
},
bases=('passbook_core.propertymapping',),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-03-21 15:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='rewriterule',
name='conditions',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2 on 2019-04-11 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0002_auto_20190321_1521'),
]
operations = [
migrations.AlterField(
model_name='applicationgatewayprovider',
name='authentication_header',
field=models.TextField(blank=True, default='X-Remote-User'),
),
]

View File

74
passbook/app_gw/models.py Normal file
View File

@ -0,0 +1,74 @@
"""passbook app_gw models"""
import re
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Policy, PropertyMapping, Provider
class ApplicationGatewayProvider(Provider):
"""Virtual server which proxies requests to any hostname in server_name to upstream"""
server_name = ArrayField(models.TextField())
upstream = ArrayField(models.TextField())
enabled = models.BooleanField(default=True)
authentication_header = models.TextField(default='X-Remote-User', blank=True)
default_content_type = models.TextField(default='application/octet-stream')
upstream_ssl_verification = models.BooleanField(default=True)
form = 'passbook.app_gw.forms.ApplicationGatewayProviderForm'
@property
def name(self):
"""since this model has no name property, return a joined list of server_names as name"""
return ', '.join(self.server_name)
def __str__(self):
return "Application Gateway %s" % ', '.join(self.server_name)
class Meta:
verbose_name = _('Application Gateway Provider')
verbose_name_plural = _('Application Gateway Providers')
class RewriteRule(PropertyMapping):
"""Rewrite requests matching `match` with `replacement`, if all polcies in `conditions` apply"""
REDIRECT_INTERNAL = 'internal'
REDIRECT_PERMANENT = 301
REDIRECT_FOUND = 302
REDIRECTS = (
(REDIRECT_INTERNAL, _('Internal')),
(REDIRECT_PERMANENT, _('Moved Permanently')),
(REDIRECT_FOUND, _('Found')),
)
match = models.TextField()
halt = models.BooleanField(default=False)
conditions = models.ManyToManyField(Policy, blank=True)
replacement = models.TextField() # python formatted strings, use {match.1}
redirect = models.CharField(max_length=50, choices=REDIRECTS)
form = 'passbook.app_gw.forms.RewriteRuleForm'
_matcher = None
@property
def compiled_matcher(self):
"""Cache the compiled regex in memory"""
if not self._matcher:
self._matcher = re.compile(self.match)
return self._matcher
def __str__(self):
return "Rewrite Rule %s" % self.name
class Meta:
verbose_name = _('Rewrite Rule')
verbose_name_plural = _('Rewrite Rules')

View File

View File

@ -0,0 +1,8 @@
"""Exception classes"""
class ReverseProxyException(Exception):
"""Base for revproxy exception"""
class InvalidUpstream(ReverseProxyException):
"""Invalid upstream set"""

View File

@ -0,0 +1,225 @@
"""passbook app_gw request handler"""
import mimetypes
from logging import getLogger
from random import SystemRandom
from urllib.parse import urlparse
import certifi
import urllib3
from django.core.cache import cache
from django.utils.http import urlencode
from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.exceptions import InvalidUpstream
from passbook.app_gw.proxy.response import get_django_response
from passbook.app_gw.proxy.rewrite import Rewriter
from passbook.app_gw.proxy.utils import encode_items, normalize_request_headers
from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream'
IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored'
LOGGER = getLogger(__name__)
QUOTE_SAFE = r'<.;>\(}*+|~=-$/_:^@)[{]&\'!,"`'
ERRORS_MESSAGES = {
'upstream-no-scheme': ("Upstream URL scheme must be either "
"'http' or 'https' (%s).")
}
HTTP_NO_VERIFY = urllib3.PoolManager()
HTTP = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
IGNORED_HOSTS = cache.get(IGNORED_HOSTNAMES_KEY, [])
POLICY_CACHE = {}
class RequestHandler:
"""Forward requests"""
_parsed_url = None
_request_headers = None
def __init__(self, app_gw, request):
self.app_gw = app_gw
self.request = request
if self.app_gw.pk not in POLICY_CACHE:
POLICY_CACHE[self.app_gw.pk] = self.app_gw.application.policies.all()
@staticmethod
def find_app_gw_for_request(request):
"""Check if a request should be proxied or forwarded to passbook"""
# Check if hostname is in cached list of ignored hostnames
# This saves us having to query the database on each request
host_header = request.META.get('HTTP_HOST')
if host_header in IGNORED_HOSTS:
# LOGGER.debug("%s is ignored", host_header)
return False
# Look through all ApplicationGatewayProviders and check hostnames
matches = ApplicationGatewayProvider.objects.filter(
server_name__contains=[host_header],
enabled=True)
if not matches.exists():
# Mo matching Providers found, add host header to ignored list
IGNORED_HOSTS.append(host_header)
cache.set(IGNORED_HOSTNAMES_KEY, IGNORED_HOSTS)
# LOGGER.debug("Ignoring %s", host_header)
return False
# At this point we're certain there's a matching ApplicationGateway
if len(matches) > 1:
# This should never happen
raise ValueError
app_gw = matches.first()
try:
# Check if ApplicationGateway is associated with application
getattr(app_gw, 'application')
if app_gw:
return app_gw
except Application.DoesNotExist:
pass
# LOGGER.debug("ApplicationGateway not associated with Application")
return True
def _get_upstream(self):
"""Choose random upstream and save in session"""
if SESSION_UPSTREAM_KEY not in self.request.session:
self.request.session[SESSION_UPSTREAM_KEY] = {}
if self.app_gw.pk not in self.request.session[SESSION_UPSTREAM_KEY]:
upstream_index = int(SystemRandom().random() * len(self.app_gw.upstream))
self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk] = upstream_index
return self.app_gw.upstream[self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk]]
def get_upstream(self):
"""Get upstream as parsed url"""
upstream = self._get_upstream()
self._parsed_url = urlparse(upstream)
if self._parsed_url.scheme not in ('http', 'https'):
raise InvalidUpstream(ERRORS_MESSAGES['upstream-no-scheme'] %
upstream)
return upstream
def _format_path_to_redirect(self):
# LOGGER.debug("Path before: %s", self.request.get_full_path())
rewriter = Rewriter(self.app_gw, self.request)
after = rewriter.build()
# LOGGER.debug("Path after: %s", after)
return after
def get_proxy_request_headers(self):
"""Get normalized headers for the upstream
Gets all headers from the original request and normalizes them.
Normalization occurs by removing the prefix ``HTTP_`` and
replacing and ``_`` by ``-``. Example: ``HTTP_ACCEPT_ENCODING``
becames ``Accept-Encoding``.
.. versionadded:: 0.9.1
:param request: The original HTTPRequest instance
:returns: Normalized headers for the upstream
"""
return normalize_request_headers(self.request)
def get_request_headers(self):
"""Return request headers that will be sent to upstream.
The header REMOTE_USER is set to the current user
if AuthenticationMiddleware is enabled and
the view's add_remote_user property is True.
.. versionadded:: 0.9.8
"""
request_headers = self.get_proxy_request_headers()
if not self.app_gw.authentication_header:
return request_headers
request_headers[self.app_gw.authentication_header] = self.request.user.get_username()
# LOGGER.debug("%s set", self.app_gw.authentication_header)
return request_headers
def check_permission(self):
"""Check if user is authenticated and has permission to access app"""
if not hasattr(self.request, 'user'):
return False
if not self.request.user.is_authenticated:
return False
policy_engine = PolicyEngine(POLICY_CACHE[self.app_gw.pk])
policy_engine.for_user(self.request.user).with_request(self.request).build()
passing, _messages = policy_engine.result
return passing
def get_encoded_query_params(self):
"""Return encoded query params to be used in proxied request"""
get_data = encode_items(self.request.GET.lists())
return urlencode(get_data)
def _created_proxy_response(self, path):
request_payload = self.request.body
# LOGGER.debug("Request headers: %s", self._request_headers)
request_url = self.get_upstream() + path
# LOGGER.debug("Request URL: %s", request_url)
if self.request.GET:
request_url += '?' + self.get_encoded_query_params()
# LOGGER.debug("Request URL: %s", request_url)
http = HTTP
if not self.app_gw.upstream_ssl_verification:
http = HTTP_NO_VERIFY
try:
proxy_response = http.urlopen(self.request.method,
request_url,
redirect=False,
retries=None,
headers=self._request_headers,
body=request_payload,
decode_content=False,
preload_content=False)
# LOGGER.debug("Proxy response header: %s",
# proxy_response.getheaders())
except urllib3.exceptions.HTTPError as error:
LOGGER.exception(error)
raise
return proxy_response
def _replace_host_on_redirect_location(self, proxy_response):
location = proxy_response.headers.get('Location')
if location:
if self.request.is_secure():
scheme = 'https://'
else:
scheme = 'http://'
request_host = scheme + self.request.META.get('HTTP_HOST')
upstream_host_http = 'http://' + self._parsed_url.netloc
upstream_host_https = 'https://' + self._parsed_url.netloc
location = location.replace(upstream_host_http, request_host)
location = location.replace(upstream_host_https, request_host)
proxy_response.headers['Location'] = location
# LOGGER.debug("Proxy response LOCATION: %s",
# proxy_response.headers['Location'])
def _set_content_type(self, proxy_response):
content_type = proxy_response.headers.get('Content-Type')
if not content_type:
content_type = (mimetypes.guess_type(self.request.path)[0] or
self.app_gw.default_content_type)
proxy_response.headers['Content-Type'] = content_type
# LOGGER.debug("Proxy response CONTENT-TYPE: %s",
# proxy_response.headers['Content-Type'])
def get_response(self):
"""Pass request to upstream and return response"""
self._request_headers = self.get_request_headers()
path = self._format_path_to_redirect()
proxy_response = self._created_proxy_response(path)
self._replace_host_on_redirect_location(proxy_response)
self._set_content_type(proxy_response)
response = get_django_response(proxy_response, strict_cookies=False)
# LOGGER.debug("RESPONSE RETURNED: %s", response)
return response

View File

@ -0,0 +1,63 @@
"""response functions from django-revproxy"""
import logging
from django.http import HttpResponse, StreamingHttpResponse
from passbook.app_gw.proxy.utils import (cookie_from_string,
set_response_headers, should_stream)
#: Default number of bytes that are going to be read in a file lecture
DEFAULT_AMT = 2 ** 16
logger = logging.getLogger('revproxy.response')
def get_django_response(proxy_response, strict_cookies=False):
"""This method is used to create an appropriate response based on the
Content-Length of the proxy_response. If the content is bigger than
MIN_STREAMING_LENGTH, which is found on utils.py,
than django.http.StreamingHttpResponse will be created,
else a django.http.HTTPResponse will be created instead
:param proxy_response: An Instance of urllib3.response.HTTPResponse that
will create an appropriate response
:param strict_cookies: Whether to only accept RFC-compliant cookies
:returns: Returns an appropriate response based on the proxy_response
content-length
"""
status = proxy_response.status
headers = proxy_response.headers
logger.debug('Proxy response headers: %s', headers)
content_type = headers.get('Content-Type')
logger.debug('Content-Type: %s', content_type)
if should_stream(proxy_response):
logger.info('Content-Length is bigger than %s', DEFAULT_AMT)
response = StreamingHttpResponse(proxy_response.stream(DEFAULT_AMT),
status=status,
content_type=content_type)
else:
content = proxy_response.data or b''
response = HttpResponse(content, status=status,
content_type=content_type)
logger.info('Normalizing response headers')
set_response_headers(response, headers)
logger.debug('Response headers: %s', getattr(response, '_headers'))
cookies = proxy_response.headers.getlist('set-cookie')
logger.info('Checking for invalid cookies')
for cookie_string in cookies:
cookie_dict = cookie_from_string(cookie_string,
strict_cookies=strict_cookies)
# if cookie is invalid cookie_dict will be None
if cookie_dict:
response.set_cookie(**cookie_dict)
logger.debug('Response cookies: %s', response.cookies)
return response

View File

@ -0,0 +1,42 @@
"""passbook app_gw rewriter"""
from passbook.app_gw.models import RewriteRule
RULE_CACHE = {}
class Context:
"""Empty class which we dynamically add attributes to"""
class Rewriter:
"""Apply rewrites"""
__application = None
__request = None
def __init__(self, application, request):
self.__application = application
self.__request = request
if self.__application.pk not in RULE_CACHE:
RULE_CACHE[self.__application.pk] = RewriteRule.objects.filter(
provider__in=[self.__application])
def __build_context(self, matches):
"""Build object with .0, .1, etc as groups and give access to request"""
context = Context()
for index, group_match in enumerate(matches.groups()):
setattr(context, "g%d" % (index + 1), group_match)
setattr(context, 'request', self.__request)
return context
def build(self):
"""Run all rules over path and return final path"""
path = self.__request.get_full_path()
for rule in RULE_CACHE[self.__application.pk]:
matches = rule.compiled_matcher.search(path)
if not matches:
continue
replace_context = self.__build_context(matches)
path = rule.replacement.format(context=replace_context)
if rule.halt:
return path
return path

View File

@ -0,0 +1,227 @@
"""Utils from django-revproxy, slightly adjusted"""
import logging
import re
from wsgiref.util import is_hop_by_hop
try:
from http.cookies import SimpleCookie
COOKIE_PREFIX = ''
except ImportError:
from Cookie import SimpleCookie
COOKIE_PREFIX = 'Set-Cookie: '
#: List containing string constant that are used to represent headers that can
#: be ignored in the required_header function
IGNORE_HEADERS = (
'HTTP_ACCEPT_ENCODING', # We want content to be uncompressed so
# we remove the Accept-Encoding from
# original request
'HTTP_HOST',
'HTTP_REMOTE_USER',
)
# Default from HTTP RFC 2616
# See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
#: Variable that represent the default charset used
DEFAULT_CHARSET = 'latin-1'
#: List containing string constants that represents possible html content type
HTML_CONTENT_TYPES = (
'text/html',
'application/xhtml+xml'
)
#: Variable used to represent a minimal content size required for response
#: to be turned into stream
MIN_STREAMING_LENGTH = 4 * 1024 # 4KB
#: Regex used to find charset in a html content type
_get_charset_re = re.compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
def is_html_content_type(content_type):
"""Function used to verify if the parameter is a proper html content type
:param content_type: String variable that represent a content-type
:returns: A boolean value stating if the content_type is a valid html
content type
"""
for html_content_type in HTML_CONTENT_TYPES:
if content_type.startswith(html_content_type):
return True
return False
def should_stream(proxy_response):
"""Function to verify if the proxy_response must be converted into
a stream.This will be done by checking the proxy_response content-length
and verify if its length is bigger than one stipulated
by MIN_STREAMING_LENGTH.
:param proxy_response: An Instance of urllib3.response.HTTPResponse
:returns: A boolean stating if the proxy_response should
be treated as a stream
"""
content_type = proxy_response.headers.get('Content-Type')
if is_html_content_type(content_type):
return False
try:
content_length = int(proxy_response.headers.get('Content-Length', 0))
except ValueError:
content_length = 0
if not content_length or content_length > MIN_STREAMING_LENGTH:
return True
return False
def get_charset(content_type):
"""Function used to retrieve the charset from a content-type.If there is no
charset in the content type then the charset defined on DEFAULT_CHARSET
will be returned
:param content_type: A string containing a Content-Type header
:returns: A string containing the charset
"""
if not content_type:
return DEFAULT_CHARSET
matched = _get_charset_re.search(content_type)
if matched:
# Extract the charset and strip its double quotes
return matched.group('charset').replace('"', '')
return DEFAULT_CHARSET
def required_header(header):
"""Function that verify if the header parameter is a essential header
:param header: A string represented a header
:returns: A boolean value that represent if the header is required
"""
if header in IGNORE_HEADERS:
return False
if header.startswith('HTTP_') or header == 'CONTENT_TYPE':
return True
return False
def set_response_headers(response, response_headers):
"""Set response's header"""
for header, value in response_headers.items():
if is_hop_by_hop(header) or header.lower() == 'set-cookie':
continue
response[header.title()] = value
logger.debug('Response headers: %s', getattr(response, '_headers'))
def normalize_request_headers(request):
"""Function used to transform header, replacing 'HTTP\\_' to ''
and replace '_' to '-'
:param request: A HttpRequest that will be transformed
:returns: A dictionary with the normalized headers
"""
norm_headers = {}
for header, value in request.META.items():
if required_header(header):
norm_header = header.replace('HTTP_', '').title().replace('_', '-')
norm_headers[norm_header] = value
return norm_headers
def encode_items(items):
"""Function that encode all elements in the list of items passed as
a parameter
:param items: A list of tuple
:returns: A list of tuple with all items encoded in 'utf-8'
"""
encoded = []
for key, values in items:
for value in values:
encoded.append((key.encode('utf-8'), value.encode('utf-8')))
return encoded
logger = logging.getLogger('revproxy.cookies')
def cookie_from_string(cookie_string, strict_cookies=False):
"""Parser for HTTP header set-cookie
The return from this function will be used as parameters for
django's response.set_cookie method. Because set_cookie doesn't
have parameter comment, this cookie attribute will be ignored.
:param cookie_string: A string representing a valid cookie
:param strict_cookies: Whether to only accept RFC-compliant cookies
:returns: A dictionary containing the cookie_string attributes
"""
if strict_cookies:
cookies = SimpleCookie(COOKIE_PREFIX + cookie_string)
if not cookies.keys():
return None
cookie_name, = cookies.keys()
cookie_dict = {k: v for k, v in cookies[cookie_name].items()
if v and k != 'comment'}
cookie_dict['key'] = cookie_name
cookie_dict['value'] = cookies[cookie_name].value
return cookie_dict
valid_attrs = ('path', 'domain', 'comment', 'expires',
'max_age', 'httponly', 'secure')
cookie_dict = {}
cookie_parts = cookie_string.split(';')
try:
cookie_dict['key'], cookie_dict['value'] = \
cookie_parts[0].split('=', 1)
cookie_dict['value'] = cookie_dict['value'].replace('"', '')
# print('aaaaaaaaaaaaaaaaaaaaaaaaaaaa')
# print(cookie_parts[0].split('=', 1))
except ValueError:
logger.warning('Invalid cookie: `%s`', cookie_string)
return None
if cookie_dict['value'].startswith('='):
logger.warning('Invalid cookie: `%s`', cookie_string)
return None
for part in cookie_parts[1:]:
if '=' in part:
attr, value = part.split('=', 1)
value = value.strip()
else:
attr = part
value = ''
attr = attr.strip().lower()
if not attr:
continue
if attr in valid_attrs:
if attr in ('httponly', 'secure'):
cookie_dict[attr] = True
elif attr in 'comment':
# ignoring comment attr as explained in the
# function docstring
continue
else:
cookie_dict[attr] = value
else:
logger.warning('Unknown cookie attribute %s', attr)
return cookie_dict

View File

@ -0,0 +1,7 @@
django-revproxy
urllib3[secure]
channels
service_identity
websocket-client
daphne<2.3.0
asgiref~=2.3

View File

@ -0,0 +1,5 @@
"""Application Security Gateway settings"""
INSTALLED_APPS = [
'channels'
]
ASGI_APPLICATION = "passbook.app_gw.websocket.routing.application"

View File

@ -0,0 +1,20 @@
"""passbook app_gw cache clean signals"""
from logging import getLogger
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.handler import IGNORED_HOSTNAMES_KEY
LOGGER = getLogger(__name__)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_app_gw_cache(sender, instance, **kwargs):
"""Invalidate Policy cache when app_gw is updated"""
if isinstance(instance, ApplicationGatewayProvider):
LOGGER.debug("Invalidating cache for ignored hostnames")
cache.delete(IGNORED_HOSTNAMES_KEY)

2
passbook/app_gw/urls.py Normal file
View File

@ -0,0 +1,2 @@
"""passbook app_gw urls"""
urlpatterns = []

View File

View File

@ -0,0 +1,83 @@
"""websocket proxy consumer"""
import threading
from logging import getLogger
from ssl import CERT_NONE
import websocket
from channels.generic.websocket import WebsocketConsumer
from passbook.app_gw.models import ApplicationGatewayProvider
LOGGER = getLogger(__name__)
class ProxyConsumer(WebsocketConsumer):
"""Proxy websocket connection to upstream"""
_headers_dict = {}
_app_gw = None
_client = None
_thread = None
def _fix_headers(self, input_dict):
"""Fix headers from bytestrings to normal strings"""
return {
key.decode('utf-8'): value.decode('utf-8')
for key, value in dict(input_dict).items()
}
def connect(self):
"""Extract host header, lookup in database and proxy connection"""
self._headers_dict = self._fix_headers(dict(self.scope.get('headers')))
host = self._headers_dict.pop('host')
query_string = self.scope.get('query_string').decode('utf-8')
matches = ApplicationGatewayProvider.objects.filter(
server_name__contains=[host],
enabled=True)
if matches.exists():
self._app_gw = matches.first()
# TODO: Get upstream that starts with wss or
upstream = self._app_gw.upstream[0].replace('http', 'ws') + self.scope.get('path')
if query_string:
upstream += '?' + query_string
sslopt = {}
if not self._app_gw.upstream_ssl_verification:
sslopt = {"cert_reqs": CERT_NONE}
self._client = websocket.WebSocketApp(
url=upstream,
subprotocols=self.scope.get('subprotocols'),
header=self._headers_dict,
on_message=self._client_on_message_handler(),
on_error=self._client_on_error_handler(),
on_close=self._client_on_close_handler(),
on_open=self._client_on_open_handler())
LOGGER.debug("Accepting connection for %s", host)
self._thread = threading.Thread(target=lambda: self._client.run_forever(sslopt=sslopt))
self._thread.start()
def _client_on_open_handler(self):
return lambda ws: self.accept(self._client.sock.handshake_response.subprotocol)
def _client_on_message_handler(self):
# pylint: disable=unused-argument,invalid-name
def message_handler(ws, message):
if isinstance(message, str):
self.send(text_data=message)
else:
self.send(bytes_data=message)
return message_handler
def _client_on_error_handler(self):
return lambda ws, error: print(error)
def _client_on_close_handler(self):
return lambda ws: self.disconnect(0)
def disconnect(self, code):
self._client.close()
def receive(self, text_data=None, bytes_data=None):
if text_data:
opcode = websocket.ABNF.OPCODE_TEXT
if bytes_data:
opcode = websocket.ABNF.OPCODE_BINARY
self._client.send(text_data or bytes_data, opcode)

View File

@ -0,0 +1,17 @@
"""app_gw websocket proxy"""
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from passbook.app_gw.websocket.consumer import ProxyConsumer
websocket_urlpatterns = [
url(r'^(.*)$', ProxyConsumer),
]
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
})

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

View File

@ -1,2 +1,2 @@
"""passbook core"""
__version__ = '0.1.15-beta'
__version__ = '0.1.35-beta'

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