Merge branch 'master' into ldap-rewrite

This commit is contained in:
Langhammer, Jens 2019-10-11 10:24:12 +02:00
commit 44a3c7fa5f
37 changed files with 313 additions and 223 deletions

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.6.2-beta current_version = 0.6.4-beta
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View file

@ -27,7 +27,7 @@ create-base-image:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.6.2-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest
stage: build-base-image stage: build-base-image
only: only:
refs: refs:
@ -41,7 +41,7 @@ build-dev-image:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.6.2-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest
stage: build-dev-image stage: build-dev-image
only: only:
refs: refs:
@ -70,13 +70,13 @@ migrations:
# services: # services:
# - postgres:latest # - postgres:latest
# - redis:latest # - redis:latest
# pylint: pylint:
# script: script:
# - pylint passbook - pylint passbook
# stage: test stage: test
# services: services:
# - postgres:latest - postgres:latest
# - redis:latest - redis:latest
coverage: coverage:
script: script:
- coverage run manage.py test - coverage run manage.py test
@ -95,7 +95,7 @@ build-passbook-server:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.6.2-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.6.4-beta
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/
@ -107,7 +107,7 @@ build-passbook-static:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.6.2-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.4-beta
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/

View file

@ -3,7 +3,6 @@ FROM docker.beryju.org/passbook/base:latest
COPY ./passbook/ /app/passbook COPY ./passbook/ /app/passbook
COPY ./manage.py /app/ COPY ./manage.py /app/
COPY ./docker/uwsgi.ini /app/ COPY ./docker/uwsgi.ini /app/
RUN chown -R passbook: /app
WORKDIR /app/ WORKDIR /app/

View file

@ -8,6 +8,7 @@ celery = "*"
cherrypy = "*" cherrypy = "*"
defusedxml = "*" defusedxml = "*"
django = "*" django = "*"
kombu = "==4.5.0"
django-cors-middleware = "*" django-cors-middleware = "*"
django-filters = "*" django-filters = "*"
django-ipware = "*" django-ipware = "*"
@ -18,7 +19,6 @@ django-otp = "*"
django-recaptcha = "*" django-recaptcha = "*"
django-redis = "*" django-redis = "*"
django-rest-framework = "*" django-rest-framework = "*"
djangorestframework = "==3.9.4"
drf-yasg = "*" drf-yasg = "*"
ldap3 = "*" ldap3 = "*"
lxml = "*" lxml = "*"
@ -35,17 +35,17 @@ service_identity = "*"
signxml = "*" signxml = "*"
urllib3 = {extras = ["secure"],version = "*"} urllib3 = {extras = ["secure"],version = "*"}
structlog = "*" structlog = "*"
pyuwsgi = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"
[dev-packages] [dev-packages]
astroid = "==2.2.5"
coverage = "*" coverage = "*"
isort = "*" isort = "*"
pylint = "==2.3.1" pylint = "==2.3.1"
pylint-django = "==2.0.10" pylint-django = "*"
prospector = "==1.1.7" prospector = "*"
django-debug-toolbar = "*" django-debug-toolbar = "*"
bumpversion = "*" bumpversion = "*"
unittest-xml-reporting = "*" unittest-xml-reporting = "*"

103
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "d03d1e494d28a90b39edd1d489afdb5e39ec09bceb18daa2a54b2cc7de61d83c" "sha256": "94b3d5140f0c31dac1fc77af75a0df30ae4fb0571bf6b7fcd722487c63dc1872"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -18,17 +18,17 @@
"default": { "default": {
"amqp": { "amqp": {
"hashes": [ "hashes": [
"sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68", "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
"sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a" "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
], ],
"version": "==2.5.1" "version": "==2.5.2"
}, },
"asn1crypto": { "asn1crypto": {
"hashes": [ "hashes": [
"sha256:d02bf8ea1b964a5ff04ac7891fe3a39150045d1e5e4fe99273ba677d11b92a04", "sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292",
"sha256:f822954b90c4c44f002e2cd46d636ab630f1fe4df22c816a82b66505c404eb2a" "sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f"
], ],
"version": "==1.0.0" "version": "==1.0.1"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
@ -101,10 +101,10 @@
}, },
"cheroot": { "cheroot": {
"hashes": [ "hashes": [
"sha256:6168371ab9aaf574ac5f75675f244bbfebf990202bf75048065e9d675b9ae719", "sha256:3ff64073efa35b39d5e107410f5c79664dc8c6c5990651e970740c80ab8878a8",
"sha256:8cc7c28961db2e13d0cac6b234a589a314c1844f7bbf54e67888ac9a2e25ac59" "sha256:d523a1525258730026aa35b86c8c47c8d0e3892fb89f0f39157d4b32a50edf05"
], ],
"version": "==7.0.0" "version": "==8.1.0"
}, },
"cherrypy": { "cherrypy": {
"hashes": [ "hashes": [
@ -242,11 +242,10 @@
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", "sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8",
"sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb" "sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"
], ],
"index": "pypi", "version": "==3.10.3"
"version": "==3.9.4"
}, },
"drf-yasg": { "drf-yasg": {
"hashes": [ "hashes": [
@ -276,13 +275,6 @@
], ],
"version": "==2.8" "version": "==2.8"
}, },
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
"version": "==0.23"
},
"inflection": { "inflection": {
"hashes": [ "hashes": [
"sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca" "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
@ -304,17 +296,18 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
], ],
"version": "==2.10.1" "version": "==2.10.3"
}, },
"kombu": { "kombu": {
"hashes": [ "hashes": [
"sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb", "sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd",
"sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566" "sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181"
], ],
"version": "==4.6.5" "index": "pypi",
"version": "==4.5.0"
}, },
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
@ -466,10 +459,10 @@
}, },
"pyasn1-modules": { "pyasn1-modules": {
"hashes": [ "hashes": [
"sha256:43c17a83c155229839cc5c6b868e8d0c6041dba149789b6d6e28801c64821722", "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b",
"sha256:e30199a9d221f1b26c885ff3d87fd08694dbbe18ed0e8e405a2a7126d30ce4c0" "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3"
], ],
"version": "==0.2.6" "version": "==0.2.7"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
@ -566,10 +559,40 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" "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": { "pyyaml": {
"hashes": [ "hashes": [
@ -736,13 +759,6 @@
"sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"
], ],
"version": "==2.0" "version": "==2.0"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
} }
}, },
"develop": { "develop": {
@ -751,7 +767,6 @@
"sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4",
"sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"
], ],
"index": "pypi",
"version": "==2.2.5" "version": "==2.2.5"
}, },
"autopep8": { "autopep8": {
@ -976,10 +991,10 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
], ],
"version": "==2019.2" "version": "==2019.3"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [

View file

@ -3,7 +3,9 @@
## Quick instance ## Quick instance
``` ```
export PASSBOOK_DOMAIN=domain.tld
docker-compose pull docker-compose pull
docker-compose up -d docker-compose up -d
docker-compose exec server ./manage.py migrate
docker-compose exec server ./manage.py createsuperuser docker-compose exec server ./manage.py createsuperuser
``` ```

View file

@ -1,19 +1,19 @@
FROM python:3.7-slim-stretch FROM python:3.7-slim-buster as locker
COPY ./Pipfile /app/ COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/ COPY ./Pipfile.lock /app/
WORKDIR /app/ WORKDIR /app/
RUN apt-get update && \ RUN pip install pipenv && \
apt-get install -y --no-install-recommends build-essential && \ pipenv lock -r > requirements.txt && \
pip install pipenv uwsgi --no-cache-dir && \ pipenv lock -rd > requirements-dev.txt
apt-get remove -y --purge build-essential && \
apt-get autoremove -y --purge && \
rm -rf /var/lib/apt/lists/*
RUN pipenv lock -r > requirements.txt && \ FROM python:3.7-slim-buster
pipenv --rm && \
pip install -r requirements.txt --no-cache-dir && \ COPY --from=locker /app/requirements.txt /app/
adduser --system --no-create-home passbook && \
chown -R passbook /app WORKDIR /app/
RUN pip install -r requirements.txt --no-cache-dir && \
adduser --system --no-create-home --uid 1000 --group --home /app passbook

View file

@ -1,5 +1,3 @@
FROM docker.beryju.org/passbook/base:latest FROM docker.beryju.org/passbook/base:latest
RUN pipenv lock --dev -r > requirements-dev.txt && \ RUN pip install -r /app/requirements-dev.txt --no-cache-dir
pipenv --rm && \
pip install -r /app/requirements-dev.txt --no-cache-dir

View file

@ -20,28 +20,15 @@ services:
- internal - internal
labels: labels:
- traefik.enable=false - traefik.enable=false
database-migrate:
build:
context: .
image: docker.beryju.org/passbook/server:${TAG:-test}
command:
- ./manage.py
- migrate
networks:
- internal
restart: 'no'
environment:
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
server: server:
build: build:
context: . context: .
image: docker.beryju.org/passbook/server:${TAG:-test} image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
command: command:
- uwsgi - uwsgi
- uwsgi.ini - uwsgi.ini
environment: environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis - PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql - PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
@ -54,15 +41,21 @@ services:
- traefik.docker.network=internal - traefik.docker.network=internal
- traefik.frontend.rule=PathPrefix:/ - traefik.frontend.rule=PathPrefix:/
worker: worker:
image: docker.beryju.org/passbook/server:${TAG:-test} image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
command: command:
- ./manage.py - celery
- worker - worker
- --autoscale=10,3
- -E
- -B
- -A=passbook.root.celery
- -s=/tmp/celerybeat-schedule
networks: networks:
- internal - internal
labels: labels:
- traefik.enable=false - traefik.enable=false
environment: environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis - PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql - PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
@ -70,7 +63,7 @@ services:
build: build:
context: . context: .
dockerfile: static.Dockerfile dockerfile: static.Dockerfile
image: docker.beryju.org/passbook/static:${TAG:-test} image: docker.beryju.org/passbook/static:latest
networks: networks:
- internal - internal
labels: labels:

View file

@ -39,7 +39,7 @@ http {
gzip on; gzip on;
gzip_types application/javascript image/* text/css; gzip_types application/javascript image/* text/css;
gunzip on; gunzip on;
add_header X-passbook-Version 0.6.2-beta; add_header X-passbook-Version 0.6.4-beta;
add_header Vary X-passbook-Version; add_header Vary X-passbook-Version;
root /data/; root /data/;

View file

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.6.2-beta" appVersion: "0.6.4-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: "0.6.2-beta" version: "0.6.4-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View file

@ -36,6 +36,7 @@ spec:
- -E - -E
- -B - -B
- -A=passbook.root.celery - -A=passbook.root.celery
- -s=/tmp/celerybeat-schedule
volumeMounts: volumeMounts:
- mountPath: /etc/passbook - mountPath: /etc/passbook
name: config-volume name: config-volume

View file

@ -2,7 +2,7 @@
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
image: image:
tag: 0.6.2-beta tag: 0.6.4-beta
nameOverride: "" nameOverride: ""

View file

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.6.2-beta' __version__ = '0.6.4-beta'

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2019-10-10 11:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='nonce',
name='description',
field=models.TextField(blank=True, default=''),
),
]

View file

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -56,6 +57,7 @@ class User(AbstractUser):
self.password_change_date = now() self.password_change_date = now()
return super().set_password(password) return super().set_password(password)
class Provider(models.Model): class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -69,11 +71,26 @@ class Provider(models.Model):
return getattr(self, 'name') return getattr(self, 'name')
return super().__str__() return super().__str__()
class PolicyModel(UUIDModel, CreatedUpdatedModel): class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have policies applied to it""" """Base model which can have policies applied to it"""
policies = models.ManyToManyField('Policy', blank=True) 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): class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
@ -86,11 +103,10 @@ class Factor(PolicyModel):
type = '' type = ''
form = '' form = ''
def has_user_settings(self): def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return False if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a tuple or string, string, string where the first string user settings are available, or an instanace of UserSettings."""
is the name the item has, the second string is the icon and the third is the view-name.""" return None
return False
def __str__(self): def __str__(self):
return f"Factor {self.slug}" return f"Factor {self.slug}"
@ -147,11 +163,10 @@ class Source(PolicyModel):
"""Return additional Info, such as a callback URL. Show in the administration interface.""" """Return additional Info, such as a callback URL. Show in the administration interface."""
return None return None
def has_user_settings(self): def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return False if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a tuple or string, string, string where the first string user settings are available, or an instanace of UserSettings."""
is the name the item has, the second string is the icon and the third is the view-name.""" return None
return False
def __str__(self): def __str__(self):
return self.name return self.name
@ -242,21 +257,29 @@ class Invitation(UUIDModel):
verbose_name = _('Invitation') verbose_name = _('Invitation')
verbose_name_plural = _('Invitations') verbose_name_plural = _('Invitations')
class Nonce(UUIDModel): class Nonce(UUIDModel):
"""One-time link for password resets/sign-up-confirmations""" """One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration) expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE) user = models.ForeignKey('User', on_delete=models.CASCADE)
expiring = models.BooleanField(default=True) 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): def __str__(self):
return f"Nonce f{self.uuid.hex} (expires={self.expires})" return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
class Meta: class Meta:
verbose_name = _('Nonce') verbose_name = _('Nonce')
verbose_name_plural = _('Nonces') verbose_name_plural = _('Nonces')
class PropertyMapping(UUIDModel): class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data.""" """User-defined key -> x mapping which can be used by providers to expose extra data."""

View file

@ -46,9 +46,6 @@
<script src="{% static 'js/passbook.js' %}"></script> <script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %} {% block scripts %}
{% endblock %} {% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body> </body>
</html> </html>

View file

@ -46,9 +46,6 @@
<script src="{% static 'js/passbook.js' %}"></script> <script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %} {% block scripts %}
{% endblock %} {% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body> </body>
</html> </html>

View file

@ -23,37 +23,18 @@
</div> </div>
<nav class="collapse navbar-collapse"> <nav class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility"> <ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
<li class="dropdown"> <a href="{% url 'passbook_core:auth-logout' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown" <span title="Username" class="fa fa-sign-out"></span>
aria-haspopup="true" aria-expanded="true"> <span class="dropdown-title">
<span title="Help" class="fa pficon-help"></span> {% trans 'Logout' %}
</button> </span>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> </a>
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %} <a href="{% url 'passbook_core:user-settings' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li>
</ul>
</li>
<li class="dropdown">
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span> <span title="Username" class="fa pficon-user"></span>
<span class="dropdown-title"> <span class="dropdown-title">
{{ user.username }} <span class="caret"></span> {{ user.username }}
</span> </span>
</button> </a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a>
</li>
<li>
<a href="{% url 'passbook_core:user-change-password' %}">{% trans 'Change Password' %}</a>
</li>
<li class="divider"></li>
<li>
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</li>
</ul>
</li>
</ul> </ul>
</nav> </nav>
</nav> </nav>

View file

@ -1,36 +0,0 @@
{% load static %}
{% load i18n %}
{% load cache %}
{% load utils %}
<div class="modal fade" id="about-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content about-modal-pf">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
</div>
<div class="modal-body">
<h1>{% trans 'passbook' %}</h1>
<div class="product-versions-pf">
<ul class="list-unstyled">
{% app_versions as vers %}
{% cache 600 versions %}
{% for app, ver in vers.items %}
<li><strong>{{ app }}</strong> {{ ver }}</li>
{% endfor %}
{% endcache %}
</ul>
</div>
<div class="trademark-pf">
Trademark and Copyright Information
</div>
</div>
<div class="modal-footer">
<img style="max-height:64px;" src="{% static 'img/logo.png' %}" alt=" Symbol">
</div>
</div>
</div>
</div>

View file

@ -16,21 +16,25 @@
<i class="fa pficon-edit"></i> {% trans 'Details' %} <i class="fa pficon-edit"></i> {% trans 'Details' %}
</a> </a>
</li> </li>
<li class="nav-divider"></li>
{% user_factors as uf %} {% user_factors as uf %}
{% for name, icon, link in uf %} {% if uf %}
<li class="{% is_active link %}"> <li class="nav-divider"></li>
<a href="{% url link %}"> {% endif %}
<i class="{{ icon }}"></i> {{ name }} {% for user_settings in uf %}
<li class="{% is_active user_settings.view_name %}">
<a href="{% url user_settings.view_name %}">
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
<li class="nav-divider"></li>
{% user_sources as us %} {% user_sources as us %}
{% for name, icon, link in us %} {% if us %}
<li class="{% if link == request.get_full_path %} active {% endif %}"> <li class="nav-divider"></li>
<a href="{{ link }}"> {% endif %}
<i class="{{ icon }}"></i> {{ name }} {% for user_settings in us %}
<li class="{% if user_settings.view_name == request.get_full_path %} active {% endif %}">
<a href="{{ user_settings.view_name }}">
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -1,37 +1,38 @@
"""passbook user settings template tags""" """passbook user settings template tags"""
from typing import List
from django import template from django import template
from django.template.context import RequestContext from django.template.context import RequestContext
from passbook.core.models import Factor, Source from passbook.core.models import Factor, Source, UserSettings
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_factors(context: RequestContext): def user_factors(context: RequestContext) -> List[UserSettings]:
"""Return list of all factors which apply to user""" """Return list of all factors which apply to user"""
user = context.get('request').user user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
matching_factors = [] matching_factors: List[UserSettings] = []
for factor in _all_factors: for factor in _all_factors:
_link = factor.has_user_settings() user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all()) policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build() policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link: if policy_engine.passing and user_settings:
matching_factors.append(_link) matching_factors.append(user_settings)
return matching_factors return matching_factors
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_sources(context: RequestContext): def user_sources(context: RequestContext) -> List[UserSettings]:
"""Return a list of all sources which are enabled for the user""" """Return a list of all sources which are enabled for the user"""
user = context.get('request').user user = context.get('request').user
_all_sources = Source.objects.filter(enabled=True).select_subclasses() _all_sources = Source.objects.filter(enabled=True).select_subclasses()
matching_sources = [] matching_sources: List[UserSettings] = []
for factor in _all_sources: for factor in _all_sources:
_link = factor.has_user_settings() user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all()) policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build() policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link: if policy_engine.passing and user_settings:
matching_sources.append(_link) matching_sources.append(user_settings)
return matching_sources return matching_sources

View file

@ -0,0 +1,5 @@
"""captcha factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_captcha')

View file

@ -1,6 +1,8 @@
"""passbook captcha factor forms""" """passbook captcha factor forms"""
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.factors.captcha.models import CaptchaFactor from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS from passbook.factors.forms import GENERAL_FIELDS
@ -21,6 +23,7 @@ class CaptchaFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'public_key': forms.TextInput(), 'public_key': forms.TextInput(),
'private_key': forms.TextInput(), 'private_key': forms.TextInput(),
} }

View file

@ -3,7 +3,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Factor from passbook.core.models import Factor, UserSettings
class OTPFactor(Factor): class OTPFactor(Factor):
@ -15,8 +15,8 @@ class OTPFactor(Factor):
type = 'passbook.factors.otp.factors.OTPFactor' type = 'passbook.factors.otp.factors.OTPFactor'
form = 'passbook.factors.otp.forms.OTPFactorForm' form = 'passbook.factors.otp.forms.OTPFactorForm'
def has_user_settings(self): def user_settings(self) -> UserSettings:
return _('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings' return UserSettings(_('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings')
def __str__(self): def __str__(self):
return f"OTP Factor {self.slug}" return f"OTP Factor {self.slug}"

View file

@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Factor, Policy, User from passbook.core.models import Factor, Policy, User, UserSettings
class PasswordFactor(Factor): class PasswordFactor(Factor):
@ -16,8 +16,9 @@ class PasswordFactor(Factor):
type = 'passbook.factors.password.factor.PasswordFactor' type = 'passbook.factors.password.factor.PasswordFactor'
form = 'passbook.factors.password.forms.PasswordFactorForm' form = 'passbook.factors.password.forms.PasswordFactorForm'
def has_user_settings(self): def user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password' return UserSettings(_('Change Password'), 'pficon-key',
'passbook_core:user-change-password')
def password_passes(self, user: User) -> bool: def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception""" """Return true if user's password passes, otherwise False or raise Exception"""

View file

@ -16,7 +16,7 @@ debug: false
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true error_report_enabled: true
domain: passbook.local domain: localhost
passbook: passbook:
sign_up: sign_up:

View file

11
passbook/recovery/apps.py Normal file
View file

@ -0,0 +1,11 @@
"""passbook Recovery app config"""
from django.apps import AppConfig
class PassbookRecoveryConfig(AppConfig):
"""passbook Recovery app config"""
name = 'passbook.recovery'
label = 'passbook_recovery'
verbose_name = 'passbook Recovery'
mountpoint = 'recovery/'

View file

View file

@ -0,0 +1,46 @@
"""passbook recovery createkey command"""
from datetime import timedelta
from getpass import getuser
from django.core.management.base import BaseCommand
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Nonce, User
from passbook.lib.config import CONFIG
LOGGER = get_logger()
class Command(BaseCommand):
"""Create Nonce used to recover access"""
help = _('Create a Key which can be used to restore access to passbook.')
def add_arguments(self, parser):
parser.add_argument('duration', default=1, action='store',
help='How long the token is valid for (in years).')
parser.add_argument('user', action='store',
help='Which user the Token gives access to.')
def get_url(self, nonce: Nonce) -> str:
"""Get full recovery link"""
path = reverse('passbook_recovery:use-nonce', kwargs={'uuid': str(nonce.uuid)})
return f"https://{CONFIG.y('domain')}{path}"
def handle(self, *args, **options):
"""Create Nonce used to recover access"""
duration = int(options.get('duration', 1))
delta = timedelta(days=duration * 365.2425)
_now = now()
expiry = _now + delta
user = User.objects.get(username=options.get('user'))
nonce = Nonce.objects.create(
expires=expiry,
user=user,
description=f'Recovery Nonce generated by {getuser()} on {_now}')
self.stdout.write((f"Store this link safely, as it will allow"
f" anyone to access passbook as {user}."))
self.stdout.write(self.get_url(nonce))

View file

@ -0,0 +1,9 @@
"""recovery views"""
from django.urls import path
from passbook.recovery.views import UseNonceView
urlpatterns = [
path('use-nonce/<uuid:uuid>/', UseNonceView.as_view(), name='use-nonce'),
]

View file

@ -0,0 +1,24 @@
"""recovery views"""
from django.contrib import messages
from django.contrib.auth import login
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext as _
from django.views import View
from passbook.core.models import Nonce
class UseNonceView(View):
"""Use nonce to login"""
def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
"""Check if nonce exists, log user in and delete nonce."""
nonce: Nonce = get_object_or_404(Nonce, pk=uuid)
if nonce.is_expired:
nonce.delete()
raise Http404
login(request, nonce.user, backend='django.contrib.auth.backends.ModelBackend')
nonce.delete()
messages.warning(request, _("Used recovery-link to authenticate."))
return redirect('passbook_core:overview')

View file

@ -72,6 +72,7 @@ INSTALLED_APPS = [
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
'passbook.lib.apps.PassbookLibConfig', 'passbook.lib.apps.PassbookLibConfig',
'passbook.audit.apps.PassbookAuditConfig', 'passbook.audit.apps.PassbookAuditConfig',
'passbook.recovery.apps.PassbookRecoveryConfig',
'passbook.sources.ldap.apps.PassbookSourceLDAPConfig', 'passbook.sources.ldap.apps.PassbookSourceLDAPConfig',
'passbook.sources.oauth.apps.PassbookSourceOAuthConfig', 'passbook.sources.oauth.apps.PassbookSourceOAuthConfig',
@ -117,7 +118,7 @@ CACHES = {
} }
DJANGO_REDIS_IGNORE_EXCEPTIONS = True DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_CACHE_ALIAS = "default" SESSION_CACHE_ALIAS = "default"
MIDDLEWARE = [ MIDDLEWARE = [

View file

View file

@ -4,7 +4,7 @@ from django.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Source, UserSourceConnection from passbook.core.models import Source, UserSettings, UserSourceConnection
from passbook.sources.oauth.clients import get_client from passbook.sources.oauth.clients import get_client
@ -37,18 +37,15 @@ class OAuthSource(Source):
reverse_lazy('passbook_sources_oauth:oauth-client-callback', reverse_lazy('passbook_sources_oauth:oauth-client-callback',
kwargs={'source_slug': self.slug}) kwargs={'source_slug': self.slug})
def has_user_settings(self): def user_settings(self) -> UserSettings:
"""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."""
icon_type = self.provider_type icon_type = self.provider_type
if icon_type == 'azure ad': if icon_type == 'azure ad':
icon_type = 'windows' icon_type = 'windows'
icon_class = 'fa fa-%s' % icon_type icon_class = 'fa fa-%s' % icon_type
view_name = 'passbook_sources_oauth:oauth-client-user' view_name = 'passbook_sources_oauth:oauth-client-user'
return self.name, icon_class, reverse((view_name), kwargs={ return UserSettings(self.name, icon_class, reverse((view_name), kwargs={
'source_slug': self.slug 'source_slug': self.slug
}) }))
class Meta: class Meta: