Compare commits
84 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
34ed0b3594 | |||
f008a3e20c | |||
9de950220f | |||
567c90b4c6 | |||
ae19236366 | |||
f9babe7089 | |||
78c74cd469 | |||
32abb27e61 | |||
8478b03892 | |||
e972f2b289 | |||
22c4fb1414 | |||
0154def916 | |||
fc69b6851d | |||
44a3c7fa5f | |||
4e6653e299 | |||
c782585287 | |||
7718b3b3b8 | |||
8ff9e72972 | |||
ef6ef68a39 | |||
48a04744e0 | |||
6446ca8bb2 | |||
b9991465ee | |||
3d8242be06 | |||
ca3bcc565d | |||
432176ea2f | |||
c1dae0b599 | |||
e70d3b6286 | |||
17e6bc921b | |||
46111e7cac | |||
3b7e47dbe2 | |||
fff99f0e3d | |||
2e15b24f0a | |||
088b9592cd | |||
b1e4e32b83 | |||
d91a852eda | |||
171c5b9759 | |||
64290b2a37 | |||
72769b8a0a | |||
1018309413 | |||
6d0ecd228e | |||
40a651e66c | |||
a390bb7b59 | |||
245ec65cbb | |||
17eea4a10c | |||
862fb0f5d2 | |||
ec73b53340 | |||
9110f7fee3 | |||
54cc1fdeef | |||
8f42a7f0b4 | |||
2c221ea819 | |||
93e0441b58 | |||
7f1455cb12 | |||
59fc223a85 | |||
0a6f555c23 | |||
6a4233d6fd | |||
15fa7e9652 | |||
f2acc154cd | |||
d21ec6c9a5 | |||
43dd858cd5 | |||
34cbf5f702 | |||
3c6e94b6a8 | |||
1cd149c815 | |||
4c6f562805 | |||
e59c4ec1c7 | |||
1169db7530 | |||
1453008796 | |||
2209b6d603 | |||
ccbc0384f9 | |||
a48924c896 | |||
dc8d8dd2b6 | |||
afca94ceb8 | |||
0b86231a36 | |||
c0df1f38b8 | |||
2b8fed8f4e | |||
c7322a32a0 | |||
64b75cab84 | |||
f58bc61999 | |||
fb8ccc0283 | |||
c38012f147 | |||
3676ff21c2 | |||
920e705d75 | |||
de0b137b1e | |||
d44ac6e2a3 | |||
71039a4012 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.4.0-beta
|
||||
current_version = 0.6.6-beta
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -23,5 +23,5 @@ values =
|
||||
|
||||
[bumpversion:file:passbook/__init__.py]
|
||||
|
||||
[bumpversion:file:passbook/core/nginx.conf]
|
||||
[bumpversion:file:docker/nginx.conf]
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
[run]
|
||||
source = passbook
|
||||
omit =
|
||||
env/
|
||||
*/wsgi.py
|
||||
manage.py
|
||||
*/migrations/*
|
||||
|
@ -2,3 +2,4 @@ env
|
||||
helm
|
||||
passbook-ui
|
||||
static
|
||||
*.env.yml
|
||||
|
@ -27,7 +27,7 @@ create-base-image:
|
||||
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/base.Dockerfile --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.4.0-beta
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest
|
||||
stage: build-base-image
|
||||
only:
|
||||
refs:
|
||||
@ -41,7 +41,7 @@ build-dev-image:
|
||||
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/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.4.0-beta
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest
|
||||
stage: build-dev-image
|
||||
only:
|
||||
refs:
|
||||
@ -70,13 +70,13 @@ migrations:
|
||||
# services:
|
||||
# - postgres:latest
|
||||
# - redis:latest
|
||||
# pylint:
|
||||
# script:
|
||||
# - pylint passbook
|
||||
# stage: test
|
||||
# services:
|
||||
# - postgres:latest
|
||||
# - redis:latest
|
||||
pylint:
|
||||
script:
|
||||
- pylint passbook
|
||||
stage: test
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
coverage:
|
||||
script:
|
||||
- coverage run manage.py test
|
||||
@ -87,15 +87,15 @@ coverage:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
|
||||
package-passbook-server:
|
||||
build-passbook-server:
|
||||
stage: build
|
||||
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 --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.4.0-beta
|
||||
stage: build
|
||||
- /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.6.6-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
@ -107,7 +107,7 @@ build-passbook-static:
|
||||
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/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.4.0-beta
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.6.6-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
@ -124,7 +124,7 @@ package-helm:
|
||||
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
|
||||
script:
|
||||
- helm init --client-only
|
||||
- helm dependency build helm/passbook
|
||||
- helm dependency update helm/passbook
|
||||
- helm package helm/passbook
|
||||
artifacts:
|
||||
paths:
|
||||
|
@ -3,11 +3,9 @@ test-warnings: true
|
||||
doc-warnings: false
|
||||
|
||||
ignore-paths:
|
||||
- env
|
||||
- migrations
|
||||
- docs
|
||||
- node_modules
|
||||
- client-packages
|
||||
|
||||
uses:
|
||||
- django
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
#,pylint.extensions.docparams
|
||||
extension-pkg-whitelist=lxml
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
ignored-modules=django-otp
|
||||
jobs=4
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
|
@ -2,7 +2,8 @@ FROM docker.beryju.org/passbook/base:latest
|
||||
|
||||
COPY ./passbook/ /app/passbook
|
||||
COPY ./manage.py /app/
|
||||
|
||||
USER passbook
|
||||
COPY ./docker/uwsgi.ini /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
USER passbook
|
||||
|
18
Pipfile
18
Pipfile
@ -4,15 +4,11 @@ url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[packages]
|
||||
asgiref = "*"
|
||||
beautifulsoup4 = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
cherrypy = "*"
|
||||
colorlog = "*"
|
||||
daphne = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
kombu = "==4.5.0"
|
||||
django-cors-middleware = "*"
|
||||
django-filters = "*"
|
||||
django-ipware = "*"
|
||||
@ -23,15 +19,13 @@ django-otp = "*"
|
||||
django-recaptcha = "*"
|
||||
django-redis = "*"
|
||||
django-rest-framework = "*"
|
||||
django-revproxy = "*"
|
||||
djangorestframework = "==3.9.4"
|
||||
drf-yasg = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
markdown = "*"
|
||||
oauthlib = "*"
|
||||
packaging = "*"
|
||||
psycopg2 = "*"
|
||||
psycopg2-binary = "*"
|
||||
pycryptodome = "*"
|
||||
pyyaml = "*"
|
||||
qrcode = "*"
|
||||
@ -40,20 +34,18 @@ sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
signxml = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
websocket_client = "*"
|
||||
structlog = "*"
|
||||
uwsgi = "*"
|
||||
pyuwsgi = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[dev-packages]
|
||||
astroid = "==2.2.5"
|
||||
coverage = "*"
|
||||
isort = "*"
|
||||
pylint = "==2.3.1"
|
||||
pylint-django = "==2.0.10"
|
||||
prospector = "==1.1.7"
|
||||
pylint-django = "*"
|
||||
prospector = "*"
|
||||
django-debug-toolbar = "*"
|
||||
bumpversion = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
|
555
Pipfile.lock
generated
555
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "cd82871d9aca8cfd548a6a62856196b2211524f12fbd416dfe5218aad9471e44"
|
||||
"sha256": "94b3d5140f0c31dac1fc77af75a0df30ae4fb0571bf6b7fcd722487c63dc1872"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -18,62 +18,24 @@
|
||||
"default": {
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68",
|
||||
"sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a"
|
||||
"sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
|
||||
"sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
|
||||
],
|
||||
"version": "==2.5.1"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:a4ce726e6ef49cca13642ff49588530ebabcc47c669c7a95af37ea5a74b9b823",
|
||||
"sha256:f62b1c88ebf5fe95db202a372982970edcf375c1513d7e70717df0750f5c2b98"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.2"
|
||||
"version": "==2.5.2"
|
||||
},
|
||||
"asn1crypto": {
|
||||
"hashes": [
|
||||
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
|
||||
"sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
|
||||
"sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292",
|
||||
"sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f"
|
||||
],
|
||||
"version": "==0.24.0"
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||
"sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2",
|
||||
"sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"autobahn": {
|
||||
"hashes": [
|
||||
"sha256:27688cbddd5545fc2ee2614ec8fa65119f1a2122606ce2ef7756392c33e3ec0f",
|
||||
"sha256:a24826ad0bcc35d32cb4576a092fa744e8b6738bd6320d2de857ad8a71df0bec"
|
||||
],
|
||||
"version": "==19.9.3"
|
||||
},
|
||||
"automat": {
|
||||
"hashes": [
|
||||
"sha256:cbd78b83fa2d81fe2a4d23d258e1661dd7493c9a50ee2f1a5b2cac61c1793b0e",
|
||||
"sha256:fdccab66b68498af9ecfa1fa43693abe546014dd25cf28543cbe9d1334916a58"
|
||||
],
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"backports.functools-lru-cache": {
|
||||
"hashes": [
|
||||
"sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a",
|
||||
"sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd"
|
||||
],
|
||||
"version": "==1.5"
|
||||
},
|
||||
"beautifulsoup4": {
|
||||
"hashes": [
|
||||
"sha256:05668158c7b85b791c5abde53e50265e16f98ad601c402ba44d70f96c4159612",
|
||||
"sha256:25288c9e176f354bf277c0a10aa96c782a6a18a17122dba2e8cec4a97e03343b",
|
||||
"sha256:f040590be10520f2ea4c2ae8c3dae441c7cfff5308ec9d58a0ec0c1b8f81d469"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.8.0"
|
||||
"version": "==19.2.0"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
@ -92,10 +54,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
||||
],
|
||||
"version": "==2019.6.16"
|
||||
"version": "==2019.9.11"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@ -130,14 +92,6 @@
|
||||
],
|
||||
"version": "==1.12.3"
|
||||
},
|
||||
"channels": {
|
||||
"hashes": [
|
||||
"sha256:9191a85800673b790d1d74666fb7676f430600b71b662581e97dd69c9aedd29a",
|
||||
"sha256:af7cdba9efb3f55b939917d1b15defb5d40259936013e60660e5e9aff98db4c5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
@ -147,33 +101,18 @@
|
||||
},
|
||||
"cheroot": {
|
||||
"hashes": [
|
||||
"sha256:427e7e3ce51ad5a6e5cf953252b5782d5dfbeb544c09910634971bc06df6621b",
|
||||
"sha256:74d733c55178812253d855990f7ad7b31ab4ee8dab80e4803bd5e52299c50395"
|
||||
"sha256:3ff64073efa35b39d5e107410f5c79664dc8c6c5990651e970740c80ab8878a8",
|
||||
"sha256:d523a1525258730026aa35b86c8c47c8d0e3892fb89f0f39157d4b32a50edf05"
|
||||
],
|
||||
"version": "==6.5.8"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"cherrypy": {
|
||||
"hashes": [
|
||||
"sha256:16fc226a280cd772ede7c309d3964002196784ac6615d8bface52be12ff51230",
|
||||
"sha256:488ea5e639885c75330686c1d7d3dfbd002f784c027a3fe5b374b41926b8cba3"
|
||||
"sha256:033368d25fcc6bca143e7efe9adbfd3a6d91cc0d90c37a649261935f116aafab",
|
||||
"sha256:683e687e7c7b1ba31ef86a113b1eafd0407269fed175bf488d3c839d37d1cc60"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"colorlog": {
|
||||
"hashes": [
|
||||
"sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42",
|
||||
"sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.2"
|
||||
},
|
||||
"constantly": {
|
||||
"hashes": [
|
||||
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
|
||||
"sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"
|
||||
],
|
||||
"version": "==15.1.0"
|
||||
"version": "==18.3.0"
|
||||
},
|
||||
"coreapi": {
|
||||
"hashes": [
|
||||
@ -210,14 +149,6 @@
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"daphne": {
|
||||
"hashes": [
|
||||
"sha256:2329b7a74b5559f7ea012879c10ba945c3a53df7d8d2b5932a904e3b4c9abcc2",
|
||||
"sha256:3cae286a995ae5b127d7de84916f0480cb5be19f81125b6a150b8326250dadd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
@ -228,11 +159,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa",
|
||||
"sha256:deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56"
|
||||
"sha256:4025317ca01f75fc79250ff7262a06d8ba97cd4f82e93394b2a0a6a4a925caeb",
|
||||
"sha256:a8ca1033acac9f33995eb2209a6bf18a4681c3e5269a878e9a7e0b7384ed1ca3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.5"
|
||||
"version": "==2.2.6"
|
||||
},
|
||||
"django-cors-middleware": {
|
||||
"hashes": [
|
||||
@ -280,11 +211,11 @@
|
||||
},
|
||||
"django-otp": {
|
||||
"hashes": [
|
||||
"sha256:246b11ee38ec1cea2e2312311a830740d1a8d0384ba15e7b70e03f851d790157",
|
||||
"sha256:cefbf5e7295498c767752d77828ce3f56cdb0373915e56fe4f87d99604742394"
|
||||
"sha256:79c8253be97246df86540d551dc705e8fe6ca76af8e8c77f78314cd1b513c2cf",
|
||||
"sha256:c5bf3916dca5d53cb377aa6dea40aa785c164013fbf750384137362dfa278cf5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
"version": "==0.7.2"
|
||||
},
|
||||
"django-recaptcha": {
|
||||
"hashes": [
|
||||
@ -309,29 +240,20 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"django-revproxy": {
|
||||
"hashes": [
|
||||
"sha256:0b539736e438aad3cd8b34563125783678f65bcb847970c95d8e9820e6dc88b3",
|
||||
"sha256:b2c6244aaf53fbbecb79084bf507761754b36895c0f6d01349066e9a355e8455"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.15"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
"sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651",
|
||||
"sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb"
|
||||
"sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8",
|
||||
"sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.4"
|
||||
"version": "==3.10.3"
|
||||
},
|
||||
"drf-yasg": {
|
||||
"hashes": [
|
||||
"sha256:68fded2ffdf46e03f33e766184b7d8f1e1a5236f94acfd0c4ba932a57b812566",
|
||||
"sha256:fcef74709ead2b365410be3d12afbfd0a6e49d1efe615a15a929da7e950bb83c"
|
||||
"sha256:4cfec631880ae527a91ec7cd3241aea2f82189f59e2f089119aa687761afb227",
|
||||
"sha256:504cce09035cf1bace63b84d9d778b772f86bb37d8a71ed6f723346362e633b2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.16.1"
|
||||
"version": "==1.17.0"
|
||||
},
|
||||
"eight": {
|
||||
"hashes": [
|
||||
@ -346,13 +268,6 @@
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"hyperlink": {
|
||||
"hashes": [
|
||||
"sha256:4288e34705da077fada1111a24a0aa08bb1e76699c9ce49876af722441845654",
|
||||
"sha256:ab4a308feb039b04f855a020a6eda3b18ca5a68e6d8f8c899cbe9e653721d04f"
|
||||
],
|
||||
"version": "==19.0.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
@ -360,20 +275,6 @@
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:0c505102757e7fa28b9f0958d8bc81301159dea16e2649858c92edc158b78a83",
|
||||
"sha256:9a9f75ce32e78170905888acbf2376a81d3f21ecb3bb4867050413411d3ca7a9"
|
||||
],
|
||||
"version": "==0.21"
|
||||
},
|
||||
"incremental": {
|
||||
"hashes": [
|
||||
"sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f",
|
||||
"sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3"
|
||||
],
|
||||
"version": "==17.5.0"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
|
||||
@ -395,17 +296,18 @@
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
||||
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
||||
],
|
||||
"version": "==2.10.1"
|
||||
"version": "==2.10.3"
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:55274dc75eb3c3994538b0973a0fadddb236b698a4bc135b8aa4981e0a710b8f",
|
||||
"sha256:e5f0312dfb9011bebbf528ccaf118a6c2b5c3b8244451f08381fb23e7715809b"
|
||||
"sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd",
|
||||
"sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181"
|
||||
],
|
||||
"version": "==4.6.4"
|
||||
"index": "pypi",
|
||||
"version": "==4.5.0"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
@ -501,11 +403,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
|
||||
"sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
|
||||
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
|
||||
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.1"
|
||||
"version": "==19.2"
|
||||
},
|
||||
"portend": {
|
||||
"hashes": [
|
||||
@ -514,19 +416,36 @@
|
||||
],
|
||||
"version": "==2.5"
|
||||
},
|
||||
"psycopg2": {
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:128d0fa910ada0157bba1cb74a9c5f92bb8a1dca77cf91a31eb274d1f889e001",
|
||||
"sha256:227fd46cf9b7255f07687e5bde454d7d67ae39ca77e170097cdef8ebfc30c323",
|
||||
"sha256:2315e7f104681d498ccf6fd70b0dba5bce65d60ac92171492bfe228e21dcc242",
|
||||
"sha256:4b5417dcd2999db0f5a891d54717cfaee33acc64f4772c4bc574d4ff95ed9d80",
|
||||
"sha256:640113ddc943522aaf71294e3f2d24013b0edd659b7820621492c9ebd3a2fb0b",
|
||||
"sha256:897a6e838319b4bf648a574afb6cabcb17d0488f8c7195100d48d872419f4457",
|
||||
"sha256:8dceca81409898c870e011c71179454962dec152a1a6b86a347f4be74b16d864",
|
||||
"sha256:b1b8e41da09a0c3ef0b3d4bb72da0dde2abebe583c1e8462973233fd5ad0235f",
|
||||
"sha256:cb407fccc12fc29dc331f2b934913405fa49b9b75af4f3a72d0f50f57ad2ca23",
|
||||
"sha256:d3a27550a8185e53b244ad7e79e307594b92fede8617d80200a8cce1fba2c60f",
|
||||
"sha256:f0e6b697a975d9d3ccd04135316c947dd82d841067c7800ccf622a8717e98df1"
|
||||
"sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809",
|
||||
"sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598",
|
||||
"sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5",
|
||||
"sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1",
|
||||
"sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d",
|
||||
"sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e",
|
||||
"sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00",
|
||||
"sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf",
|
||||
"sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43",
|
||||
"sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5",
|
||||
"sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70",
|
||||
"sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6",
|
||||
"sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd",
|
||||
"sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877",
|
||||
"sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3",
|
||||
"sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67",
|
||||
"sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68",
|
||||
"sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b",
|
||||
"sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a",
|
||||
"sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b",
|
||||
"sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2",
|
||||
"sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e",
|
||||
"sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e",
|
||||
"sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f",
|
||||
"sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f",
|
||||
"sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7",
|
||||
"sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737",
|
||||
"sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.3"
|
||||
@ -540,10 +459,10 @@
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:43c17a83c155229839cc5c6b868e8d0c6041dba149789b6d6e28801c64821722",
|
||||
"sha256:e30199a9d221f1b26c885ff3d87fd08694dbbe18ed0e8e405a2a7126d30ce4c0"
|
||||
"sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b",
|
||||
"sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3"
|
||||
],
|
||||
"version": "==0.2.6"
|
||||
"version": "==0.2.7"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
@ -618,13 +537,6 @@
|
||||
],
|
||||
"version": "==3.9.0"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
"hashes": [
|
||||
"sha256:6b672c02fdf7470df9674ab82263841ce8333fb143f32f021f6cb26f0e512420",
|
||||
"sha256:8ffaa0a53da57e89de14ced7185ac746227a8894dbd5a3c718bf05ddbd1d56cd"
|
||||
],
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"pyjwkest": {
|
||||
"hashes": [
|
||||
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
|
||||
@ -647,10 +559,40 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.2"
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"pyuwsgi": {
|
||||
"hashes": [
|
||||
"sha256:15a4626740753b0d0dfeeac7d367f9b2e89ab6af16c195927e60f75359fc1bbc",
|
||||
"sha256:24c40c3b889eb9f283d43feffbc0f7c7fc024e914451425156ddb68af3df1e71",
|
||||
"sha256:393737bd43a7e38f0a4a1601a37a69c4bf893635b37665ff958170fdb604fdb7",
|
||||
"sha256:5a08308f87e639573c1efaa5966a6d04410cd45a73c4586a932fe3ee4b56369d",
|
||||
"sha256:5f4b36c0dbb9931c4da8008aa423158be596e3b4a23cec95a958631603a94e45",
|
||||
"sha256:7c31794f71bbd0ccf542cab6bddf38aa69e84e31ae0f9657a2e18ebdc150c01a",
|
||||
"sha256:802ec6dad4b6707b934370926ec1866603abe31ba03c472f56149001b3533ba1",
|
||||
"sha256:814d73d4569add69a6c19bb4a27cd5adb72b196e5e080caed17dbda740402072",
|
||||
"sha256:829299cd117cf8abe837796bf587e61ce6bfe18423a3a1c510c21e9825789c2c",
|
||||
"sha256:85f2210ceae5f48b7d8fad2240d831f4b890cac85cd98ca82683ac6aa481dfc8",
|
||||
"sha256:861c94442b28cd64af033e88e0f63c66dbd5609f67952dc18694098b47a43f3a",
|
||||
"sha256:957bc6316ffc8463795d56d9953d58e7f32aa5aad1c5ac80bc45c69f3299961e",
|
||||
"sha256:9760c3f56fb5f15852d163429096600906478e9ed2c189a52f2bb21d8a2a986c",
|
||||
"sha256:a4b24703ea818196d0be1dc64b3b57b79c67e8dee0cfa207a4216220912035a7",
|
||||
"sha256:ad7f4968c1ddbf139a306d9b075360d959cc554d994ba5e1f512af9a40e62357",
|
||||
"sha256:b1127d34b90f74faf1707718c57a4193ac028b9f4aec0238638983132297d456",
|
||||
"sha256:bcb04d6ec644b3e08d03c64851e06edd7110489261e50627a4bcadf66ff6920e",
|
||||
"sha256:bebfebb9ee83d7cf37668bf54275b677b7ae283e84a944f9f3ac6a4b66f95d4b",
|
||||
"sha256:c29892dafc65a8b6eb95823fa4bac7754ca3fd1c28ab8d2a973289531b340a27",
|
||||
"sha256:cb296b50b51ba022b0090b28d032ff1dd395a6db03672b65a39e83532edad527",
|
||||
"sha256:ce777ebdf49ce736fc04abf555b5c41ab3f130127543a689dcf8d4871cd18fe4",
|
||||
"sha256:d8b4bf930b6a19bc9ee982b9163d948c87501ad91b71516924e8ed25fe85d2ee",
|
||||
"sha256:e2a420f2c4d35f3ec0b7e752a80d7bd385e2c5a64f67c05f2d2d74230e3114b6",
|
||||
"sha256:fed899ce96f4f2b4d1b9f338dd145a4040ee1d8a5152213af0dd8d4a4d36e9fe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.18.post0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@ -710,35 +652,35 @@
|
||||
},
|
||||
"ruamel.yaml.clib": {
|
||||
"hashes": [
|
||||
"sha256:0bbe19d3e099f8ba384e1846e6b54f245f58aeec8700edbbf9abb87afa54fd82",
|
||||
"sha256:2f38024592613f3a8772bbc2904be027d9abf463518ba145f2d0c8e6da27009f",
|
||||
"sha256:44449b3764a3f75815eea8ae5930b98e8326be64a90b0f782747318f861abfe0",
|
||||
"sha256:5710be9a357801c31c1eaa37b9bc92d38176d785af5b2f0c9751385c5dc9659a",
|
||||
"sha256:5a089acb6833ed5f412e24cbe3e665683064c1429824d2819137b5ade54435c3",
|
||||
"sha256:6143386ddd61599ea081c012a69a16e5bdd7b3c6c231bd039534365a48940f30",
|
||||
"sha256:6726aaf851f5f9e4cbdd3e1e414bc700bdd39220e8bc386415fd41c87b1b53c2",
|
||||
"sha256:68fbc3b5d94d145a391452f886ae5fca240cb7e3ab6bd66e1a721507cdaac28a",
|
||||
"sha256:75ebddf99ba9e0b48f32b5bdcf9e5a2b84c017da9e0db7bf11995fa414aa09cd",
|
||||
"sha256:79948a6712baa686773a43906728e20932c923f7b2a91be7347993be2d745e55",
|
||||
"sha256:8a2dd8e8b08d369558cade05731172c4b5e2f4c5097762c6b352bd28fd9f9dc4",
|
||||
"sha256:c747acdb5e8c242ab2280df6f0c239e62838af4bee647031d96b3db2f9cefc04",
|
||||
"sha256:cadc8eecd27414dca30366b2535cb5e3f3b47b4e2d6be7a0b13e4e52e459ff9f",
|
||||
"sha256:cee86ecc893a6a8ecaa7c6a9c2d06f75f614176210d78a5f155f8e78d6989509",
|
||||
"sha256:e59af39e895aff28ee5f55515983cab3466d1a029c91c04db29da1c0f09cf333",
|
||||
"sha256:eee7ecd2eee648884fae6c51ae50c814acdcc5d6340dc96c970158aebcd25ac6",
|
||||
"sha256:ef8d4522d231cb9b29f6cdd0edc8faac9d9715c60dc7becbd6eb82c915a98e5b",
|
||||
"sha256:f504d45230cc9abf2810623b924ae048b224a90adb01f97db4e766cfdda8e6eb"
|
||||
"sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6",
|
||||
"sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd",
|
||||
"sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a",
|
||||
"sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9",
|
||||
"sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919",
|
||||
"sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6",
|
||||
"sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784",
|
||||
"sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b",
|
||||
"sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52",
|
||||
"sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448",
|
||||
"sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5",
|
||||
"sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070",
|
||||
"sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c",
|
||||
"sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947",
|
||||
"sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc",
|
||||
"sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973",
|
||||
"sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad",
|
||||
"sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython' and python_version < '3.8'",
|
||||
"version": "==0.1.2"
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:528f936118679e9a52dacb96bfefe20acb5d63e0797856c64a582cc3c2bc1f9e",
|
||||
"sha256:b4edcb1296fee107439345d0f8b23432b8732b7e28407f928367d0a4a36301a9"
|
||||
"sha256:15e51e74b924180c98bcd636cb4634945b0a99a124d50b433c3a9dc6a582e8db",
|
||||
"sha256:1d6a2ee908ec6d8f96c27d78bc39e203df4d586d287c233140af7d8d1aca108a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.11.2"
|
||||
"version": "==0.12.3"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -763,13 +705,6 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"soupsieve": {
|
||||
"hashes": [
|
||||
"sha256:8662843366b8d8779dec4e2f921bebec9afd856a5ff2e82cd419acc5054a1a92",
|
||||
"sha256:a5a6166b4767725fd52ae55fee8c8b6137d9a51e9f1edea461a062a759160118"
|
||||
],
|
||||
"version": "==1.9.3"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
|
||||
@ -777,6 +712,14 @@
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"structlog": {
|
||||
"hashes": [
|
||||
"sha256:5feae03167620824d3ae3e8915ea8589fc28d1ad6f3edf3cc90ed7c7cb33fab5",
|
||||
"sha256:db441b81c65b0f104a7ce5d86c5432be099956b98b8a2c8be0b3fb3a7a0b1536"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"tempora": {
|
||||
"hashes": [
|
||||
"sha256:cb60b1d2b1664104e307f8e5269d7f4acdb077c82e35cd57246ae14a3427d2d6",
|
||||
@ -784,37 +727,6 @@
|
||||
],
|
||||
"version": "==1.14.1"
|
||||
},
|
||||
"twisted": {
|
||||
"hashes": [
|
||||
"sha256:02214ef6f125804969aedd55daccea57060b98dae6a2aa0a4cb60c4d0acb8a2c",
|
||||
"sha256:15b51047ab116ee61d791cf9fe6f037f35e909a6d344ccb437d1691627c4d8a1",
|
||||
"sha256:17704d98d58c9c52d97e88570732e4c094a93fe5df937d01b759bab593345eec",
|
||||
"sha256:222e0cfd60b0c867dd303bce6355a3ffac46574079dff11ae7a1775235ad12c8",
|
||||
"sha256:23090c9fcec01ce4e102912a39eb4645b2bf916abe459804f87853d977ced6e3",
|
||||
"sha256:5102fc2bf0d870c1e217aa09ed7a48b633cc579950a31ecae9cecc556ebffdf2",
|
||||
"sha256:6bc71d5a2320576a3ac7f2dac7802c290fcf9f1972c59f9ef5c5b85b8bac1e1e",
|
||||
"sha256:6c7703b62de08fd5873d60e6ed30478cdb39e3a37b1ead3a5d2fed10deb6e112",
|
||||
"sha256:6ca398abd58730070e9bc34e8a01d1198438b2ff130e95492090a2fec5fb683b",
|
||||
"sha256:98840f28c44894f44dc597747b4cddc740197dc6f6f18ba4dd810422094e35cb",
|
||||
"sha256:998e3baf509c7cf7973b8174c1050ac10f6a8bc1aaf0178ad6a7c422c75a0c68",
|
||||
"sha256:a5f2de00c6630c8f5ad32fca64fc4c853536c21e9ea8d0d2ae54804ef5836b9c",
|
||||
"sha256:aad65a24b27253eb94f2749131a872487b093c599c5873c03d90a65cc9b8a2fc",
|
||||
"sha256:ab788465701f553f764f4442d22b850f39a6a6abd4861e70c05b4c27119c9b50",
|
||||
"sha256:c7244e24fcb72f838be57d3e117ad7df135ff5af4c9d4c565417d671cd1e68c9",
|
||||
"sha256:d5db93026568f60cacdc0615fcd21d46f694a6bfad0ef3ff53cde2b4bb85a39d",
|
||||
"sha256:da92426002703b02d8fccff3acfea2d8baf76a9052e8c55ea76d0407eeaa06ce",
|
||||
"sha256:f4f0af14d288140ecb00861a3bd1e0b94ffdc63057cc1abe8b9dc84f6b6dcf18",
|
||||
"sha256:f985f31e3244d18610816b55becf8fbf445c8e30fe0731500cadaf19f296baf0"
|
||||
],
|
||||
"version": "==19.7.0"
|
||||
},
|
||||
"txaio": {
|
||||
"hashes": [
|
||||
"sha256:67e360ac73b12c52058219bb5f8b3ed4105d2636707a36a7cdafb56fe06db7fe",
|
||||
"sha256:b6b235d432cc58ffe111b43e337db71a5caa5d3eaa88f0eacf60b431c7626ef5"
|
||||
],
|
||||
"version": "==18.8.1"
|
||||
},
|
||||
"uritemplate": {
|
||||
"hashes": [
|
||||
"sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd",
|
||||
@ -828,11 +740,11 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.25.3"
|
||||
"version": "==1.25.6"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
@ -841,61 +753,12 @@
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9",
|
||||
"sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.56.0"
|
||||
},
|
||||
"zc.lockfile": {
|
||||
"hashes": [
|
||||
"sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b",
|
||||
"sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"
|
||||
],
|
||||
"version": "==2.0"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"zope.interface": {
|
||||
"hashes": [
|
||||
"sha256:086707e0f413ff8800d9c4bc26e174f7ee4c9c8b0302fbad68d083071822316c",
|
||||
"sha256:1157b1ec2a1f5bf45668421e3955c60c610e31913cc695b407a574efdbae1f7b",
|
||||
"sha256:11ebddf765bff3bbe8dbce10c86884d87f90ed66ee410a7e6c392086e2c63d02",
|
||||
"sha256:14b242d53f6f35c2d07aa2c0e13ccb710392bcd203e1b82a1828d216f6f6b11f",
|
||||
"sha256:1b3d0dcabc7c90b470e59e38a9acaa361be43b3a6ea644c0063951964717f0e5",
|
||||
"sha256:20a12ab46a7e72b89ce0671e7d7a6c3c1ca2c2766ac98112f78c5bddaa6e4375",
|
||||
"sha256:298f82c0ab1b182bd1f34f347ea97dde0fffb9ecf850ecf7f8904b8442a07487",
|
||||
"sha256:2f6175722da6f23dbfc76c26c241b67b020e1e83ec7fe93c9e5d3dd18667ada2",
|
||||
"sha256:3b877de633a0f6d81b600624ff9137312d8b1d0f517064dfc39999352ab659f0",
|
||||
"sha256:4265681e77f5ac5bac0905812b828c9fe1ce80c6f3e3f8574acfb5643aeabc5b",
|
||||
"sha256:550695c4e7313555549aa1cdb978dc9413d61307531f123558e438871a883d63",
|
||||
"sha256:5f4d42baed3a14c290a078e2696c5f565501abde1b2f3f1a1c0a94fbf6fbcc39",
|
||||
"sha256:62dd71dbed8cc6a18379700701d959307823b3b2451bdc018594c48956ace745",
|
||||
"sha256:7040547e5b882349c0a2cc9b50674b1745db551f330746af434aad4f09fba2cc",
|
||||
"sha256:7e099fde2cce8b29434684f82977db4e24f0efa8b0508179fce1602d103296a2",
|
||||
"sha256:7e5c9a5012b2b33e87980cee7d1c82412b2ebabcb5862d53413ba1a2cfde23aa",
|
||||
"sha256:81295629128f929e73be4ccfdd943a0906e5fe3cdb0d43ff1e5144d16fbb52b1",
|
||||
"sha256:95cc574b0b83b85be9917d37cd2fad0ce5a0d21b024e1a5804d044aabea636fc",
|
||||
"sha256:968d5c5702da15c5bf8e4a6e4b67a4d92164e334e9c0b6acf080106678230b98",
|
||||
"sha256:9e998ba87df77a85c7bed53240a7257afe51a07ee6bc3445a0bf841886da0b97",
|
||||
"sha256:a0c39e2535a7e9c195af956610dba5a1073071d2d85e9d2e5d789463f63e52ab",
|
||||
"sha256:a15e75d284178afe529a536b0e8b28b7e107ef39626a7809b4ee64ff3abc9127",
|
||||
"sha256:a6a6ff82f5f9b9702478035d8f6fb6903885653bff7ec3a1e011edc9b1a7168d",
|
||||
"sha256:b639f72b95389620c1f881d94739c614d385406ab1d6926a9ffe1c8abbea23fe",
|
||||
"sha256:bad44274b151d46619a7567010f7cde23a908c6faa84b97598fd2f474a0c6891",
|
||||
"sha256:bbcef00d09a30948756c5968863316c949d9cedbc7aabac5e8f0ffbdb632e5f1",
|
||||
"sha256:d788a3999014ddf416f2dc454efa4a5dbeda657c6aba031cf363741273804c6b",
|
||||
"sha256:eed88ae03e1ef3a75a0e96a55a99d7937ed03e53d0cffc2451c208db445a2966",
|
||||
"sha256:f99451f3a579e73b5dd58b1b08d1179791d49084371d9a47baad3b22417f0317"
|
||||
],
|
||||
"version": "==4.6.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -921,13 +784,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.6.2"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
||||
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"bumpversion": {
|
||||
"hashes": [
|
||||
"sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
|
||||
@ -936,19 +792,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.5.3"
|
||||
},
|
||||
"certifi": {
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==2019.6.16"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
"index": "pypi",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
@ -990,11 +840,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa",
|
||||
"sha256:deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56"
|
||||
"sha256:4025317ca01f75fc79250ff7262a06d8ba97cd4f82e93394b2a0a6a4a925caeb",
|
||||
"sha256:a8ca1033acac9f33995eb2209a6bf18a4681c3e5269a878e9a7e0b7384ed1ca3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.5"
|
||||
"version": "==2.2.6"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
@ -1004,14 +854,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
|
||||
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
|
||||
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"dodgy": {
|
||||
"hashes": [
|
||||
"sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c"
|
||||
@ -1020,24 +862,17 @@
|
||||
},
|
||||
"gitdb2": {
|
||||
"hashes": [
|
||||
"sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
|
||||
"sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
|
||||
"sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350",
|
||||
"sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"
|
||||
],
|
||||
"version": "==2.0.5"
|
||||
"version": "==2.0.6"
|
||||
},
|
||||
"gitpython": {
|
||||
"hashes": [
|
||||
"sha256:947cc75913e7b6da108458136607e2ee0e40c20be1e12d4284e7c6c12956c276",
|
||||
"sha256:d2f4945f8260f6981d724f5957bc076398ada55cb5d25aaee10108bcdc894100"
|
||||
"sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21",
|
||||
"sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
"version": "==3.0.3"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
@ -1091,13 +926,6 @@
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
|
||||
"sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
|
||||
],
|
||||
"version": "==1.5.0.1"
|
||||
},
|
||||
"prospector": {
|
||||
"hashes": [
|
||||
"sha256:aba551e53dc1a5a432afa67385eaa81d7b4cf4c162dc1a4d0ee00b3a0712ad90"
|
||||
@ -1126,13 +954,6 @@
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
|
||||
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
|
||||
],
|
||||
"version": "==2.4.2"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09",
|
||||
@ -1163,16 +984,17 @@
|
||||
},
|
||||
"pylint-plugin-utils": {
|
||||
"hashes": [
|
||||
"sha256:8d9e31d5ea8b7b0003e1f0f136b44a5235896a32e47c5bc2ef1143e9f6ba0b74"
|
||||
"sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a",
|
||||
"sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"
|
||||
],
|
||||
"version": "==0.5"
|
||||
"version": "==0.6"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.2"
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@ -1193,27 +1015,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==5.1.2"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f",
|
||||
"sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d"
|
||||
],
|
||||
"version": "==24.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"requirements-detector": {
|
||||
"hashes": [
|
||||
"sha256:9fbc4b24e8b7c3663aff32e3eba34596848c6b91bd425079b386973bd8d08931"
|
||||
@ -1242,9 +1043,10 @@
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:713e53b79cbcf97bc5245a06080a33d54a77e7cce2f789c835a143bcdb5c033e"
|
||||
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
|
||||
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
|
||||
],
|
||||
"version": "==1.9.1"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
@ -1260,21 +1062,6 @@
|
||||
],
|
||||
"version": "==1.31.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:1be3e4e3198f2d0e47b928e9d9a8ec1b63525db29095cec1467f4c5a4ea8ebf9",
|
||||
"sha256:7e39a30e3d34a7a6539378e39d7490326253b7ee354878a92255656dc4284457"
|
||||
],
|
||||
"version": "==4.35.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:b2cec0dc1ac55bd74280d257f43763cf0cf928bdcd0de0fd70be70aa1195e3b0",
|
||||
"sha256:e37d5a73d77b095b85314dde807bfb85b580b5b9d137f5b21332f4636990d97a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
@ -1304,24 +1091,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.5.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.25.3"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
|
||||
|
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# passbook
|
||||
|
||||
## Quick instance
|
||||
|
||||
```
|
||||
export PASSBOOK_DOMAIN=domain.tld
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose exec server ./manage.py migrate
|
||||
docker-compose exec server ./manage.py createsuperuser
|
||||
```
|
@ -1,15 +1,20 @@
|
||||
FROM python:3.7-alpine
|
||||
FROM python:3.7-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache openssl-dev build-base libxml2-dev libxslt-dev libffi-dev gcc musl-dev libgcc zlib-dev postgresql-dev && \
|
||||
pip install pipenv --no-cache-dir && \
|
||||
RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv --rm && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
adduser -S passbook && \
|
||||
chown -R passbook /app
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.7-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN pip install -r requirements.txt --no-cache-dir && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
||||
|
@ -1,4 +1,3 @@
|
||||
FROM docker.beryju.org/passbook/base:latest
|
||||
|
||||
RUN pipenv lock --dev -r > requirements-dev.txt && \
|
||||
pip install -r /app/requirements-dev.txt --no-cache-dir
|
||||
RUN pip install -r /app/requirements-dev.txt --no-cache-dir
|
||||
|
90
docker-compose.yml
Normal file
90
docker-compose.yml
Normal file
@ -0,0 +1,90 @@
|
||||
---
|
||||
version: '3.2'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internal
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
- POSTGRES_USER=passbook
|
||||
- POSTGRES_DB=passbook
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
redis:
|
||||
image: redis
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||
command:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.port=8000
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
worker:
|
||||
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||
command:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
static:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: static.Dockerfile
|
||||
image: docker.beryju.org/passbook/static:latest
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.frontend.rule=PathPrefix:/static, /robots.txt
|
||||
- traefik.port=80
|
||||
- traefik.docker.network=internal
|
||||
traefik:
|
||||
image: traefik:1.7
|
||||
command: --api --docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- "0.0.0.0:80:80"
|
||||
- "0.0.0.0:443:443"
|
||||
- "0.0.0.0:8080:8080"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
database:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
internal: {}
|
@ -39,9 +39,9 @@ http {
|
||||
gzip on;
|
||||
gzip_types application/javascript image/* text/css;
|
||||
gunzip on;
|
||||
add_header X-passbook-Version 0.4.0-beta;
|
||||
add_header X-passbook-Version 0.6.6-beta;
|
||||
add_header Vary X-passbook-Version;
|
||||
root /static/;
|
||||
root /data/;
|
||||
|
||||
location /_/healthz {
|
||||
return 204;
|
10
docker/uwsgi.ini
Normal file
10
docker/uwsgi.ini
Normal file
@ -0,0 +1,10 @@
|
||||
[uwsgi]
|
||||
http = 0.0.0.0:8000
|
||||
chdir = /app
|
||||
wsgi-file = passbook/root/wsgi.py
|
||||
processes = 2
|
||||
master = true
|
||||
threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.4.0-beta"
|
||||
appVersion: "0.6.6-beta"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.4.0-beta"
|
||||
version: "0.6.6-beta"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
BIN
helm/passbook/charts/postgresql-4.2.2.tgz
Normal file
BIN
helm/passbook/charts/postgresql-4.2.2.tgz
Normal file
Binary file not shown.
Binary file not shown.
@ -1,12 +1,9 @@
|
||||
dependencies:
|
||||
- name: rabbitmq
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 4.3.2
|
||||
- name: postgresql
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 3.10.1
|
||||
version: 4.2.2
|
||||
- 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
|
||||
version: 9.2.1
|
||||
digest: sha256:8782e974a1094eaeecf1d68f093ca4fb84977217b2bd38b09790a05ec289aec2
|
||||
generated: "2019-10-02T21:03:25.90491153Z"
|
||||
|
@ -1,6 +1,6 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 5.3.9
|
||||
version: 4.2.2
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
- name: redis
|
||||
version: 9.2.1
|
||||
|
@ -1,77 +0,0 @@
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-appgw
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
passbook.io/component: appgw
|
||||
spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- ./manage.py
|
||||
args:
|
||||
- app_gw_web
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "p2.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: P2_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
- name: P2_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: kubernetes-healthcheck-host
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: kubernetes-healthcheck-host
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
memory: 300M
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 500M
|
@ -1,20 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-appgw
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
passbook.io/component: appgw
|
@ -12,101 +12,5 @@ data:
|
||||
host: "{{ .Release.Name }}-redis-master"
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
|
||||
# Error reporting, sends stacktrace to sentry.beryju.org
|
||||
error_report_enabled: {{ .Values.config.error_reporting }}
|
||||
|
||||
{{- if .Values.config.secret_key }}
|
||||
secret_key: {{ .Values.config.secret_key }}
|
||||
{{- else }}
|
||||
secret_key: {{ randAlphaNum 50 }}
|
||||
{{- end }}
|
||||
|
||||
primary_domain: {{ .Values.primary_domain }}
|
||||
domains:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- kubernetes-healthcheck-host
|
||||
|
||||
passbook:
|
||||
sign_up:
|
||||
# Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true
|
||||
enabled: true
|
||||
password_reset:
|
||||
# Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
|
||||
enabled: true
|
||||
# Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
|
||||
verification:
|
||||
- email
|
||||
# Text used in title, on login page and multiple other places
|
||||
branding: passbook
|
||||
login:
|
||||
# Override URL used for logo
|
||||
logo_url: null
|
||||
# Override URL used for Background on Login page
|
||||
bg_url: null
|
||||
# Optionally add a subtext, placed below logo on the login page
|
||||
subtext: null
|
||||
footer:
|
||||
links:
|
||||
# Optionally add links to the footer on the login page
|
||||
# - name: test
|
||||
# href: https://test
|
||||
# Specify which fields can be used to authenticate. Can be any combination of `username` and `email`
|
||||
uid_fields:
|
||||
- username
|
||||
- email
|
||||
session:
|
||||
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
|
||||
# Provider-specific settings
|
||||
ldap:
|
||||
# # Completely enable or disable LDAP provider
|
||||
# enabled: false
|
||||
# # AD Domain, used to generate `userPrincipalName`
|
||||
# domain: corp.contoso.com
|
||||
# # Base DN in which passbook should look for users
|
||||
# base_dn: dn=corp,dn=contoso,dn=com
|
||||
# # LDAP field which is used to set the django username
|
||||
# username_field: sAMAccountName
|
||||
# # LDAP server to connect to, can be set to `<domain_name>`
|
||||
# server:
|
||||
# name: corp.contoso.com
|
||||
# use_tls: false
|
||||
# # Bind credentials, used for account creation
|
||||
# bind:
|
||||
# username: Administraotr@corp.contoso.com
|
||||
# password: VerySecurePassword!
|
||||
# Which field from `uid_fields` maps to which LDAP Attribute
|
||||
login_field_map:
|
||||
username: sAMAccountName
|
||||
email: mail # or userPrincipalName
|
||||
user_attribute_map:
|
||||
active_directory:
|
||||
username: "%(sAMAccountName)s"
|
||||
email: "%(mail)s"
|
||||
name: "%(displayName)"
|
||||
# # Create new users in LDAP upon sign-up
|
||||
# create_users: true
|
||||
# # Reset LDAP password when user reset their password
|
||||
# reset_password: true
|
||||
oauth_client:
|
||||
# List of python packages with sources types to load.
|
||||
types:
|
||||
- passbook.oauth_client.source_types.discord
|
||||
- passbook.oauth_client.source_types.facebook
|
||||
- passbook.oauth_client.source_types.github
|
||||
- passbook.oauth_client.source_types.google
|
||||
- passbook.oauth_client.source_types.reddit
|
||||
- passbook.oauth_client.source_types.supervisr
|
||||
- passbook.oauth_client.source_types.twitter
|
||||
- passbook.oauth_client.source_types.azure_ad
|
||||
saml_idp:
|
||||
signing: true
|
||||
autosubmit: false
|
||||
issuer: passbook
|
||||
assertion_valid_for: 86400
|
||||
# List of python packages with provider types to load.
|
||||
types:
|
||||
- passbook.saml_idp.processors.generic
|
||||
- passbook.saml_idp.processors.salesforce
|
||||
domain: ".{{ .Values.ingress.hosts[0] }}"
|
||||
|
@ -37,14 +37,9 @@ spec:
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- range .Values.ingress.app_gw_hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
- path: /robots.txt
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-appgw
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
11
helm/passbook/templates/secret.yaml
Normal file
11
helm/passbook/templates/secret.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
data:
|
||||
{{- if .Values.config.secret_key }}
|
||||
secret_key: {{ .Values.config.secret_key | b64enc | quote }}
|
||||
{{- else }}
|
||||
secret_key: {{ randAlphaNum 50 | b64enc | quote}}
|
||||
{{- end }}
|
@ -8,7 +8,7 @@ metadata:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
@ -31,24 +31,29 @@ spec:
|
||||
- ./manage.py
|
||||
args:
|
||||
- migrate
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "p2.fullname" . }}-config
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: P2_REDIS__PASSWORD
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
- name: P2_POSTGRESQL__PASSWORD
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
@ -56,25 +61,26 @@ spec:
|
||||
command:
|
||||
- uwsgi
|
||||
args:
|
||||
- --http 0.0.0.0:8000
|
||||
- --wsgi-file passbook/root/wsgi.py
|
||||
- --master
|
||||
- --processes 24
|
||||
- --threads 2
|
||||
- --offload-threads 4
|
||||
- --stats 0.0.0.0:8001
|
||||
- --stats-http
|
||||
- uwsgi.ini
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "p2.fullname" . }}-config
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: P2_REDIS__PASSWORD
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
- name: P2_POSTGRESQL__PASSWORD
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
@ -83,9 +89,6 @@ spec:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
@ -102,8 +105,8 @@ spec:
|
||||
value: kubernetes-healthcheck-host
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 150M
|
||||
cpu: 100m
|
||||
memory: 200M
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 300M
|
||||
cpu: 300m
|
||||
memory: 350M
|
||||
|
@ -8,7 +8,7 @@ metadata:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
@ -29,31 +29,37 @@ spec:
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- ./manage.py
|
||||
- celery
|
||||
args:
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "p2.fullname" . }}-config
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: P2_REDIS__PASSWORD
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
- name: P2_POSTGRESQL__PASSWORD
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
|
@ -1,11 +1,8 @@
|
||||
# Default values for passbook.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
tag: 0.4.0-beta
|
||||
tag: 0.6.6-beta
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -19,11 +16,13 @@ config:
|
||||
|
||||
postgresql:
|
||||
postgresqlDatabase: passbook
|
||||
postgresqlPassword: foo
|
||||
|
||||
rabbitmq:
|
||||
rabbitmq:
|
||||
password: foo
|
||||
redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@ -37,28 +36,7 @@ ingress:
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
app_gw_hosts:
|
||||
- '*.passbook.k8s.local'
|
||||
defaultHost: passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = '0.4.0-beta'
|
||||
__version__ = '0.6.6-beta'
|
||||
|
@ -11,7 +11,7 @@ class UserForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ['username', 'name', 'email', 'is_staff', 'is_active']
|
||||
fields = ['username', 'name', 'email', 'is_staff', 'is_active', 'attributes']
|
||||
widgets = {
|
||||
'name': forms.TextInput
|
||||
}
|
||||
|
@ -179,8 +179,8 @@
|
||||
<span class="card-pf-aggregate-status-notification">
|
||||
<a href="#">
|
||||
{% if worker_count < 1%}
|
||||
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
|
||||
title="{% trans 'No workers connected. Policies will not work and you may expect other issues.' %}"></span> {{ worker_count }}
|
||||
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
|
||||
title="{% trans 'No workers connected.' %}"></span> {{ worker_count }}
|
||||
{% else %}
|
||||
<span class="pficon pficon-ok"></span>{{ worker_count }}
|
||||
{% endif %}
|
||||
|
@ -7,6 +7,10 @@
|
||||
<div class="container">
|
||||
<h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
|
||||
<hr>
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -8,7 +8,7 @@ from structlog import get_logger
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
|
||||
register = template.Library()
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER = get_logger()
|
||||
|
||||
@register.simple_tag()
|
||||
def get_links(model_instance):
|
||||
|
@ -61,6 +61,7 @@ urlpatterns = [
|
||||
# Users
|
||||
path('users/', users.UserListView.as_view(),
|
||||
name='users'),
|
||||
path('users/create/', users.UserCreateView.as_view(), name='user-create'),
|
||||
path('users/<int:pk>/update/',
|
||||
users.UserUpdateView.as_view(), name='user-update'),
|
||||
path('users/<int:pk>/delete/',
|
||||
|
@ -3,8 +3,8 @@ from django.core.cache import cache
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core import __version__
|
||||
from passbook.core.models import (Application, Factor, Invitation, Policy,
|
||||
Provider, Source, User)
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
@ -12,7 +12,7 @@ from passbook.admin.forms.policies import PolicyTestForm
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core.models import Policy
|
||||
from passbook.lib.utils.reflection import path_to_class
|
||||
from passbook.policy.engine import PolicyEngine
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
|
||||
|
||||
class PolicyListView(AdminRequiredMixin, ListView):
|
||||
|
@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import View
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from passbook.admin.forms.users import UserForm
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
@ -19,6 +19,17 @@ class UserListView(AdminRequiredMixin, ListView):
|
||||
template_name = 'administration/user/list.html'
|
||||
|
||||
|
||||
class UserCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create user"""
|
||||
|
||||
model = User
|
||||
form_class = UserForm
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:users')
|
||||
success_message = _('Successfully created User')
|
||||
|
||||
|
||||
class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||
"""Update user"""
|
||||
|
||||
|
BIN
passbook/app_gw/.DS_Store
vendored
BIN
passbook/app_gw/.DS_Store
vendored
Binary file not shown.
@ -1,16 +0,0 @@
|
||||
"""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')
|
@ -1,13 +0,0 @@
|
||||
"""
|
||||
ASGI entrypoint. Configures Django and then runs the application
|
||||
defined in the ASGI_APPLICATION setting.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import django
|
||||
from channels.routing import get_default_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
django.setup()
|
||||
application = get_default_application()
|
@ -1,29 +0,0 @@
|
||||
"""passbook app_gw webserver management command"""
|
||||
|
||||
from daphne.cli import CommandLineInterface
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import autoreload
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Run Daphne Webserver for app_gw"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""passbook daphne server"""
|
||||
autoreload.run_with_reloader(self.daphne_server)
|
||||
|
||||
def daphne_server(self):
|
||||
"""Run daphne server within autoreload"""
|
||||
autoreload.raise_last_exception()
|
||||
CommandLineInterface().run([
|
||||
'-p', str(CONFIG.y('app_gw.port', 8000)),
|
||||
'-b', CONFIG.y('app_gw.listen', '0.0.0.0'), # nosec
|
||||
'--access-log', '/dev/null',
|
||||
'--application-close-timeout', '500',
|
||||
'passbook.app_gw.asgi:application'
|
||||
])
|
@ -1,33 +0,0 @@
|
||||
"""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.y('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
BIN
passbook/app_gw/migrations/.DS_Store
vendored
Binary file not shown.
@ -1,18 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,8 +0,0 @@
|
||||
"""Exception classes"""
|
||||
|
||||
class ReverseProxyException(Exception):
|
||||
"""Base for revproxy exception"""
|
||||
|
||||
|
||||
class InvalidUpstream(ReverseProxyException):
|
||||
"""Invalid upstream set"""
|
@ -1,233 +0,0 @@
|
||||
"""passbook app_gw request handler"""
|
||||
import mimetypes
|
||||
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 structlog import get_logger
|
||||
|
||||
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.policy.engine import PolicyEngine
|
||||
|
||||
SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream'
|
||||
IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored'
|
||||
LOGGER = get_logger(__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)
|
||||
|
||||
# If response has a 'Location' header, we rewrite that location as well
|
||||
if 'Location' in response:
|
||||
LOGGER.debug("Rewriting Location header")
|
||||
for server_name in self.app_gw.server_name:
|
||||
response['Location'] = response['Location'].replace(
|
||||
self._parsed_url.hostname, server_name)
|
||||
LOGGER.debug(response['Location'])
|
||||
|
||||
# LOGGER.debug("RESPONSE RETURNED: %s", response)
|
||||
return response
|
@ -1,62 +0,0 @@
|
||||
"""response functions from django-revproxy"""
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
from structlog import get_logger
|
||||
|
||||
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 = get_logger(__name__)
|
||||
|
||||
|
||||
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
|
@ -1,42 +0,0 @@
|
||||
"""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
|
@ -1,226 +0,0 @@
|
||||
"""Utils from django-revproxy, slightly adjusted"""
|
||||
import re
|
||||
from wsgiref.util import is_hop_by_hop
|
||||
|
||||
from structlog import get_logger
|
||||
|
||||
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 = get_logger()
|
||||
|
||||
|
||||
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('"', '')
|
||||
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
|
@ -1,5 +0,0 @@
|
||||
"""Application Security Gateway settings"""
|
||||
INSTALLED_APPS = [
|
||||
'channels'
|
||||
]
|
||||
ASGI_APPLICATION = "passbook.app_gw.websocket.routing.application"
|
@ -1,19 +0,0 @@
|
||||
"""passbook app_gw cache clean signals"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.app_gw.models import ApplicationGatewayProvider
|
||||
from passbook.app_gw.proxy.handler import IGNORED_HOSTNAMES_KEY
|
||||
|
||||
LOGGER = get_logger(__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)
|
@ -1,2 +0,0 @@
|
||||
"""passbook app_gw urls"""
|
||||
urlpatterns = []
|
@ -1,83 +0,0 @@
|
||||
"""websocket proxy consumer"""
|
||||
import threading
|
||||
from ssl import CERT_NONE
|
||||
|
||||
import websocket
|
||||
from channels.generic.websocket import WebsocketConsumer
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.app_gw.models import ApplicationGatewayProvider
|
||||
|
||||
LOGGER = get_logger(__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)
|
@ -1,17 +0,0 @@
|
||||
"""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)
|
||||
),
|
||||
})
|
@ -1,7 +1,8 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-16 09:13
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@ -23,7 +24,7 @@ class Migration(migrations.Migration):
|
||||
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('app', models.TextField()),
|
||||
('_context', models.TextField()),
|
||||
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
|
||||
('request_ip', models.GenericIPAddressField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
@ -33,19 +34,4 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Audit Entries',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LoginAttempt',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('target_uid', models.CharField(max_length=254)),
|
||||
('request_ip', models.GenericIPAddressField()),
|
||||
('attempts', models.IntegerField(default=1)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='loginattempt',
|
||||
unique_together={('target_uid', 'request_ip', 'created')},
|
||||
),
|
||||
]
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 12:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_audit', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='loginattempt',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 12:40
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_audit', '0002_auto_20190221_1201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='auditentry',
|
||||
name='_context',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='auditentry',
|
||||
name='context',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
@ -1,16 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-08 14:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_audit', '0003_auto_20190221_1240'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='LoginAttempt',
|
||||
),
|
||||
]
|
@ -10,7 +10,7 @@ from structlog import get_logger
|
||||
|
||||
from passbook.lib.models import UUIDModel
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER = get_logger()
|
||||
|
||||
class AuditEntry(UUIDModel):
|
||||
"""An individual audit log entry"""
|
||||
@ -60,7 +60,8 @@ class AuditEntry(UUIDModel):
|
||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||
request_ip=client_ip or '255.255.255.255',
|
||||
context=kwargs)
|
||||
LOGGER.debug("Logged %s from %s (%s)", action, user, client_ip)
|
||||
LOGGER.debug("Created Audit entry", action=action,
|
||||
user=user, from_ip=client_ip, context=kwargs)
|
||||
return entry
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -1,10 +0,0 @@
|
||||
"""passbook captcha app"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookCaptchaFactorConfig(AppConfig):
|
||||
"""passbook captcha app"""
|
||||
|
||||
name = 'passbook.captcha_factor'
|
||||
label = 'passbook_captcha_factor'
|
||||
verbose_name = 'passbook Captcha'
|
@ -1,2 +0,0 @@
|
||||
"""passbook core"""
|
||||
__version__ = '0.2.6-beta'
|
||||
|
@ -1,12 +1,6 @@
|
||||
"""passbook core app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
class PassbookCoreConfig(AppConfig):
|
||||
"""passbook core app config"""
|
||||
@ -15,13 +9,3 @@ class PassbookCoreConfig(AppConfig):
|
||||
label = 'passbook_core'
|
||||
verbose_name = 'passbook Core'
|
||||
mountpoint = ''
|
||||
|
||||
def ready(self):
|
||||
import_module('passbook.policy.engine')
|
||||
factors_to_load = CONFIG.y('passbook.factors', [])
|
||||
for factors_to_load in factors_to_load:
|
||||
try:
|
||||
import_module(factors_to_load)
|
||||
LOGGER.info("Loaded %s", factors_to_load)
|
||||
except ImportError as exc:
|
||||
LOGGER.debug(exc)
|
||||
|
@ -16,7 +16,7 @@ class ApplicationForm(forms.ModelForm):
|
||||
|
||||
model = Application
|
||||
fields = ['name', 'slug', 'launch_url', 'icon_url',
|
||||
'policies', 'provider', 'skip_authorization']
|
||||
'provider', 'policies', 'skip_authorization']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'launch_url': forms.TextInput(),
|
||||
|
@ -9,7 +9,7 @@ from passbook.core.models import User
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.lib.utils.ui import human_list
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER = get_logger()
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
"""Allow users to login"""
|
||||
@ -81,13 +81,3 @@ class SignUpForm(forms.Form):
|
||||
if password != password_repeat:
|
||||
raise ValidationError(_("Passwords don't match"))
|
||||
return self.cleaned_data.get('password_repeat')
|
||||
|
||||
|
||||
class PasswordFactorForm(forms.Form):
|
||||
"""Password authentication form"""
|
||||
|
||||
password = forms.CharField(widget=forms.PasswordInput(attrs={
|
||||
'placeholder': _('Password'),
|
||||
'autofocus': 'autofocus',
|
||||
'autocomplete': 'current-password'
|
||||
}))
|
||||
|
@ -26,7 +26,7 @@ class GroupForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ['name', 'parent', 'members', 'tags']
|
||||
fields = ['name', 'parent', 'members', 'attributes']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
}
|
||||
|
@ -3,40 +3,8 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
|
||||
GroupMembershipPolicy, PasswordPolicy,
|
||||
SSOLoginPolicy, WebhookPolicy)
|
||||
|
||||
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
|
||||
|
||||
class FieldMatcherPolicyForm(forms.ModelForm):
|
||||
"""FieldMatcherPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = FieldMatcherPolicy
|
||||
fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ]
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'value': forms.TextInput(),
|
||||
}
|
||||
|
||||
|
||||
class WebhookPolicyForm(forms.ModelForm):
|
||||
"""WebhookPolicyForm Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = WebhookPolicy
|
||||
fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers',
|
||||
'result_jsonpath', 'result_json_value', ]
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'json_body': forms.TextInput(),
|
||||
'json_headers': forms.TextInput(),
|
||||
'result_jsonpath': forms.TextInput(),
|
||||
'result_json_value': forms.TextInput(),
|
||||
}
|
||||
from passbook.core.models import DebugPolicy
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
|
||||
|
||||
class DebugPolicyForm(forms.ModelForm):
|
||||
@ -52,49 +20,3 @@ class DebugPolicyForm(forms.ModelForm):
|
||||
labels = {
|
||||
'result': _('Allow user')
|
||||
}
|
||||
|
||||
|
||||
class GroupMembershipPolicyForm(forms.ModelForm):
|
||||
"""GroupMembershipPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = GroupMembershipPolicy
|
||||
fields = GENERAL_FIELDS + ['group', ]
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'order': forms.NumberInput(),
|
||||
}
|
||||
|
||||
class SSOLoginPolicyForm(forms.ModelForm):
|
||||
"""Edit SSOLoginPolicy instances"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SSOLoginPolicy
|
||||
fields = GENERAL_FIELDS
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'order': forms.NumberInput(),
|
||||
}
|
||||
|
||||
class PasswordPolicyForm(forms.ModelForm):
|
||||
"""PasswordPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = PasswordPolicy
|
||||
fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase',
|
||||
'amount_symbols', 'length_min', 'symbol_charset',
|
||||
'error_message']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'symbol_charset': forms.TextInput(),
|
||||
'error_message': forms.TextInput(),
|
||||
}
|
||||
labels = {
|
||||
'amount_uppercase': _('Minimum amount of Uppercase Characters'),
|
||||
'amount_lowercase': _('Minimum amount of Lowercase Characters'),
|
||||
'amount_symbols': _('Minimum amount of Symbols Characters'),
|
||||
'length_min': _('Minimum Length'),
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
"""passbook import_users management command"""
|
||||
from csv import DictReader
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.validators import EmailValidator, ValidationError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Import users from CSV file"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional arguments
|
||||
parser.add_argument('file', nargs='+', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Create Users from CSV file"""
|
||||
for file in options.get('file'):
|
||||
with open(file, 'r') as _file:
|
||||
reader = DictReader(_file)
|
||||
for user in reader:
|
||||
LOGGER.debug('User %s', user.get('username'))
|
||||
try:
|
||||
# only import users with valid email addresses
|
||||
if user.get('email'):
|
||||
validator = EmailValidator()
|
||||
validator(user.get('email'))
|
||||
# use combination of username and email to check for existing user
|
||||
if User.objects.filter(
|
||||
username=user.get('username'),
|
||||
email=user.get('email')).exists():
|
||||
LOGGER.debug('User %s exists already, skipping', user.get('username'))
|
||||
# Create user
|
||||
User.objects.create(
|
||||
username=user.get('username'),
|
||||
email=user.get('email'),
|
||||
name=user.get('name'),
|
||||
password=user.get('password'))
|
||||
LOGGER.debug('Created User %s', user.get('username'))
|
||||
except ValidationError as exc:
|
||||
LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
|
||||
continue
|
@ -1,35 +0,0 @@
|
||||
"""passbook Webserver management command"""
|
||||
|
||||
import cherrypy
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.root.wsgi import application
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Run CherryPy webserver"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""passbook cherrypy server"""
|
||||
cherrypy.config.update(CONFIG.y('web'))
|
||||
cherrypy.tree.graft(application, '/')
|
||||
# Mount NullObject to serve static files
|
||||
cherrypy.tree.mount(None, settings.STATIC_URL, config={
|
||||
'/': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': settings.STATIC_ROOT,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 86400,
|
||||
'tools.gzip.on': True,
|
||||
}
|
||||
})
|
||||
cherrypy.engine.start()
|
||||
for file in CONFIG.loaded_file:
|
||||
cherrypy.engine.autoreload.files.add(file)
|
||||
LOGGER.info("Added '%s' to autoreload triggers", file)
|
||||
cherrypy.engine.block()
|
@ -1,22 +0,0 @@
|
||||
"""passbook Worker management command"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import autoreload
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Run Celery Worker"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""celery worker"""
|
||||
autoreload.run_with_reloader(self.celery_worker)
|
||||
|
||||
def celery_worker(self):
|
||||
"""Run celery worker within autoreload"""
|
||||
autoreload.raise_last_exception()
|
||||
CELERY_APP.worker_main(['worker', '--autoscale=10,3', '-E', '-B'])
|
@ -1,21 +1,24 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-16 09:10
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:06
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0009_alter_user_last_name_max_length'),
|
||||
('auth', '0011_update_proxy_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@ -34,6 +37,8 @@ class Migration(migrations.Migration):
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
('name', models.TextField()),
|
||||
('password_change_date', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
@ -44,39 +49,17 @@ class Migration(migrations.Migration):
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=80, verbose_name='name')),
|
||||
('extra_data', models.TextField(blank=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('expires', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('fixed_username', models.TextField(blank=True, default=None)),
|
||||
('fixed_email', models.TextField(blank=True, default=None)),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Invitation',
|
||||
'verbose_name_plural': 'Invitations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Policy',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.TextField(blank=True, null=True)),
|
||||
('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)),
|
||||
('negate', models.BooleanField(default=False)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('timeout', models.IntegerField(default=30)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
@ -85,28 +68,136 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='PolicyModel',
|
||||
fields=[
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PropertyMapping',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Property Mapping',
|
||||
'verbose_name_plural': 'Property Mappings',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DebugPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('result', models.BooleanField(default=False)),
|
||||
('wait_min', models.IntegerField(default=5)),
|
||||
('wait_max', models.IntegerField(default=30)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Debug Policy',
|
||||
'verbose_name_plural': 'Debug Policies',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Factor',
|
||||
fields=[
|
||||
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
|
||||
('name', models.TextField()),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('order', models.IntegerField()),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.policymodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
|
||||
('name', models.TextField()),
|
||||
('slug', models.SlugField()),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.policymodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('property_mappings', models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Nonce',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
|
||||
('expiring', models.BooleanField(default=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Nonce',
|
||||
'verbose_name_plural': 'Nonces',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('expires', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('fixed_username', models.TextField(blank=True, default=None)),
|
||||
('fixed_email', models.TextField(blank=True, default=None)),
|
||||
('needs_confirmation', models.BooleanField(default=True)),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Invitation',
|
||||
'verbose_name_plural': 'Invitations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=80, verbose_name='name')),
|
||||
('tags', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('name', 'parent')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(to='passbook_core.Group'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserSourceConnection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'source')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
@ -124,131 +215,9 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=('passbook_core.policymodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DebugPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('result', models.BooleanField(default=False)),
|
||||
('wait_min', models.IntegerField(default=5)),
|
||||
('wait_max', models.IntegerField(default=30)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Debug Policy',
|
||||
'verbose_name_plural': 'Debug Policys',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Factor',
|
||||
fields=[
|
||||
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
|
||||
('name', models.TextField()),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('order', models.IntegerField()),
|
||||
('type', models.TextField(unique=True)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.policymodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FieldMatcherPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('user_field', models.TextField(choices=[('username', 'Username'), ('first_name', 'First Name'), ('last_name', 'Last Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')])),
|
||||
('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('endswith', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)),
|
||||
('value', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Field matcher Policy',
|
||||
'verbose_name_plural': 'Field matcher Policys',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PasswordPolicyPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('amount_uppercase', models.IntegerField(default=0)),
|
||||
('amount_lowercase', models.IntegerField(default=0)),
|
||||
('amount_symbols', models.IntegerField(default=0)),
|
||||
('length_min', models.IntegerField(default=0)),
|
||||
('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Password Policy Policy',
|
||||
'verbose_name_plural': 'Password Policy Policys',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
|
||||
('name', models.TextField()),
|
||||
('slug', models.SlugField()),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.policymodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WebhookPolicy',
|
||||
fields=[
|
||||
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
|
||||
('url', models.URLField()),
|
||||
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)),
|
||||
('json_body', models.TextField()),
|
||||
('json_headers', models.TextField()),
|
||||
('result_jsonpath', models.TextField()),
|
||||
('result_json_value', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Webhook Policy',
|
||||
'verbose_name_plural': 'Webhook Policys',
|
||||
},
|
||||
bases=('passbook_core.policy',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='policymodel',
|
||||
name='policies',
|
||||
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(to='passbook_core.Group'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usersourceconnection',
|
||||
name='source',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='group',
|
||||
unique_together={('name', 'parent')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='applications',
|
||||
field=models.ManyToManyField(to='passbook_core.Application'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='sources',
|
||||
field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='usersourceconnection',
|
||||
unique_together={('user', 'source')},
|
||||
),
|
||||
]
|
||||
|
@ -1,29 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-16 10:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='debugpolicy',
|
||||
options={'verbose_name': 'Debug Policy', 'verbose_name_plural': 'Debug Policies'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='fieldmatcherpolicy',
|
||||
options={'verbose_name': 'Field matcher Policy', 'verbose_name_plural': 'Field matcher Policies'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='passwordpolicypolicy',
|
||||
options={'verbose_name': 'Password Policy Policy', 'verbose_name_plural': 'Password Policy Policies'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='webhookpolicy',
|
||||
options={'verbose_name': 'Webhook Policy', 'verbose_name_plural': 'Webhook Policies'},
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 2.2 on 2019-04-18 09:09
|
||||
# Generated by Django 2.2.6 on 2019-10-10 11:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_saml_idp', '0002_samlpropertymapping'),
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='samlprovider',
|
||||
name='audience',
|
||||
model_name='nonce',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
]
|
@ -1,17 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-16 10:04
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0002_auto_20190216_1002'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='PasswordPolicyPolicy',
|
||||
new_name='PasswordPolicy',
|
||||
),
|
||||
]
|
29
passbook/core/migrations/0003_auto_20191011_0914.py
Normal file
29
passbook/core/migrations/0003_auto_20191011_0914.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-11 09:14
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0002_nonce_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='group',
|
||||
old_name='tags',
|
||||
new_name='attributes',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='source',
|
||||
name='property_mappings',
|
||||
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='attributes',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
@ -1,17 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-16 10:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0003_auto_20190216_1004'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='passwordpolicy',
|
||||
options={'verbose_name': 'Password Policy', 'verbose_name_plural': 'Password Policies'},
|
||||
),
|
||||
]
|
@ -1,28 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 12:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0004_auto_20190216_1013'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='policy',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='policymodel',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usersourceconnection',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 12:32
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0005_auto_20190221_1201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='factor',
|
||||
name='arguments',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict),
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 12:33
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0006_factor_arguments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='factor',
|
||||
name='arguments',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 15:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0007_auto_20190221_1233'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='fieldmatcherpolicy',
|
||||
name='match_action',
|
||||
field=models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('contains', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50),
|
||||
),
|
||||
]
|
@ -1,44 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-24 09:50
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0008_auto_20190221_1516'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DummyFactor',
|
||||
fields=[
|
||||
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.factor',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PasswordFactor',
|
||||
fields=[
|
||||
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
|
||||
('backends', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.factor',),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='factor',
|
||||
name='arguments',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='factor',
|
||||
name='type',
|
||||
),
|
||||
]
|
@ -1,21 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-24 10:16
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0009_auto_20190224_0950'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='dummyfactor',
|
||||
options={'verbose_name': 'Dummy Factor', 'verbose_name_plural': 'Dummy Factors'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='passwordfactor',
|
||||
options={'verbose_name': 'Password Factor', 'verbose_name_plural': 'Password Factors'},
|
||||
),
|
||||
]
|
@ -1,25 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-25 14:38
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0010_auto_20190224_1016'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='passwordfactor',
|
||||
name='password_policies',
|
||||
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='password_change_date',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -1,31 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-25 19:12
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0011_auto_20190225_1438'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Nonce',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Nonce',
|
||||
'verbose_name_plural': 'Nonces',
|
||||
},
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-25 19:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0012_nonce'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invitation',
|
||||
name='needs_confirmation',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-26 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0014_auto_20190226_0850'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='passwordpolicy',
|
||||
name='error_message',
|
||||
field=models.TextField(default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -1,38 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-27 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_names(apps, schema_editor):
|
||||
"""migrate first_name and last_name to name"""
|
||||
User = apps.get_model("passbook_core", "User")
|
||||
for user in User.objects.all():
|
||||
user.name = '%s %s' % (user.first_name, user.last_name)
|
||||
user.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0015_passwordpolicy_error_message'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=models.TextField(default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(migrate_names),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=models.TextField(),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fieldmatcherpolicy',
|
||||
name='user_field',
|
||||
field=models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
|
||||
),
|
||||
]
|
@ -1,26 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-08 10:40
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0016_auto_20190227_1355'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PropertyMapping',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Property Mapping',
|
||||
'verbose_name_plural': 'Property Mappings',
|
||||
},
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-08 10:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0017_propertymapping'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='provider',
|
||||
name='property_mappings',
|
||||
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
|
||||
),
|
||||
]
|
@ -1,25 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-10 16:15
|
||||
|
||||
import django.contrib.postgres.fields.hstore
|
||||
from django.contrib.postgres.operations import HStoreExtension
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0018_provider_property_mappings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='group',
|
||||
name='extra_data',
|
||||
),
|
||||
HStoreExtension(),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='tags',
|
||||
field=django.contrib.postgres.fields.hstore.HStoreField(default=dict),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-21 12:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0020_groupmembershippolicy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='policy',
|
||||
name='timeout',
|
||||
field=models.IntegerField(default=30),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.1.7 on 2019-04-04 19:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0021_policy_timeout'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='nonce',
|
||||
name='expiring',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
@ -1,17 +0,0 @@
|
||||
# Generated by Django 2.2 on 2019-04-13 15:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0022_nonce_expiring'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='applications',
|
||||
),
|
||||
]
|
@ -1,13 +1,12 @@
|
||||
"""passbook core models"""
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.fields import ArrayField, HStoreField
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
@ -17,38 +16,26 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.signals import password_changed
|
||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||
from passbook.policies.exceptions import PolicyException
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def default_nonce_duration():
|
||||
"""Default duration a Nonce is valid"""
|
||||
return now() + timedelta(hours=4)
|
||||
|
||||
|
||||
class PolicyResult:
|
||||
"""Small data-class to hold policy results"""
|
||||
|
||||
passing: bool = False
|
||||
messages: List[str] = []
|
||||
|
||||
def __init__(self, passing: bool, *messages: str):
|
||||
self.passing = passing
|
||||
self.messages = messages
|
||||
|
||||
def __str__(self):
|
||||
return f"<PolicyResult passing={self.passing}>"
|
||||
|
||||
class Group(UUIDModel):
|
||||
"""Custom Group model which supports a basic hierarchy"""
|
||||
|
||||
name = models.CharField(_('name'), max_length=80)
|
||||
parent = models.ForeignKey('Group', blank=True, null=True,
|
||||
on_delete=models.SET_NULL, related_name='children')
|
||||
tags = HStoreField(default=dict)
|
||||
attributes = JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "Group %s" % self.name
|
||||
return f"Group {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -64,12 +51,15 @@ class User(AbstractUser):
|
||||
groups = models.ManyToManyField('Group')
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
attributes = JSONField(default=dict, blank=True)
|
||||
|
||||
def set_password(self, password):
|
||||
if self.pk:
|
||||
password_changed.send(sender=self, user=self, password=password)
|
||||
self.password_change_date = now()
|
||||
return super().set_password(password)
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||
|
||||
@ -83,11 +73,26 @@ class Provider(models.Model):
|
||||
return getattr(self, 'name')
|
||||
return super().__str__()
|
||||
|
||||
|
||||
class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
||||
"""Base model which can have policies applied to it"""
|
||||
|
||||
policies = models.ManyToManyField('Policy', blank=True)
|
||||
|
||||
|
||||
class UserSettings:
|
||||
"""Dataclass for Factor and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
|
||||
def __init__(self, name: str, icon: str, view_name: str):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.view_name = view_name
|
||||
|
||||
|
||||
class Factor(PolicyModel):
|
||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||
|
||||
@ -100,55 +105,14 @@ class Factor(PolicyModel):
|
||||
type = ''
|
||||
form = ''
|
||||
|
||||
def has_user_settings(self):
|
||||
"""Entrypoint to integrate with User settings. Can either return False if no
|
||||
user settings are available, or a tuple or string, string, string where the first string
|
||||
is the name the item has, the second string is the icon and the third is the view-name."""
|
||||
return False
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return "Factor %s" % self.slug
|
||||
return f"Factor {self.slug}"
|
||||
|
||||
class PasswordFactor(Factor):
|
||||
"""Password-based Django-backend Authentication Factor"""
|
||||
|
||||
backends = ArrayField(models.TextField())
|
||||
password_policies = models.ManyToManyField('Policy', blank=True)
|
||||
|
||||
type = 'passbook.core.auth.factors.password.PasswordFactor'
|
||||
form = 'passbook.core.forms.factors.PasswordFactorForm'
|
||||
|
||||
def has_user_settings(self):
|
||||
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
|
||||
|
||||
def password_passes(self, user: User) -> bool:
|
||||
"""Return true if user's password passes, otherwise False or raise Exception"""
|
||||
for policy in self.policies.all():
|
||||
if not policy.passes(user):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return "Password Factor %s" % self.slug
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Password Factor')
|
||||
verbose_name_plural = _('Password Factors')
|
||||
|
||||
class DummyFactor(Factor):
|
||||
"""Dummy factor, mostly used to debug"""
|
||||
|
||||
type = 'passbook.core.auth.factors.dummy.DummyFactor'
|
||||
form = 'passbook.core.forms.factors.DummyFactorForm'
|
||||
|
||||
def __str__(self):
|
||||
return "Dummy Factor %s" % self.slug
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Dummy Factor')
|
||||
verbose_name_plural = _('Dummy Factors')
|
||||
|
||||
class Application(PolicyModel):
|
||||
"""Every Application which uses passbook for authentication/identification/authorization
|
||||
@ -174,12 +138,14 @@ class Application(PolicyModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Source(PolicyModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
enabled = models.BooleanField(default=True)
|
||||
property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True)
|
||||
|
||||
form = '' # ModelForm-based class ued to create/edit instance
|
||||
|
||||
@ -200,15 +166,15 @@ class Source(PolicyModel):
|
||||
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
||||
return None
|
||||
|
||||
def has_user_settings(self):
|
||||
"""Entrypoint to integrate with User settings. Can either return False if no
|
||||
user settings are available, or a tuple or string, string, string where the first string
|
||||
is the name the item has, the second string is the icon and the third is the view-name."""
|
||||
return False
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserSourceConnection(CreatedUpdatedModel):
|
||||
"""Connection between User and Source."""
|
||||
|
||||
@ -219,6 +185,7 @@ class UserSourceConnection(CreatedUpdatedModel):
|
||||
|
||||
unique_together = (('user', 'source'),)
|
||||
|
||||
|
||||
class Policy(UUIDModel, CreatedUpdatedModel):
|
||||
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
|
||||
other types to add other fields, more logic, etc."""
|
||||
@ -241,151 +208,12 @@ class Policy(UUIDModel, CreatedUpdatedModel):
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return "%s action %s" % (self.name, self.action)
|
||||
return f"{self.name} action {self.action}"
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check if user instance passes this policy"""
|
||||
raise NotImplementedError()
|
||||
raise PolicyException()
|
||||
|
||||
class FieldMatcherPolicy(Policy):
|
||||
"""Policy which checks if a field of the User model matches/doesn't match a
|
||||
certain pattern"""
|
||||
|
||||
MATCH_STARTSWITH = 'startswith'
|
||||
MATCH_ENDSWITH = 'endswith'
|
||||
MATCH_CONTAINS = 'contains'
|
||||
MATCH_REGEXP = 'regexp'
|
||||
MATCH_EXACT = 'exact'
|
||||
|
||||
MATCHES = (
|
||||
(MATCH_STARTSWITH, _('Starts with')),
|
||||
(MATCH_ENDSWITH, _('Ends with')),
|
||||
(MATCH_CONTAINS, _('Contains')),
|
||||
(MATCH_REGEXP, _('Regexp')),
|
||||
(MATCH_EXACT, _('Exact')),
|
||||
)
|
||||
|
||||
USER_FIELDS = (
|
||||
('username', _('Username'),),
|
||||
('name', _('Name'),),
|
||||
('email', _('E-Mail'),),
|
||||
('is_staff', _('Is staff'),),
|
||||
('is_active', _('Is active'),),
|
||||
('data_joined', _('Date joined'),),
|
||||
)
|
||||
|
||||
user_field = models.TextField(choices=USER_FIELDS)
|
||||
match_action = models.CharField(max_length=50, choices=MATCHES)
|
||||
value = models.TextField()
|
||||
|
||||
form = 'passbook.core.forms.policies.FieldMatcherPolicyForm'
|
||||
|
||||
def __str__(self):
|
||||
description = "%s, user.%s %s '%s'" % (self.name, self.user_field,
|
||||
self.match_action, self.value)
|
||||
if self.name:
|
||||
description = "%s: %s" % (self.name, description)
|
||||
return description
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
"""Check if user instance passes this role"""
|
||||
if not hasattr(user, self.user_field):
|
||||
raise ValueError("Field does not exist")
|
||||
user_field_value = getattr(user, self.user_field, None)
|
||||
LOGGER.debug("Checked '%s' %s with '%s'...",
|
||||
user_field_value, self.match_action, self.value)
|
||||
passes = False
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
|
||||
passes = user_field_value.startswith(self.value)
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
|
||||
passes = user_field_value.endswith(self.value)
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
|
||||
passes = self.value in user_field_value
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
|
||||
pattern = re.compile(self.value)
|
||||
passes = bool(pattern.match(user_field_value))
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
|
||||
passes = user_field_value == self.value
|
||||
|
||||
LOGGER.debug("User got '%r'", passes)
|
||||
return PolicyResult(passes)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Field matcher Policy')
|
||||
verbose_name_plural = _('Field matcher Policies')
|
||||
|
||||
class PasswordPolicy(Policy):
|
||||
"""Policy to make sure passwords have certain properties"""
|
||||
|
||||
amount_uppercase = models.IntegerField(default=0)
|
||||
amount_lowercase = models.IntegerField(default=0)
|
||||
amount_symbols = models.IntegerField(default=0)
|
||||
length_min = models.IntegerField(default=0)
|
||||
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
|
||||
error_message = models.TextField()
|
||||
|
||||
form = 'passbook.core.forms.policies.PasswordPolicyForm'
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
# Only check if password is being set
|
||||
if not hasattr(user, '__password__'):
|
||||
return PolicyResult(True)
|
||||
password = getattr(user, '__password__')
|
||||
|
||||
filter_regex = r''
|
||||
if self.amount_lowercase > 0:
|
||||
filter_regex += r'[a-z]{%d,}' % self.amount_lowercase
|
||||
if self.amount_uppercase > 0:
|
||||
filter_regex += r'[A-Z]{%d,}' % self.amount_uppercase
|
||||
if self.amount_symbols > 0:
|
||||
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
|
||||
result = bool(re.compile(filter_regex).match(password))
|
||||
LOGGER.debug("User got %r", result)
|
||||
if not result:
|
||||
return PolicyResult(result, self.error_message)
|
||||
return PolicyResult(result)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Password Policy')
|
||||
verbose_name_plural = _('Password Policies')
|
||||
|
||||
|
||||
class WebhookPolicy(Policy):
|
||||
"""Policy that asks webhook"""
|
||||
|
||||
METHOD_GET = 'GET'
|
||||
METHOD_POST = 'POST'
|
||||
METHOD_PATCH = 'PATCH'
|
||||
METHOD_DELETE = 'DELETE'
|
||||
METHOD_PUT = 'PUT'
|
||||
|
||||
METHODS = (
|
||||
(METHOD_GET, METHOD_GET),
|
||||
(METHOD_POST, METHOD_POST),
|
||||
(METHOD_PATCH, METHOD_PATCH),
|
||||
(METHOD_DELETE, METHOD_DELETE),
|
||||
(METHOD_PUT, METHOD_PUT),
|
||||
)
|
||||
|
||||
url = models.URLField()
|
||||
method = models.CharField(max_length=10, choices=METHODS)
|
||||
json_body = models.TextField()
|
||||
json_headers = models.TextField()
|
||||
result_jsonpath = models.TextField()
|
||||
result_json_value = models.TextField()
|
||||
|
||||
form = 'passbook.core.forms.policies.WebhookPolicyForm'
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
"""Call webhook asynchronously and report back"""
|
||||
raise NotImplementedError()
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Webhook Policy')
|
||||
verbose_name_plural = _('Webhook Policies')
|
||||
|
||||
class DebugPolicy(Policy):
|
||||
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
|
||||
@ -397,10 +225,10 @@ class DebugPolicy(Policy):
|
||||
|
||||
form = 'passbook.core.forms.policies.DebugPolicyForm'
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Wait random time then return result"""
|
||||
wait = SystemRandom().randrange(self.wait_min, self.wait_max)
|
||||
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
|
||||
LOGGER.debug("Policy waiting", policy=self, delay=wait)
|
||||
sleep(wait)
|
||||
return PolicyResult(self.result, 'Debugging')
|
||||
|
||||
@ -409,35 +237,6 @@ class DebugPolicy(Policy):
|
||||
verbose_name = _('Debug Policy')
|
||||
verbose_name_plural = _('Debug Policies')
|
||||
|
||||
class GroupMembershipPolicy(Policy):
|
||||
"""Policy to check if the user is member in a certain group"""
|
||||
|
||||
group = models.ForeignKey('Group', on_delete=models.CASCADE)
|
||||
|
||||
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
|
||||
|
||||
def passes(self, user: User) -> PolicyResult:
|
||||
return PolicyResult(self.group.user_set.filter(pk=user.pk).exists())
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Group Membership Policy')
|
||||
verbose_name_plural = _('Group Membership Policies')
|
||||
|
||||
class SSOLoginPolicy(Policy):
|
||||
"""Policy that applies to users that have authenticated themselves through SSO"""
|
||||
|
||||
form = 'passbook.core.forms.policies.SSOLoginPolicyForm'
|
||||
|
||||
def passes(self, user) -> PolicyResult:
|
||||
"""Check if user instance passes this policy"""
|
||||
from passbook.core.auth.view import AuthenticationView
|
||||
return PolicyResult(user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False))
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('SSO Login Policy')
|
||||
verbose_name_plural = _('SSO Login Policies')
|
||||
|
||||
class Invitation(UUIDModel):
|
||||
"""Single-use invitation link"""
|
||||
@ -451,31 +250,39 @@ class Invitation(UUIDModel):
|
||||
@property
|
||||
def link(self):
|
||||
"""Get link to use invitation"""
|
||||
return reverse_lazy('passbook_core:auth-sign-up') + '?invitation=%s' % self.uuid
|
||||
return reverse_lazy('passbook_core:auth-sign-up') + f'?invitation={self.uuid.hex}'
|
||||
|
||||
def __str__(self):
|
||||
return "Invitation %s created by %s" % (self.uuid, self.created_by)
|
||||
return f"Invitation {self.uuid.hex} created by {self.created_by}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Invitation')
|
||||
verbose_name_plural = _('Invitations')
|
||||
|
||||
|
||||
class Nonce(UUIDModel):
|
||||
"""One-time link for password resets/sign-up-confirmations"""
|
||||
|
||||
expires = models.DateTimeField(default=default_nonce_duration)
|
||||
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
||||
expiring = models.BooleanField(default=True)
|
||||
description = models.TextField(default='', blank=True)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if nonce is expired yet."""
|
||||
return now() > self.expires
|
||||
|
||||
def __str__(self):
|
||||
return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires)
|
||||
return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Nonce')
|
||||
verbose_name_plural = _('Nonces')
|
||||
|
||||
|
||||
class PropertyMapping(UUIDModel):
|
||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||
|
||||
@ -485,7 +292,7 @@ class PropertyMapping(UUIDModel):
|
||||
objects = InheritanceManager()
|
||||
|
||||
def __str__(self):
|
||||
return "Property Mapping %s" % self.name
|
||||
return f"Property Mapping {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -5,37 +5,20 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.exceptions import PasswordPolicyInvalid
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER = get_logger()
|
||||
|
||||
user_signed_up = Signal(providing_args=['request', 'user'])
|
||||
invitation_created = Signal(providing_args=['request', 'invitation'])
|
||||
invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
|
||||
password_changed = Signal(providing_args=['user', 'password'])
|
||||
|
||||
@receiver(password_changed)
|
||||
# pylint: disable=unused-argument
|
||||
def password_policy_checker(sender, password, **kwargs):
|
||||
"""Run password through all password policies which are applied to the user"""
|
||||
from passbook.core.models import PasswordFactor
|
||||
from passbook.policy.engine import PolicyEngine
|
||||
setattr(sender, '__password__', password)
|
||||
_all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
|
||||
for factor in _all_factors:
|
||||
policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses())
|
||||
policy_engine.for_user(sender).build()
|
||||
passing, messages = policy_engine.result
|
||||
if not passing:
|
||||
raise PasswordPolicyInvalid(*messages)
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def invalidate_policy_cache(sender, instance, **kwargs):
|
||||
def invalidate_policy_cache(sender, instance, **_):
|
||||
"""Invalidate Policy cache when policy is updated"""
|
||||
from passbook.core.models import Policy
|
||||
if isinstance(instance, Policy):
|
||||
LOGGER.debug("Invalidating cache for %s", instance.pk)
|
||||
LOGGER.debug("Invalidating policy cache", policy=instance)
|
||||
keys = cache.keys("%s#*" % instance.pk)
|
||||
cache.delete_many(keys)
|
||||
LOGGER.debug("Deleted %d keys", len(keys))
|
||||
|
@ -1,3 +1,7 @@
|
||||
.navbar-brand-name {
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.dynamic-array-widget .array-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -23,7 +27,6 @@
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
2
passbook/core/static/robots.txt
Normal file
2
passbook/core/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
@ -1,28 +1,14 @@
|
||||
"""passbook core tasks"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.timezone import now
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Nonce
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
@CELERY_APP.task()
|
||||
def send_email(to_address, subject, template, context):
|
||||
"""Send Email to user(s)"""
|
||||
html_content = render_to_string(template, context=context)
|
||||
text_content = strip_tags(html_content)
|
||||
msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address])
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
LOGGER = get_logger()
|
||||
|
||||
@CELERY_APP.task()
|
||||
def clean_nonces():
|
||||
"""Remove expired nonces"""
|
||||
amount, _ = Nonce.objects.filter(expires__lt=datetime.now(), expiring=True).delete()
|
||||
LOGGER.debug("Deleted expired %d nonces", amount)
|
||||
amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete()
|
||||
LOGGER.debug("Deleted expired nonces", amount=amount)
|
||||
|
@ -46,9 +46,6 @@
|
||||
<script src="{% static 'js/passbook.js' %}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
<div class="modals">
|
||||
{% include 'partials/about_modal.html' %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -46,9 +46,6 @@
|
||||
<script src="{% static 'js/passbook.js' %}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
<div class="modals">
|
||||
{% include 'partials/about_modal.html' %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user