diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4fd00a571..15d87c6a0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.6.2-beta +current_version = 0.6.4-beta tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P.*) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7eb45938b..0f4d337ef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.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.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 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.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 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 @@ -95,7 +95,7 @@ build-passbook-server: 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.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: - 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.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: - tags - /^version/.*$/ diff --git a/Dockerfile b/Dockerfile index 6fe1180fd..975079cf0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,6 @@ FROM docker.beryju.org/passbook/base:latest COPY ./passbook/ /app/passbook COPY ./manage.py /app/ COPY ./docker/uwsgi.ini /app/ -RUN chown -R passbook: /app WORKDIR /app/ diff --git a/Pipfile b/Pipfile index f92c93292..211c02a1b 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ celery = "*" cherrypy = "*" defusedxml = "*" django = "*" +kombu = "==4.5.0" django-cors-middleware = "*" django-filters = "*" django-ipware = "*" @@ -18,7 +19,6 @@ django-otp = "*" django-recaptcha = "*" django-redis = "*" django-rest-framework = "*" -djangorestframework = "==3.9.4" drf-yasg = "*" ldap3 = "*" lxml = "*" @@ -35,17 +35,17 @@ service_identity = "*" signxml = "*" urllib3 = {extras = ["secure"],version = "*"} structlog = "*" +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 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e6676f4f7..56e555694 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d03d1e494d28a90b39edd1d489afdb5e39ec09bceb18daa2a54b2cc7de61d83c" + "sha256": "94b3d5140f0c31dac1fc77af75a0df30ae4fb0571bf6b7fcd722487c63dc1872" }, "pipfile-spec": 6, "requires": { @@ -18,17 +18,17 @@ "default": { "amqp": { "hashes": [ - "sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68", - "sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a" + "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8", + "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d" ], - "version": "==2.5.1" + "version": "==2.5.2" }, "asn1crypto": { "hashes": [ - "sha256:d02bf8ea1b964a5ff04ac7891fe3a39150045d1e5e4fe99273ba677d11b92a04", - "sha256:f822954b90c4c44f002e2cd46d636ab630f1fe4df22c816a82b66505c404eb2a" + "sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292", + "sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f" ], - "version": "==1.0.0" + "version": "==1.0.1" }, "attrs": { "hashes": [ @@ -101,10 +101,10 @@ }, "cheroot": { "hashes": [ - "sha256:6168371ab9aaf574ac5f75675f244bbfebf990202bf75048065e9d675b9ae719", - "sha256:8cc7c28961db2e13d0cac6b234a589a314c1844f7bbf54e67888ac9a2e25ac59" + "sha256:3ff64073efa35b39d5e107410f5c79664dc8c6c5990651e970740c80ab8878a8", + "sha256:d523a1525258730026aa35b86c8c47c8d0e3892fb89f0f39157d4b32a50edf05" ], - "version": "==7.0.0" + "version": "==8.1.0" }, "cherrypy": { "hashes": [ @@ -242,11 +242,10 @@ }, "djangorestframework": { "hashes": [ - "sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", - "sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb" + "sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8", + "sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090" ], - "index": "pypi", - "version": "==3.9.4" + "version": "==3.10.3" }, "drf-yasg": { "hashes": [ @@ -276,13 +275,6 @@ ], "version": "==2.8" }, - "importlib-metadata": { - "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" - ], - "version": "==0.23" - }, "inflection": { "hashes": [ "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca" @@ -304,17 +296,18 @@ }, "jinja2": { "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", + "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" ], - "version": "==2.10.1" + "version": "==2.10.3" }, "kombu": { "hashes": [ - "sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb", - "sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566" + "sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd", + "sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181" ], - "version": "==4.6.5" + "index": "pypi", + "version": "==4.5.0" }, "ldap3": { "hashes": [ @@ -466,10 +459,10 @@ }, "pyasn1-modules": { "hashes": [ - "sha256:43c17a83c155229839cc5c6b868e8d0c6041dba149789b6d6e28801c64821722", - "sha256:e30199a9d221f1b26c885ff3d87fd08694dbbe18ed0e8e405a2a7126d30ce4c0" + "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b", + "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3" ], - "version": "==0.2.6" + "version": "==0.2.7" }, "pycparser": { "hashes": [ @@ -566,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": [ @@ -736,13 +759,6 @@ "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" ], "version": "==2.0" - }, - "zipp": { - "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" - ], - "version": "==0.6.0" } }, "develop": { @@ -751,7 +767,6 @@ "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" ], - "index": "pypi", "version": "==2.2.5" }, "autopep8": { @@ -976,10 +991,10 @@ }, "pytz": { "hashes": [ - "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", - "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" ], - "version": "==2019.2" + "version": "==2019.3" }, "pyyaml": { "hashes": [ diff --git a/README.md b/README.md index 40a1f13ed..11172f736 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ ## 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 ``` diff --git a/base.Dockerfile b/base.Dockerfile index 97a7005d5..f46c29376 100644 --- a/base.Dockerfile +++ b/base.Dockerfile @@ -1,19 +1,19 @@ -FROM python:3.7-slim-stretch +FROM python:3.7-slim-buster as locker COPY ./Pipfile /app/ COPY ./Pipfile.lock /app/ WORKDIR /app/ -RUN apt-get update && \ - apt-get install -y --no-install-recommends build-essential && \ - pip install pipenv uwsgi --no-cache-dir && \ - apt-get remove -y --purge build-essential && \ - apt-get autoremove -y --purge && \ - rm -rf /var/lib/apt/lists/* +RUN pip install pipenv && \ + pipenv lock -r > requirements.txt && \ + pipenv lock -rd > requirements-dev.txt -RUN pipenv lock -r > requirements.txt && \ - pipenv --rm && \ - pip install -r requirements.txt --no-cache-dir && \ - adduser --system --no-create-home passbook && \ - chown -R passbook /app +FROM python:3.7-slim-buster + +COPY --from=locker /app/requirements.txt /app/ + +WORKDIR /app/ + +RUN pip install -r requirements.txt --no-cache-dir && \ + adduser --system --no-create-home --uid 1000 --group --home /app passbook diff --git a/dev.Dockerfile b/dev.Dockerfile index 4bdc708c8..9081e6532 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,5 +1,3 @@ FROM docker.beryju.org/passbook/base:latest -RUN pipenv lock --dev -r > requirements-dev.txt && \ - pipenv --rm && \ - pip install -r /app/requirements-dev.txt --no-cache-dir +RUN pip install -r /app/requirements-dev.txt --no-cache-dir diff --git a/docker-compose.yml b/docker-compose.yml index 54d856ad5..a510365c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,28 +20,15 @@ services: - internal labels: - 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: build: context: . - image: docker.beryju.org/passbook/server:${TAG:-test} + 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} @@ -54,15 +41,21 @@ services: - traefik.docker.network=internal - traefik.frontend.rule=PathPrefix:/ worker: - image: docker.beryju.org/passbook/server:${TAG:-test} + image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest} command: - - ./manage.py + - 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} @@ -70,7 +63,7 @@ services: build: context: . dockerfile: static.Dockerfile - image: docker.beryju.org/passbook/static:${TAG:-test} + image: docker.beryju.org/passbook/static:latest networks: - internal labels: diff --git a/docker/nginx.conf b/docker/nginx.conf index ce75a986a..ba7480c6d 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -39,7 +39,7 @@ http { gzip on; gzip_types application/javascript image/* text/css; 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; root /data/; diff --git a/helm/passbook/Chart.yaml b/helm/passbook/Chart.yaml index 776db847b..c558232ee 100644 --- a/helm/passbook/Chart.yaml +++ b/helm/passbook/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v1 -appVersion: "0.6.2-beta" +appVersion: "0.6.4-beta" description: A Helm chart for 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 diff --git a/helm/passbook/templates/worker-deployment.yaml b/helm/passbook/templates/worker-deployment.yaml index 2d1cc6ddd..da6512062 100644 --- a/helm/passbook/templates/worker-deployment.yaml +++ b/helm/passbook/templates/worker-deployment.yaml @@ -36,6 +36,7 @@ spec: - -E - -B - -A=passbook.root.celery + - -s=/tmp/celerybeat-schedule volumeMounts: - mountPath: /etc/passbook name: config-volume diff --git a/helm/passbook/values.yaml b/helm/passbook/values.yaml index 2fb7ba494..66dde1506 100644 --- a/helm/passbook/values.yaml +++ b/helm/passbook/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. image: - tag: 0.6.2-beta + tag: 0.6.4-beta nameOverride: "" diff --git a/passbook/__init__.py b/passbook/__init__.py index 3e0f840f1..1e1dfaa3c 100644 --- a/passbook/__init__.py +++ b/passbook/__init__.py @@ -1,2 +1,2 @@ """passbook""" -__version__ = '0.6.2-beta' +__version__ = '0.6.4-beta' diff --git a/passbook/core/migrations/0002_nonce_description.py b/passbook/core/migrations/0002_nonce_description.py new file mode 100644 index 000000000..2e98fe1b8 --- /dev/null +++ b/passbook/core/migrations/0002_nonce_description.py @@ -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=''), + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 82bdbf9e3..4b413151e 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -2,6 +2,7 @@ from datetime import timedelta from random import SystemRandom from time import sleep +from typing import Optional from uuid import uuid4 from django.contrib.auth.models import AbstractUser @@ -56,6 +57,7 @@ class User(AbstractUser): self.password_change_date = now() return super().set_password(password) + class Provider(models.Model): """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" @@ -69,11 +71,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""" @@ -86,11 +103,10 @@ 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 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 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 @@ -242,21 +257,29 @@ class Invitation(UUIDModel): 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 f"Nonce f{self.uuid.hex} (expires={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.""" diff --git a/passbook/core/templates/base/skeleton.html b/passbook/core/templates/base/skeleton.html index cc3935343..80b231e4a 100644 --- a/passbook/core/templates/base/skeleton.html +++ b/passbook/core/templates/base/skeleton.html @@ -46,9 +46,6 @@ {% block scripts %} {% endblock %} -
- {% include 'partials/about_modal.html' %} -
diff --git a/passbook/core/templates/base/skeleton_login.html b/passbook/core/templates/base/skeleton_login.html index a747775c9..1c6e955c4 100644 --- a/passbook/core/templates/base/skeleton_login.html +++ b/passbook/core/templates/base/skeleton_login.html @@ -46,9 +46,6 @@ {% block scripts %} {% endblock %} -
- {% include 'partials/about_modal.html' %} -
diff --git a/passbook/core/templates/overview/base.html b/passbook/core/templates/overview/base.html index 04982586d..f48a0f234 100644 --- a/passbook/core/templates/overview/base.html +++ b/passbook/core/templates/overview/base.html @@ -23,37 +23,18 @@ diff --git a/passbook/core/templates/partials/about_modal.html b/passbook/core/templates/partials/about_modal.html deleted file mode 100644 index fc5251b70..000000000 --- a/passbook/core/templates/partials/about_modal.html +++ /dev/null @@ -1,36 +0,0 @@ -{% load static %} -{% load i18n %} -{% load cache %} - -{% load utils %} - - diff --git a/passbook/core/templates/user/base.html b/passbook/core/templates/user/base.html index f4c69937e..953dd39e4 100644 --- a/passbook/core/templates/user/base.html +++ b/passbook/core/templates/user/base.html @@ -16,23 +16,27 @@ {% trans 'Details' %} - {% user_factors as uf %} - {% for name, icon, link in uf %} - + {% if uf %} + + {% endif %} + {% for user_settings in uf %} +
  • + + {{ user_settings.name }} + +
  • {% endfor %} - {% user_sources as us %} - {% for name, icon, link in us %} - + {% if us %} + + {% endif %} + {% for user_settings in us %} +
  • + + {{ user_settings.name }} + +
  • {% endfor %} diff --git a/passbook/core/templatetags/passbook_user_settings.py b/passbook/core/templatetags/passbook_user_settings.py index f560d03d3..18c332408 100644 --- a/passbook/core/templatetags/passbook_user_settings.py +++ b/passbook/core/templatetags/passbook_user_settings.py @@ -1,37 +1,38 @@ """passbook user settings template tags""" +from typing import List from django import template 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 register = template.Library() @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""" user = context.get('request').user _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() - matching_factors = [] + matching_factors: List[UserSettings] = [] for factor in _all_factors: - _link = factor.has_user_settings() + user_settings = factor.user_settings() policy_engine = PolicyEngine(factor.policies.all()) policy_engine.for_user(user).with_request(context.get('request')).build() - if policy_engine.passing and _link: - matching_factors.append(_link) + if policy_engine.passing and user_settings: + matching_factors.append(user_settings) return matching_factors @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""" user = context.get('request').user _all_sources = Source.objects.filter(enabled=True).select_subclasses() - matching_sources = [] + matching_sources: List[UserSettings] = [] for factor in _all_sources: - _link = factor.has_user_settings() + user_settings = factor.user_settings() policy_engine = PolicyEngine(factor.policies.all()) policy_engine.for_user(user).with_request(context.get('request')).build() - if policy_engine.passing and _link: - matching_sources.append(_link) + if policy_engine.passing and user_settings: + matching_sources.append(user_settings) return matching_sources diff --git a/passbook/factors/captcha/admin.py b/passbook/factors/captcha/admin.py new file mode 100644 index 000000000..50049a8d0 --- /dev/null +++ b/passbook/factors/captcha/admin.py @@ -0,0 +1,5 @@ +"""captcha factor admin""" + +from passbook.lib.admin import admin_autoregister + +admin_autoregister('passbook_factors_captcha') diff --git a/passbook/factors/captcha/forms.py b/passbook/factors/captcha/forms.py index 687b44a46..fdbdeca7f 100644 --- a/passbook/factors/captcha/forms.py +++ b/passbook/factors/captcha/forms.py @@ -1,6 +1,8 @@ """passbook captcha factor forms""" from captcha.fields import ReCaptchaField 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.forms import GENERAL_FIELDS @@ -21,6 +23,7 @@ class CaptchaFactorForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), + 'policies': FilteredSelectMultiple(_('policies'), False), 'public_key': forms.TextInput(), 'private_key': forms.TextInput(), } diff --git a/passbook/factors/otp/models.py b/passbook/factors/otp/models.py index b86233a20..31d64df97 100644 --- a/passbook/factors/otp/models.py +++ b/passbook/factors/otp/models.py @@ -3,7 +3,7 @@ from django.db import models from django.utils.translation import gettext as _ -from passbook.core.models import Factor +from passbook.core.models import Factor, UserSettings class OTPFactor(Factor): @@ -15,8 +15,8 @@ class OTPFactor(Factor): type = 'passbook.factors.otp.factors.OTPFactor' form = 'passbook.factors.otp.forms.OTPFactorForm' - def has_user_settings(self): - return _('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings' + def user_settings(self) -> UserSettings: + return UserSettings(_('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings') def __str__(self): return f"OTP Factor {self.slug}" diff --git a/passbook/factors/password/models.py b/passbook/factors/password/models.py index 31b7ada9d..513139c49 100644 --- a/passbook/factors/password/models.py +++ b/passbook/factors/password/models.py @@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models 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): @@ -16,8 +16,9 @@ class PasswordFactor(Factor): type = 'passbook.factors.password.factor.PasswordFactor' form = 'passbook.factors.password.forms.PasswordFactorForm' - def has_user_settings(self): - return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password' + def user_settings(self): + return UserSettings(_('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""" diff --git a/passbook/lib/default.yml b/passbook/lib/default.yml index 8c0119294..57ef14365 100644 --- a/passbook/lib/default.yml +++ b/passbook/lib/default.yml @@ -16,7 +16,7 @@ debug: false # Error reporting, sends stacktrace to sentry.services.beryju.org error_report_enabled: true -domain: passbook.local +domain: localhost passbook: sign_up: diff --git a/passbook/recovery/__init__.py b/passbook/recovery/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/apps.py b/passbook/recovery/apps.py new file mode 100644 index 000000000..0a6db1a06 --- /dev/null +++ b/passbook/recovery/apps.py @@ -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/' diff --git a/passbook/recovery/management/__init__.py b/passbook/recovery/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/management/commands/__init__.py b/passbook/recovery/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/recovery/management/commands/create_recovery_key.py b/passbook/recovery/management/commands/create_recovery_key.py new file mode 100644 index 000000000..49a3fc3ef --- /dev/null +++ b/passbook/recovery/management/commands/create_recovery_key.py @@ -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)) diff --git a/passbook/recovery/urls.py b/passbook/recovery/urls.py new file mode 100644 index 000000000..36a3f93f6 --- /dev/null +++ b/passbook/recovery/urls.py @@ -0,0 +1,9 @@ +"""recovery views""" + +from django.urls import path + +from passbook.recovery.views import UseNonceView + +urlpatterns = [ + path('use-nonce//', UseNonceView.as_view(), name='use-nonce'), +] diff --git a/passbook/recovery/views.py b/passbook/recovery/views.py new file mode 100644 index 000000000..fd870138e --- /dev/null +++ b/passbook/recovery/views.py @@ -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') diff --git a/passbook/root/settings.py b/passbook/root/settings.py index dab4d9021..0c7041798 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -72,6 +72,7 @@ INSTALLED_APPS = [ 'passbook.api.apps.PassbookAPIConfig', 'passbook.lib.apps.PassbookLibConfig', 'passbook.audit.apps.PassbookAuditConfig', + 'passbook.recovery.apps.PassbookRecoveryConfig', 'passbook.sources.ldap.apps.PassbookSourceLDAPConfig', 'passbook.sources.oauth.apps.PassbookSourceOAuthConfig', @@ -117,7 +118,7 @@ CACHES = { } DJANGO_REDIS_IGNORE_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" MIDDLEWARE = [ diff --git a/passbook/sources/__init__.py b/passbook/sources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/sources/oauth/models.py b/passbook/sources/oauth/models.py index 053889194..be6392966 100644 --- a/passbook/sources/oauth/models.py +++ b/passbook/sources/oauth/models.py @@ -4,7 +4,7 @@ from django.db import models from django.urls import reverse, reverse_lazy 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 @@ -37,18 +37,15 @@ class OAuthSource(Source): reverse_lazy('passbook_sources_oauth:oauth-client-callback', kwargs={'source_slug': self.slug}) - 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.""" + def user_settings(self) -> UserSettings: icon_type = self.provider_type if icon_type == 'azure ad': icon_type = 'windows' icon_class = 'fa fa-%s' % icon_type 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 - }) + })) class Meta: