diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eecdb53e..d5e16383 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,14 @@ Change history ============== +2.3.0 (TBD) +----------- + +.. warning:: + + Two-factor authentication is enabled by default. The ``DISABLE_2FA`` environment variable + can be used to disable it if needed. + 2.2.1 (2024-03-02) ------------------ diff --git a/Dockerfile b/Dockerfile index 2d5e1778..906a7155 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,6 +51,7 @@ COPY --from=backend-build /usr/local/bin/uwsgi /usr/local/bin/uwsgi # Stage 3.2 - Copy source code WORKDIR /app COPY ./bin/docker_start.sh /start.sh +COPY ./bin/celery_worker.sh /celery_worker.sh RUN mkdir /app/log /app/config # copy frontend build statics diff --git a/bin/celery_worker.sh b/bin/celery_worker.sh new file mode 100755 index 00000000..33fdf84a --- /dev/null +++ b/bin/celery_worker.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +LOGLEVEL=${CELERY_LOGLEVEL:-INFO} +CONCURRENCY=${CELERY_WORKER_CONCURRENCY:-1} + +QUEUE=${1:-${CELERY_WORKER_QUEUE:=celery}} +WORKER_NAME=${2:-${CELERY_WORKER_NAME:="${QUEUE}"@%n}} + +_binary=$(which celery) + +if [[ "$ENABLE_COVERAGE" ]]; then + _binary="coverage run $_binary" +fi + +echo "Starting celery worker $WORKER_NAME with queue $QUEUE" +exec $_binary --workdir src --app objects.celery worker \ + -Q $QUEUE \ + -n $WORKER_NAME \ + -l $LOGLEVEL \ + -O fair \ + -c $CONCURRENCY diff --git a/docker-compose.yml b/docker-compose.yml index db1a1f51..12a173d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,14 @@ services: - POSTGRES_USER=${DB_USER:-objects} - POSTGRES_PASSWORD=${DB_PASSWORD:-objects} + redis: + image: redis:7 + command: ["redis-server", "--appendonly", "yes"] + web: - build: . - environment: + build: &web_build + context: . + environment: &web_env - DJANGO_SETTINGS_MODULE=objects.conf.docker - SECRET_KEY=${SECRET_KEY:-1(@f(-6s_u(5fd&1sg^uvu2s(c-9sapw)1era8q&)g)h@cwxxg} - OBJECTS_SUPERUSER_USERNAME=admin @@ -22,8 +27,18 @@ services: - 8000:8000 depends_on: - db - volumes: + - redis + volumes: &web_volumes - media:/app/media # Shared media volume to get access to saved OAS files + celery: + build: *web_build + environment: *web_env + command: /celery_worker.sh + depends_on: + - db + - redis + volumes: *web_volumes + volumes: media: diff --git a/docs/installation/config.rst b/docs/installation/config.rst index 211826a9..e0f82432 100644 --- a/docs/installation/config.rst +++ b/docs/installation/config.rst @@ -29,8 +29,8 @@ Required settings Defaults to ``*`` for the ``docker`` environment and defaults to ``127.0.0.1,localhost`` for the ``dev`` environment. -Database settings ------------------ +Common settings +--------------- * ``DB_HOST``: Hostname of the PostgreSQL database. Defaults to ``db`` for the ``docker`` environment, otherwise defaults to ``localhost``. @@ -43,6 +43,12 @@ Database settings * ``DB_PORT``: Port number of the database. Defaults to ``5432``. +* ``CELERY_BROKER_URL``: URL for the Redis task broker for Celery. Defaults + to ``redis://127.0.0.1:6379/1``. + +* ``CELERY_RESULT_BACKEND``: URL for the Redis result broker for Celery. + Defaults to ``redis://127.0.0.1:6379/1``. + Elastic APM settings -------------------- @@ -84,17 +90,14 @@ Other settings sent to the Notificaties API for operations on the Object endpoint. Defaults to ``True`` for the ``dev`` environment, otherwise defaults to ``False``. -* ``TWO_FACTOR_FORCE_OTP_ADMIN``: Enforce 2 Factor Authentication in the admin or not. - Default ``True``. You'll probably want to disable this when using OIDC. - -* ``TWO_FACTOR_PATCH_ADMIN``: Whether to use the 2 Factor Authentication login flow for - the admin or not. Default ``True``. You'll probably want to disable this when using OIDC. - * ``USE_X_FORWARDED_HOST``: whether to grab the domain/host from the ``X-Forwarded-Host`` header or not. This header is typically set by reverse proxies (such as nginx, traefik, Apache...). Default ``False`` - this is a header that can be spoofed and you need to ensure you control it before enabling this. +* ``DISABLE_2FA``: whether to disable two-factor authentication. Defaults to ``False``. + If set to ``False``, 2FA will be required if not using OIDC. + Initial superuser creation -------------------------- diff --git a/requirements/base.in b/requirements/base.in index 82102590..90f6e98e 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,4 +1,5 @@ # Core python libraries +celery glom # data represenation based on spec Pillow # handle images psycopg2 # database driver @@ -8,13 +9,12 @@ python-decouple # processing of envvar configs jsonschema # Framework libraries -django~=3.2 +django~=4.2 django-admin-index django-axes django-redis django-rosetta -maykin-django-two-factor-auth -maykin-django-two-factor-auth[phonenumbers] +maykin-2fa mozilla-django-oidc-db # API libraries @@ -30,5 +30,6 @@ sentry-sdk # error monitoring elastic-apm # Elastic APM integration # Common ground libraries -vng-api-common[markdown_docs]>=1.6.4 +notifications-api-common +commonground-api-common[markdown_docs] zgw-consumers # external api auths diff --git a/requirements/base.txt b/requirements/base.txt index feaa7bd3..34996741 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,16 +4,28 @@ # # pip-compile --no-emit-index-url requirements/base.in # +amqp==5.2.0 + # via kombu asgiref==3.7.2 # via django +asn1crypto==1.5.1 + # via webauthn attrs==20.3.0 # via # glom # jsonschema +billiard==3.6.4.0 + # via celery boltons==21.0.0 # via # face # glom +cbor2==5.6.1 + # via webauthn +celery==5.2.2 + # via + # -r requirements/base.in + # notifications-api-common certifi==2020.12.5 # via # django-simple-certmanager @@ -24,24 +36,37 @@ cffi==1.16.0 # via cryptography chardet==4.0.0 # via requests +click==8.1.7 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.3.0 + # via celery +commonground-api-common[markdown-docs,markdown_docs]==1.13.0 + # via -r requirements/base.in coreapi==2.3.3 - # via drf-yasg + # via commonground-api-common coreschema==0.0.4 - # via - # coreapi - # drf-yasg + # via coreapi cryptography==41.0.7 # via # django-simple-certmanager # josepy # mozilla-django-oidc # pyopenssl -django==3.2.23 + # webauthn +django==4.2.11 # via # -r requirements/base.in + # commonground-api-common # django-admin-index # django-axes - # django-choices # django-filter # django-formtools # django-jsonform @@ -56,47 +81,44 @@ django==3.2.23 # django-sendfile2 # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers django-admin-index==3.1.0 # via -r requirements/base.in -django-axes==5.41.1 +django-axes==6.3.0 # via -r requirements/base.in -django-choices==2.0.0 - # via vng-api-common -django-filter==2.4.0 +django-filter==23.5 # via # -r requirements/base.in - # vng-api-common + # commonground-api-common django-formtools==2.3 - # via maykin-django-two-factor-auth -django-ipware==3.0.2 - # via django-axes + # via django-two-factor-auth django-jsonform==2.21.4 # via mozilla-django-oidc-db django-markup==1.3 - # via vng-api-common + # via commonground-api-common django-ordered-model==3.7.4 # via django-admin-index django-otp==1.0.6 - # via maykin-django-two-factor-auth + # via django-two-factor-auth django-phonenumber-field==5.2.0 - # via maykin-django-two-factor-auth + # via django-two-factor-auth django-privates==2.0.0.post0 # via django-simple-certmanager -django-redis==5.2.0 +django-redis==5.4.0 # via -r requirements/base.in django-relativedelta==2.0.0 # via zgw-consumers django-rest-framework-condition==0.1.1 - # via vng-api-common + # via commonground-api-common django-rosetta==0.9.8 # via -r requirements/base.in django-sendfile2==0.7.0 @@ -105,27 +127,33 @@ django-simple-certmanager==1.4.1 # via zgw-consumers django-solo==2.2.0 # via + # commonground-api-common # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers -djangorestframework==3.12.4 +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via maykin-2fa +djangorestframework==3.14.0 # via # -r requirements/base.in + # commonground-api-common # djangorestframework-gis # drf-nested-routers # drf-spectacular # drf-yasg - # vng-api-common -djangorestframework-camel-case==1.2.0 - # via vng-api-common + # notifications-api-common +djangorestframework-camel-case==1.4.2 + # via + # commonground-api-common + # notifications-api-common djangorestframework-gis==1.0 # via -r requirements/base.in drf-nested-routers==0.93.3 - # via vng-api-common -drf-spectacular==0.16.0 + # via commonground-api-common +drf-spectacular==0.26.5 # via -r requirements/base.in -drf-yasg==1.20.0 - # via vng-api-common +drf-yasg==1.21.7 + # via commonground-api-common elastic-apm==6.1.1 # via -r requirements/base.in face==20.1.1 @@ -134,7 +162,8 @@ faker==8.1.0 # via zgw-consumers gemma-zds-client==1.0.1 # via - # vng-api-common + # commonground-api-common + # notifications-api-common # zgw-consumers glom==23.5.0 # via @@ -147,51 +176,60 @@ inflection==0.5.1 # drf-spectacular # drf-yasg iso-639==0.4.5 - # via vng-api-common + # via commonground-api-common isodate==0.6.0 - # via vng-api-common + # via commonground-api-common itypes==1.2.0 # via coreapi jinja2==3.1.3 # via coreschema josepy==1.9.0 # via mozilla-django-oidc -jsonschema==3.2.0 +jsonschema==4.17.3 # via # -r requirements/base.in # drf-spectacular -markdown==3.3.4 - # via vng-api-common +kombu==5.3.5 + # via celery +markdown==3.5.2 + # via commonground-api-common markupsafe==2.1.3 # via jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.3 +maykin-2fa==1.0.0 # via -r requirements/base.in mozilla-django-oidc==4.0.0 # via mozilla-django-oidc-db mozilla-django-oidc-db==0.14.1 # via -r requirements/base.in +notifications-api-common==0.2.2 + # via + # -r requirements/base.in + # commonground-api-common oyaml==1.0 - # via vng-api-common + # via commonground-api-common packaging==23.2 # via drf-yasg -phonenumbers==8.12.29 - # via maykin-django-two-factor-auth +phonenumberslite==8.13.30 + # via django-two-factor-auth pillow==10.2.0 # via -r requirements/base.in polib==1.1.1 # via django-rosetta +prompt-toolkit==3.0.43 + # via click-repl psycopg2==2.8.6 # via -r requirements/base.in pycparser==2.20 # via cffi pyjwt==2.4.0 # via + # commonground-api-common # gemma-zds-client - # vng-api-common pyopenssl==23.3.0 # via # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyrsistent==0.17.3 # via jsonschema @@ -206,37 +244,36 @@ python-dotenv==1.0.0 pytz==2021.1 # via # -r requirements/base.in - # django + # celery + # djangorestframework + # drf-yasg pyyaml==6.0.1 # via # drf-spectacular + # drf-yasg # gemma-zds-client # oyaml - # vng-api-common qrcode==6.1 - # via maykin-django-two-factor-auth + # via django-two-factor-auth redis==3.5.3 # via django-redis requests==2.25.1 # via + # commonground-api-common # coreapi # django-rosetta # gemma-zds-client # mozilla-django-oidc # requests-mock - # vng-api-common # zgw-consumers requests-mock==1.8.0 # via zgw-consumers -ruamel-yaml==0.17.4 - # via drf-yasg sentry-sdk==1.0.0 # via -r requirements/base.in -six==1.15.0 +six==1.16.0 # via # django-markup # isodate - # jsonschema # python-dateutil # qrcode # requests-mock @@ -258,12 +295,19 @@ urllib3==1.26.6 # sentry-sdk uwsgi==2.0.21 # via -r requirements/base.in -vng-api-common[markdown-docs]==1.8.0 +vine==5.1.0 # via - # -r requirements/base.in - # vng-api-common + # amqp + # celery + # kombu +wcwidth==0.2.13 + # via prompt-toolkit +webauthn==2.0.0 + # via django-two-factor-auth zgw-consumers==0.27.0 - # via -r requirements/base.in + # via + # -r requirements/base.in + # notifications-api-common # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/ci.txt b/requirements/ci.txt index 0f320a93..4bdc9878 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -4,10 +4,18 @@ # # pip-compile --no-emit-index-url --output-file=requirements/ci.txt requirements/base.txt requirements/test-tools.in # +amqp==5.2.0 + # via + # -r requirements/base.txt + # kombu asgiref==3.7.2 # via # -r requirements/base.txt # django +asn1crypto==1.5.1 + # via + # -r requirements/base.txt + # webauthn attrs==20.3.0 # via # -r requirements/base.txt @@ -15,11 +23,23 @@ attrs==20.3.0 # jsonschema beautifulsoup4==4.9.3 # via webtest +billiard==3.6.4.0 + # via + # -r requirements/base.txt + # celery boltons==21.0.0 # via # -r requirements/base.txt # face # glom +cbor2==5.6.1 + # via + # -r requirements/base.txt + # webauthn +celery==5.2.2 + # via + # -r requirements/base.txt + # notifications-api-common certifi==2020.12.5 # via # -r requirements/base.txt @@ -35,15 +55,37 @@ chardet==4.0.0 # via # -r requirements/base.txt # requests +click==8.1.7 + # via + # -r requirements/base.txt + # celery + # click-didyoumean + # click-plugins + # click-repl +click-didyoumean==0.3.0 + # via + # -r requirements/base.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/base.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/base.txt + # celery +commonground-api-common[markdown-docs]==1.13.0 + # via + # -r requirements/base.txt + # commonground-api-common coreapi==2.3.3 # via # -r requirements/base.txt - # drf-yasg + # commonground-api-common coreschema==0.0.4 # via # -r requirements/base.txt # coreapi - # drf-yasg coverage==4.5.4 # via -r requirements/test-tools.in cryptography==41.0.7 @@ -53,14 +95,15 @@ cryptography==41.0.7 # josepy # mozilla-django-oidc # pyopenssl + # webauthn cssselect==1.1.0 # via pyquery -django==3.2.23 +django==4.2.11 # via # -r requirements/base.txt + # commonground-api-common # django-admin-index # django-axes - # django-choices # django-filter # django-formtools # django-jsonform @@ -75,41 +118,36 @@ django==3.2.23 # django-sendfile2 # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers django-admin-index==3.1.0 # via -r requirements/base.txt -django-axes==5.41.1 +django-axes==6.3.0 # via -r requirements/base.txt -django-choices==2.0.0 +django-filter==23.5 # via # -r requirements/base.txt - # vng-api-common -django-filter==2.4.0 - # via - # -r requirements/base.txt - # vng-api-common + # commonground-api-common django-formtools==2.3 # via # -r requirements/base.txt - # maykin-django-two-factor-auth -django-ipware==3.0.2 - # via - # -r requirements/base.txt - # django-axes + # django-two-factor-auth django-jsonform==2.21.4 # via # -r requirements/base.txt # mozilla-django-oidc-db django-markup==1.3 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # commonground-api-common django-ordered-model==3.7.4 # via # -r requirements/base.txt @@ -117,16 +155,16 @@ django-ordered-model==3.7.4 django-otp==1.0.6 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-phonenumber-field==5.2.0 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-privates==2.0.0.post0 # via # -r requirements/base.txt # django-simple-certmanager -django-redis==5.2.0 +django-redis==5.4.0 # via -r requirements/base.txt django-relativedelta==2.0.0 # via @@ -135,7 +173,7 @@ django-relativedelta==2.0.0 django-rest-framework-condition==0.1.1 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common django-rosetta==0.9.8 # via -r requirements/base.txt django-sendfile2==0.7.0 @@ -149,35 +187,43 @@ django-simple-certmanager==1.4.1 django-solo==2.2.0 # via # -r requirements/base.txt + # commonground-api-common # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via + # -r requirements/base.txt + # django-two-factor-auth + # maykin-2fa django-webtest==1.9.7 # via -r requirements/test-tools.in -djangorestframework==3.12.4 +djangorestframework==3.14.0 # via # -r requirements/base.txt + # commonground-api-common # djangorestframework-gis # drf-nested-routers # drf-spectacular # drf-yasg - # vng-api-common -djangorestframework-camel-case==1.2.0 + # notifications-api-common +djangorestframework-camel-case==1.4.2 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common + # notifications-api-common djangorestframework-gis==1.0 # via -r requirements/base.txt drf-nested-routers==0.93.3 # via # -r requirements/base.txt - # vng-api-common -drf-spectacular==0.16.0 + # commonground-api-common +drf-spectacular==0.26.5 # via -r requirements/base.txt -drf-yasg==1.20.0 +drf-yasg==1.21.7 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common elastic-apm==6.1.1 # via -r requirements/base.txt face==20.1.1 @@ -196,7 +242,8 @@ freezegun==1.1.0 gemma-zds-client==1.0.1 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common + # notifications-api-common # zgw-consumers glom==23.5.0 # via @@ -214,11 +261,11 @@ inflection==0.5.1 iso-639==0.4.5 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common isodate==0.6.0 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common itypes==1.2.0 # via # -r requirements/base.txt @@ -231,46 +278,58 @@ josepy==1.9.0 # via # -r requirements/base.txt # mozilla-django-oidc -jsonschema==3.2.0 +jsonschema==4.17.3 # via # -r requirements/base.txt # drf-spectacular +kombu==5.3.5 + # via + # -r requirements/base.txt + # celery lxml==4.7.1 # via pyquery -markdown==3.3.4 - # via -r requirements/base.txt -markupsafe==2.1.3 +markdown==3.5.2 # via # -r requirements/base.txt - # jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.3 + # commonground-api-common +markupsafe==2.1.3 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # jinja2 +maykin-2fa==1.0.0 + # via -r requirements/base.txt mozilla-django-oidc==4.0.0 # via # -r requirements/base.txt # mozilla-django-oidc-db mozilla-django-oidc-db==0.14.1 # via -r requirements/base.txt +notifications-api-common==0.2.2 + # via + # -r requirements/base.txt + # commonground-api-common oyaml==1.0 # via # -r requirements/base.txt - # vng-api-common + # commonground-api-common packaging==23.2 # via # -r requirements/base.txt # drf-yasg -phonenumbers==8.12.29 +phonenumberslite==8.13.30 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth pillow==10.2.0 # via -r requirements/base.txt polib==1.1.1 # via # -r requirements/base.txt # django-rosetta +prompt-toolkit==3.0.43 + # via + # -r requirements/base.txt + # click-repl psycopg2==2.8.6 # via -r requirements/base.txt pycparser==2.20 @@ -280,13 +339,14 @@ pycparser==2.20 pyjwt==2.4.0 # via # -r requirements/base.txt + # commonground-api-common # gemma-zds-client - # vng-api-common pyopenssl==23.3.0 # via # -r requirements/base.txt # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyquery==1.4.3 # via -r requirements/test-tools.in @@ -307,18 +367,20 @@ python-dotenv==1.0.0 pytz==2021.1 # via # -r requirements/base.txt - # django + # celery + # djangorestframework + # drf-yasg pyyaml==6.0.1 # via # -r requirements/base.txt # drf-spectacular + # drf-yasg # gemma-zds-client # oyaml - # vng-api-common qrcode==6.1 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth redis==3.5.3 # via # -r requirements/base.txt @@ -326,30 +388,25 @@ redis==3.5.3 requests==2.25.1 # via # -r requirements/base.txt + # commonground-api-common # coreapi # django-rosetta # gemma-zds-client # mozilla-django-oidc # requests-mock - # vng-api-common # zgw-consumers requests-mock==1.8.0 # via # -r requirements/base.txt # -r requirements/test-tools.in # zgw-consumers -ruamel-yaml==0.17.4 - # via - # -r requirements/base.txt - # drf-yasg sentry-sdk==1.0.0 # via -r requirements/base.txt -six==1.15.0 +six==1.16.0 # via # -r requirements/base.txt # django-markup # isodate - # jsonschema # python-dateutil # qrcode # requests-mock @@ -384,18 +441,30 @@ urllib3==1.26.6 # sentry-sdk uwsgi==2.0.21 # via -r requirements/base.txt -vng-api-common[markdown-docs]==1.8.0 +vine==5.1.0 # via # -r requirements/base.txt - # vng-api-common + # amqp + # celery + # kombu waitress==2.1.1 # via webtest +wcwidth==0.2.13 + # via + # -r requirements/base.txt + # prompt-toolkit +webauthn==2.0.0 + # via + # -r requirements/base.txt + # django-two-factor-auth webob==1.8.7 # via webtest webtest==2.0.35 # via django-webtest zgw-consumers==0.27.0 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # notifications-api-common # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/dev.txt b/requirements/dev.txt index 1f2e664a..4fcc442b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,10 +6,18 @@ # alabaster==0.7.12 # via sphinx +amqp==5.2.0 + # via + # -r requirements/ci.txt + # kombu asgiref==3.7.2 # via # -r requirements/ci.txt # django +asn1crypto==1.5.1 + # via + # -r requirements/ci.txt + # webauthn attrs==20.3.0 # via # -r requirements/ci.txt @@ -21,6 +29,10 @@ beautifulsoup4==4.9.3 # via # -r requirements/ci.txt # webtest +billiard==3.6.4.0 + # via + # -r requirements/ci.txt + # celery black==23.12.1 # via -r requirements/dev.in boltons==21.0.0 @@ -34,6 +46,14 @@ bump2version==1.0.1 # via bumpversion bumpversion==0.6.0 # via -r requirements/dev.in +cbor2==5.6.1 + # via + # -r requirements/ci.txt + # webauthn +celery==5.2.2 + # via + # -r requirements/ci.txt + # notifications-api-common certifi==2020.12.5 # via # -r requirements/ci.txt @@ -51,19 +71,39 @@ chardet==4.0.0 # requests click==8.1.7 # via + # -r requirements/ci.txt # black + # celery + # click-didyoumean + # click-plugins + # click-repl # pip-tools +click-didyoumean==0.3.0 + # via + # -r requirements/ci.txt + # celery +click-plugins==1.1.1 + # via + # -r requirements/ci.txt + # celery +click-repl==0.3.0 + # via + # -r requirements/ci.txt + # celery +commonground-api-common[markdown-docs]==1.13.0 + # via + # -r requirements/ci.txt + # commonground-api-common commonmark==0.9.1 # via recommonmark coreapi==2.3.3 # via # -r requirements/ci.txt - # drf-yasg + # commonground-api-common coreschema==0.0.4 # via # -r requirements/ci.txt # coreapi - # drf-yasg coverage==4.5.4 # via -r requirements/ci.txt cryptography==41.0.7 @@ -73,16 +113,17 @@ cryptography==41.0.7 # josepy # mozilla-django-oidc # pyopenssl + # webauthn cssselect==1.1.0 # via # -r requirements/ci.txt # pyquery -django==3.2.23 +django==4.2.11 # via # -r requirements/ci.txt + # commonground-api-common # django-admin-index # django-axes - # django-choices # django-debug-toolbar # django-extensions # django-filter @@ -99,39 +140,32 @@ django==3.2.23 # django-sendfile2 # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers django-admin-index==3.1.0 # via -r requirements/ci.txt -django-axes==5.41.1 +django-axes==6.3.0 # via -r requirements/ci.txt -django-choices==2.0.0 - # via - # -r requirements/ci.txt - # vng-api-common django-debug-toolbar==4.2.0 # via -r requirements/dev.in django-extensions==3.2.3 # via -r requirements/dev.in -django-filter==2.4.0 +django-filter==23.5 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common django-formtools==2.3 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth -django-ipware==3.0.2 - # via - # -r requirements/ci.txt - # django-axes + # django-two-factor-auth django-jsonform==2.21.4 # via # -r requirements/ci.txt @@ -145,16 +179,16 @@ django-ordered-model==3.7.4 django-otp==1.0.6 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-phonenumber-field==5.2.0 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-privates==2.0.0.post0 # via # -r requirements/ci.txt # django-simple-certmanager -django-redis==5.2.0 +django-redis==5.4.0 # via -r requirements/ci.txt django-relativedelta==2.0.0 # via @@ -163,7 +197,7 @@ django-relativedelta==2.0.0 django-rest-framework-condition==0.1.1 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common django-rosetta==0.9.8 # via -r requirements/ci.txt django-sendfile2==0.7.0 @@ -177,23 +211,31 @@ django-simple-certmanager==1.4.1 django-solo==2.2.0 # via # -r requirements/ci.txt + # commonground-api-common # mozilla-django-oidc-db - # vng-api-common + # notifications-api-common # zgw-consumers +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via + # -r requirements/ci.txt + # django-two-factor-auth + # maykin-2fa django-webtest==1.9.7 # via -r requirements/ci.txt -djangorestframework==3.12.4 +djangorestframework==3.14.0 # via # -r requirements/ci.txt + # commonground-api-common # djangorestframework-gis # drf-nested-routers # drf-spectacular # drf-yasg - # vng-api-common -djangorestframework-camel-case==1.2.0 + # notifications-api-common +djangorestframework-camel-case==1.4.2 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common + # notifications-api-common djangorestframework-gis==1.0 # via -r requirements/ci.txt docutils==0.18.1 @@ -205,13 +247,13 @@ docutils==0.18.1 drf-nested-routers==0.93.3 # via # -r requirements/ci.txt - # vng-api-common -drf-spectacular==0.16.0 + # commonground-api-common +drf-spectacular==0.26.5 # via -r requirements/ci.txt -drf-yasg==1.20.0 +drf-yasg==1.21.7 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common elastic-apm==6.1.1 # via -r requirements/ci.txt face==20.1.1 @@ -232,7 +274,8 @@ freezegun==1.1.0 gemma-zds-client==1.0.1 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common + # notifications-api-common # zgw-consumers glom==23.5.0 # via @@ -252,11 +295,11 @@ inflection==0.5.1 iso-639==0.4.5 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common isodate==0.6.0 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common isort==5.13.2 # via -r requirements/dev.in itypes==1.2.0 @@ -272,24 +315,26 @@ josepy==1.9.0 # via # -r requirements/ci.txt # mozilla-django-oidc -jsonschema==3.2.0 +jsonschema==4.17.3 # via # -r requirements/ci.txt # drf-spectacular +kombu==5.3.5 + # via + # -r requirements/ci.txt + # celery lxml==4.7.1 # via # -r requirements/ci.txt # pyquery -markdown==3.3.4 +markdown==3.5.2 # via -r requirements/ci.txt markupsafe==2.1.3 # via # -r requirements/ci.txt # jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.3 - # via - # -r requirements/ci.txt - # maykin-django-two-factor-auth +maykin-2fa==1.0.0 + # via -r requirements/ci.txt mccabe==0.7.0 # via flake8 mozilla-django-oidc==4.0.0 @@ -300,10 +345,14 @@ mozilla-django-oidc-db==0.14.1 # via -r requirements/ci.txt mypy-extensions==0.4.3 # via black +notifications-api-common==0.2.2 + # via + # -r requirements/ci.txt + # commonground-api-common oyaml==1.0 # via # -r requirements/ci.txt - # vng-api-common + # commonground-api-common packaging==23.2 # via # -r requirements/ci.txt @@ -313,10 +362,10 @@ packaging==23.2 # sphinx pathspec==0.11.2 # via black -phonenumbers==8.12.29 +phonenumberslite==8.13.30 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth pillow==10.2.0 # via -r requirements/ci.txt pip-tools==7.3.0 @@ -327,6 +376,10 @@ polib==1.1.1 # via # -r requirements/ci.txt # django-rosetta +prompt-toolkit==3.0.43 + # via + # -r requirements/ci.txt + # click-repl psycopg2==2.8.6 # via -r requirements/ci.txt pycodestyle==2.11.1 @@ -344,13 +397,14 @@ pygments==2.17.2 pyjwt==2.4.0 # via # -r requirements/ci.txt + # commonground-api-common # gemma-zds-client - # vng-api-common pyopenssl==23.3.0 # via # -r requirements/ci.txt # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyproject-hooks==1.0.0 # via build @@ -374,18 +428,20 @@ pytz==2021.1 # via # -r requirements/ci.txt # babel - # django + # celery + # djangorestframework + # drf-yasg pyyaml==6.0.1 # via # -r requirements/ci.txt # drf-spectacular + # drf-yasg # gemma-zds-client # oyaml - # vng-api-common qrcode==6.1 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth recommonmark==0.7.1 # via -r requirements/dev.in redis==3.5.3 @@ -395,30 +451,25 @@ redis==3.5.3 requests==2.25.1 # via # -r requirements/ci.txt + # commonground-api-common # coreapi # django-rosetta # gemma-zds-client # mozilla-django-oidc # requests-mock # sphinx - # vng-api-common # zgw-consumers requests-mock==1.8.0 # via # -r requirements/ci.txt # zgw-consumers -ruamel-yaml==0.17.4 - # via - # -r requirements/ci.txt - # drf-yasg sentry-sdk==1.0.0 # via -r requirements/ci.txt -six==1.15.0 +six==1.16.0 # via # -r requirements/ci.txt # django-markup # isodate - # jsonschema # python-dateutil # qrcode # requests-mock @@ -490,14 +541,24 @@ urllib3==1.26.6 # sentry-sdk uwsgi==2.0.21 # via -r requirements/ci.txt -vng-api-common[markdown-docs]==1.8.0 +vine==5.1.0 # via # -r requirements/ci.txt - # vng-api-common + # amqp + # celery + # kombu waitress==2.1.1 # via # -r requirements/ci.txt # webtest +wcwidth==0.2.13 + # via + # -r requirements/ci.txt + # prompt-toolkit +webauthn==2.0.0 + # via + # -r requirements/ci.txt + # django-two-factor-auth webob==1.8.7 # via # -r requirements/ci.txt @@ -509,7 +570,9 @@ webtest==2.0.35 wheel==0.42.0 # via pip-tools zgw-consumers==0.27.0 - # via -r requirements/ci.txt + # via + # -r requirements/ci.txt + # notifications-api-common # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/src/objects/__init__.py b/src/objects/__init__.py index 85d26250..77df6d6b 100644 --- a/src/objects/__init__.py +++ b/src/objects/__init__.py @@ -1,3 +1,6 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) __version__ = "2.2.1" __author__ = "Maykin Media" __homepage__ = "https://github.com/maykinmedia/objects-api" diff --git a/src/objects/accounts/management/commands/createinitialsuperuser.py b/src/objects/accounts/management/commands/createinitialsuperuser.py index 436dadc0..42680e00 100644 --- a/src/objects/accounts/management/commands/createinitialsuperuser.py +++ b/src/objects/accounts/management/commands/createinitialsuperuser.py @@ -1,4 +1,6 @@ import os +import secrets +import string from django.conf import settings from django.contrib.auth.management.commands.createsuperuser import ( @@ -50,7 +52,8 @@ def handle(self, **options): user = qs.get() if not password and options["generate_password"]: - password = self.UserModel.objects.make_random_password(length=20) + alphabet = string.ascii_letters + string.digits + password = "".join(secrets.choice(alphabet) for _ in range(20)) if password: self.stdout.write("Setting user password...") diff --git a/src/objects/api/fields.py b/src/objects/api/fields.py index 3cd1b28f..1e4a2da1 100644 --- a/src/objects/api/fields.py +++ b/src/objects/api/fields.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -42,7 +42,7 @@ def to_internal_value(self, data): try: return self.get_queryset().get_by_url(data) except ObjectDoesNotExist: - self.fail("does_not_exist", value=smart_text(data)) + self.fail("does_not_exist", value=smart_str(data)) except (TypeError, ValueError): self.fail("invalid") diff --git a/src/objects/api/kanalen.py b/src/objects/api/kanalen.py index 2ac56ccf..d2b29bb1 100644 --- a/src/objects/api/kanalen.py +++ b/src/objects/api/kanalen.py @@ -3,7 +3,7 @@ from django.conf import settings from django.db import models -from vng_api_common.notifications.kanalen import Kanaal +from notifications_api_common.kanalen import Kanaal from objects.core.models import ObjectRecord diff --git a/src/objects/api/mixins.py b/src/objects/api/mixins.py index dad3010d..1731b3bf 100644 --- a/src/objects/api/mixins.py +++ b/src/objects/api/mixins.py @@ -1,5 +1,10 @@ from django.db import models +from notifications_api_common.viewsets import ( + NotificationCreateMixin, + NotificationDestroyMixin, + conditional_atomic, +) from rest_framework.exceptions import NotAcceptable from rest_framework.renderers import BrowsableAPIRenderer from vng_api_common.exceptions import PreconditionFailed @@ -10,11 +15,6 @@ GeoMixin as _GeoMixin, extract_header, ) -from vng_api_common.notifications.viewsets import ( - NotificationCreateMixin, - NotificationDestroyMixin, - conditional_atomic, -) class GeoMixin(_GeoMixin): diff --git a/src/objects/api/v1/filters.py b/src/objects/api/v1/filters.py index 49b9e70d..bb36d9f6 100644 --- a/src/objects/api/v1/filters.py +++ b/src/objects/api/v1/filters.py @@ -1,3 +1,5 @@ +from datetime import date as date_ + from django import forms from django.utils.translation import gettext_lazy as _ @@ -108,8 +110,8 @@ def filter_data_attrs(self, queryset, name, value: str): return queryset - def filter_date(self, queryset, name, value: date): + def filter_date(self, queryset, name, value: date_): return queryset.filter_for_date(value) - def filter_registration_date(self, queryset, name, value: date): + def filter_registration_date(self, queryset, name, value: date_): return queryset.filter_for_registration_date(value) diff --git a/src/objects/api/v1/openapi.yaml b/src/objects/api/v1/openapi.yaml index 832f404a..2fdecd42 100644 --- a/src/objects/api/v1/openapi.yaml +++ b/src/objects/api/v1/openapi.yaml @@ -639,8 +639,8 @@ components: typeVersion: type: integer maximum: 32767 - description: Version of the OBJECTTYPE for data in the object record minimum: 0 + description: Version of the OBJECTTYPE for data in the object record data: type: object additionalProperties: {} @@ -661,6 +661,7 @@ components: type: string format: date readOnly: true + nullable: true description: Legal end date of the object record registrationAt: type: string @@ -668,13 +669,17 @@ components: readOnly: true description: The date when the record was registered in the system correctionFor: - type: string - readOnly: true + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record corrected by the current record - correctedBy: - type: string readOnly: true + correctedBy: + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record, which corrects the current record + readOnly: true required: - startAt - typeVersion @@ -786,8 +791,8 @@ components: typeVersion: type: integer maximum: 32767 - description: Version of the OBJECTTYPE for data in the object record minimum: 0 + description: Version of the OBJECTTYPE for data in the object record data: type: object additionalProperties: {} @@ -808,6 +813,7 @@ components: type: string format: date readOnly: true + nullable: true description: Legal end date of the object record registrationAt: type: string @@ -815,12 +821,16 @@ components: readOnly: true description: The date when the record was registered in the system correctionFor: - type: string + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record corrected by the current record correctedBy: - type: string - readOnly: true + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record, which corrects the current record + readOnly: true required: - startAt - typeVersion diff --git a/src/objects/api/v2/openapi.yaml b/src/objects/api/v2/openapi.yaml index d47d0b7f..4e3a2f7f 100644 --- a/src/objects/api/v2/openapi.yaml +++ b/src/objects/api/v2/openapi.yaml @@ -727,8 +727,8 @@ components: typeVersion: type: integer maximum: 32767 - description: Version of the OBJECTTYPE for data in the object record minimum: 0 + description: Version of the OBJECTTYPE for data in the object record data: type: object additionalProperties: {} @@ -749,6 +749,7 @@ components: type: string format: date readOnly: true + nullable: true description: Legal end date of the object record registrationAt: type: string @@ -756,13 +757,17 @@ components: readOnly: true description: The date when the record was registered in the system correctionFor: - type: string - readOnly: true + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record corrected by the current record - correctedBy: - type: string readOnly: true + correctedBy: + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record, which corrects the current record + readOnly: true required: - startAt - typeVersion @@ -787,6 +792,9 @@ components: - read_only - read_and_write type: string + description: |- + * `read_only` - Read-only + * `read_and_write` - Read and write MultiLineString: type: object description: GeoJSON multi-line-string geometry @@ -879,8 +887,8 @@ components: typeVersion: type: integer maximum: 32767 - description: Version of the OBJECTTYPE for data in the object record minimum: 0 + description: Version of the OBJECTTYPE for data in the object record data: type: object additionalProperties: {} @@ -901,6 +909,7 @@ components: type: string format: date readOnly: true + nullable: true description: Legal end date of the object record registrationAt: type: string @@ -908,12 +917,16 @@ components: readOnly: true description: The date when the record was registered in the system correctionFor: - type: string + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record corrected by the current record correctedBy: - type: string - readOnly: true + type: integer + maximum: 2147483647 + minimum: 0 description: Index of the record, which corrects the current record + readOnly: true required: - startAt - typeVersion @@ -1022,7 +1035,11 @@ components: mode: allOf: - $ref: '#/components/schemas/ModeEnum' - description: Permission mode + description: |- + Permission mode + + * `read_only` - Read-only + * `read_and_write` - Read and write use_fields: type: boolean description: Use field-based authorization diff --git a/src/objects/celery.py b/src/objects/celery.py new file mode 100644 index 00000000..f5c5e0e0 --- /dev/null +++ b/src/objects/celery.py @@ -0,0 +1,9 @@ +from celery import Celery + +from .setup import setup_env + +setup_env() + +app = Celery("objects") +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index d77e8afb..db8e498c 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -79,14 +79,15 @@ "solo", "django_markup", "vng_api_common", - "vng_api_common.notifications", + "notifications_api_common", "simple_certmanager", "zgw_consumers", - # 2fa apps + # Two-factor authentication in the Django admin, enforced. "django_otp", "django_otp.plugins.otp_static", "django_otp.plugins.otp_totp", "two_factor", + "maykin_2fa", # Project applications. "objects.accounts", "objects.api", @@ -102,11 +103,11 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "maykin_2fa.middleware.OTPMiddleware", "mozilla_django_oidc_db.middleware.SessionRefresh", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "axes.middleware.AxesMiddleware", - "django_otp.middleware.OTPMiddleware", ] ROOT_URLCONF = "objects.urls" @@ -171,8 +172,6 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True USE_THOUSAND_SEPARATOR = True @@ -331,7 +330,7 @@ "objects.utils.admin_index.should_display_dropdown_menu" ) -# Django-Axes (4.0+) +# Django-Axes # # The number of login attempts allowed before a record is created for the # failed logins. Default: 3 @@ -340,15 +339,10 @@ # will be forgotten. Can be set to a python timedelta object or an integer. If # an integer, will be interpreted as a number of hours. Default: None AXES_COOLOFF_TIME = 1 -# If True only locks based on user id and never locks by IP if attempts limit -# exceed, otherwise utilize the existing IP and user locking logic Default: -# False -AXES_ONLY_USER_FAILURES = True # If set, specifies a template to render when a user is locked out. Template # receives cooloff_time and failure_limit as context variables. Default: None AXES_LOCKOUT_TEMPLATE = "account_blocked.html" -AXES_USE_USER_AGENT = True # Default: False -AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True # Default: False +AXES_LOCKOUT_PARAMETERS = [["ip_address", "user_agent", "username"]] # The default meta precedence order IPWARE_META_PRECEDENCE_ORDER = ( @@ -426,10 +420,21 @@ NOTIFICATIONS_DISABLED = config("NOTIFICATIONS_DISABLED", False) # -# Maykin fork of DJANGO-TWO-FACTOR-AUTH +# MAYKIN-2FA +# Uses django-two-factor-auth under the hood, so relevant upstream package settings +# apply too. # -TWO_FACTOR_FORCE_OTP_ADMIN = config("TWO_FACTOR_FORCE_OTP_ADMIN", not DEBUG) -TWO_FACTOR_PATCH_ADMIN = config("TWO_FACTOR_PATCH_ADMIN", True) + +# we run the admin site monkeypatch instead. +TWO_FACTOR_PATCH_ADMIN = False +# add entries from AUTHENTICATION_BACKENDS that already enforce their own two-factor +# auth, avoiding having some set up MFA again in the project. +MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = [ + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", +] + +if config("DISABLE_2FA", default=False): # pragma: no cover + MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS # # Mozilla Django OIDC DB settings @@ -437,3 +442,15 @@ OIDC_AUTHENTICATE_CLASS = "mozilla_django_oidc_db.views.OIDCAuthenticationRequestView" MOZILLA_DJANGO_OIDC_DB_CACHE = "oidc" MOZILLA_DJANGO_OIDC_DB_CACHE_TIMEOUT = 5 * 60 + +# +# CELERY - async task queue +# +CELERY_BROKER_URL = config("CELERY_BROKER_URL", "redis://localhost:6379/0") +CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", "redis://localhost:6379/0") + +# Add (by default) 5 (soft), 15 (hard) minute timeouts to all Celery tasks. +CELERY_TASK_TIME_LIMIT = config("CELERY_TASK_HARD_TIME_LIMIT", default=15 * 60) # hard +CELERY_TASK_SOFT_TIME_LIMIT = config( + "CELERY_TASK_SOFT_TIME_LIMIT", default=5 * 60 +) # soft diff --git a/src/objects/conf/ci.py b/src/objects/conf/ci.py index 23946766..def866cd 100644 --- a/src/objects/conf/ci.py +++ b/src/objects/conf/ci.py @@ -29,9 +29,3 @@ AXES_BEHIND_REVERSE_PROXY = False NOTIFICATIONS_DISABLED = True - - -# -# Maykin fork of django-two-factor-auth -# -TWO_FACTOR_FORCE_OTP_ADMIN = False diff --git a/src/objects/conf/dev.py b/src/objects/conf/dev.py index f06e07ec..0c77b1b7 100644 --- a/src/objects/conf/dev.py +++ b/src/objects/conf/dev.py @@ -102,8 +102,10 @@ if "test" in sys.argv: NOTIFICATIONS_DISABLED = True - TWO_FACTOR_PATCH_ADMIN = False - TWO_FACTOR_FORCE_OTP_ADMIN = False + +# None of the authentication backends require two-factor authentication. +if config("DISABLE_2FA", default=True): # pragma: no cover + MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS # Override settings with local settings. try: diff --git a/src/objects/fixtures/default_admin_index.json b/src/objects/fixtures/default_admin_index.json index b11e8500..48aa5b94 100644 --- a/src/objects/fixtures/default_admin_index.json +++ b/src/objects/fixtures/default_admin_index.json @@ -29,7 +29,7 @@ "appgroup" ], [ - "notifications", + "notifications_api_common", "notificationsconfig" ], [ diff --git a/src/objects/templates/admin/base_site.html b/src/objects/templates/admin/base_site.html index 6e1a9e1f..1352afce 100644 --- a/src/objects/templates/admin/base_site.html +++ b/src/objects/templates/admin/base_site.html @@ -23,9 +23,9 @@

{{ settings.PROJECT_NAME }} {% if site_url %} {{ settings.SITE_TITLE }} / {% endif %} - {% url 'admin:two_factor:profile' as 2fa_profile_url %} - {% if 2fa_profile_url %} - {% trans "View 2fa profile" %} / + {% url 'maykin_2fa:account_security' as 2fa_account_security_url %} + {% if 2fa_account_security_url %} + {% trans "Account security" %} / {% endif %} {% if user.has_usable_password %} {% trans 'Change password' %} / diff --git a/src/objects/templates/admin/login.html b/src/objects/templates/admin/login.html deleted file mode 100644 index d408af04..00000000 --- a/src/objects/templates/admin/login.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "two_factor/admin/login.html" %} -{% load solo_tags i18n %} - - -{% block content %} -{{ block.super }} - -{% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} -{% if oidc_config.enabled %} -
{% trans "or" %}
-
- {% trans "Login with organization account" %} -
-{% endif %} - -{% endblock %} diff --git a/src/objects/templates/maykin_2fa/base.html b/src/objects/templates/maykin_2fa/base.html new file mode 100644 index 00000000..68fa4301 --- /dev/null +++ b/src/objects/templates/maykin_2fa/base.html @@ -0,0 +1,9 @@ +{% extends "maykin_2fa/base.html" %} + +{# Django 3.2 #} +{% block breadcrumbs %}{% endblock %} + +{# Do not show any version information #} +{% block footer %} + +{% endblock %} diff --git a/src/objects/templates/maykin_2fa/login.html b/src/objects/templates/maykin_2fa/login.html new file mode 100644 index 00000000..51987a80 --- /dev/null +++ b/src/objects/templates/maykin_2fa/login.html @@ -0,0 +1,23 @@ +{% extends "maykin_2fa/login.html" %} +{% load solo_tags i18n %} + +{% block extra_login_options %} + {% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} + {% if oidc_config.enabled %} +
{% trans "or" %}
+
+ {% trans "Login with organization account" %} +
+ {% endif %} +{% endblock %} + +{% block extra_recovery_options %} +
  • + {% trans 'Contact support to start the account recovery process' %} +
  • +{% endblock extra_recovery_options %} + +{# Do not show any version information #} +{% block footer %} + +{% endblock %} diff --git a/src/objects/templates/two_factor/admin/login.html b/src/objects/templates/two_factor/admin/login.html deleted file mode 100644 index afdff9b3..00000000 --- a/src/objects/templates/two_factor/admin/login.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "admin/login.html" %} diff --git a/src/objects/tests/admin/test_token_permissions.py b/src/objects/tests/admin/test_token_permissions.py index a9195275..40eb9d59 100644 --- a/src/objects/tests/admin/test_token_permissions.py +++ b/src/objects/tests/admin/test_token_permissions.py @@ -1,6 +1,7 @@ from django.urls import reverse_lazy from django_webtest import WebTest +from maykin_2fa.test import disable_admin_mfa from requests_mock import Mocker from objects.accounts.tests.factories import UserFactory @@ -11,6 +12,7 @@ OBJECT_TYPES_API = "https://example.com/objecttypes/v1/" +@disable_admin_mfa() class AddPermissionTests(WebTest): url = reverse_lazy("admin:token_permission_add") diff --git a/src/objects/tests/v1/test_notifications_send.py b/src/objects/tests/v1/test_notifications_send.py index 75d4884f..43621815 100644 --- a/src/objects/tests/v1/test_notifications_send.py +++ b/src/objects/tests/v1/test_notifications_send.py @@ -4,9 +4,9 @@ import requests_mock from freezegun import freeze_time +from notifications_api_common.models import NotificationsConfig from rest_framework import status from rest_framework.test import APITestCase -from vng_api_common.notifications.models import NotificationsConfig from zgw_consumers.constants import APITypes from zgw_consumers.models import Service @@ -20,12 +20,7 @@ from objects.utils.test import TokenAuthMixin from ..constants import GEO_WRITE_KWARGS -from ..utils import ( - mock_objecttype, - mock_objecttype_version, - mock_service_oas_get, - notifications_client_mock, -) +from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get from .utils import reverse OBJECT_TYPES_API = "https://example.com/objecttypes/v1/" @@ -49,9 +44,8 @@ def setUpTestData(cls): def setUp(self): super().setUp() - config = NotificationsConfig.get_solo() - Service.objects.update_or_create( - api_root=config.api_root, + service, _ = Service.objects.update_or_create( + api_root="https://notificaties-api.vng.cloud/api/v1/", defaults=dict( api_type=APITypes.nrc, client_id="test", @@ -60,13 +54,15 @@ def setUp(self): user_representation="Test", ), ) + config = NotificationsConfig.get_solo() + config.notifications_api_service = service + config.save() - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_create_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_create_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -95,8 +91,7 @@ def test_send_notif_create_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -110,12 +105,11 @@ def test_send_notif_create_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_update_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_update_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -147,8 +141,7 @@ def test_send_notif_update_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -162,12 +155,11 @@ def test_send_notif_update_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_partial_update_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_partial_update_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -199,8 +191,7 @@ def test_send_notif_partial_update_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -214,12 +205,11 @@ def test_send_notif_partial_update_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_delete_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_delete_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -238,8 +228,7 @@ def test_send_notif_delete_object(self, mocker, mock_client): response.status_code, status.HTTP_204_NO_CONTENT, response.data ) - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": full_url, diff --git a/src/objects/tests/v1/test_schema.py b/src/objects/tests/v1/test_schema.py new file mode 100644 index 00000000..e0b07335 --- /dev/null +++ b/src/objects/tests/v1/test_schema.py @@ -0,0 +1,10 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from .utils import reverse + + +class APISchemaTest(APITestCase): + def test_schema_endoint(self): + response = self.client.get(reverse("schema-redoc")) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/objects/tests/v2/test_notifications_kanaal.py b/src/objects/tests/v2/test_notifications_kanaal.py index 87764024..4b5937f4 100644 --- a/src/objects/tests/v2/test_notifications_kanaal.py +++ b/src/objects/tests/v2/test_notifications_kanaal.py @@ -5,8 +5,11 @@ from django.core.management import call_command from django.test import override_settings +from notifications_api_common.kanalen import KANAAL_REGISTRY, Kanaal +from notifications_api_common.models import NotificationsConfig from rest_framework.test import APITestCase -from vng_api_common.notifications.kanalen import Kanaal +from zgw_consumers.constants import APITypes +from zgw_consumers.models import Service from objects.core.models import Object @@ -20,23 +23,35 @@ def setUpTestData(cls): site.domain = "example.com" site.save() - @patch( - "vng_api_common.notifications.management.commands.register_kanaal.get_client" - ) + kanaal = Kanaal(label="kanaal_test", main_resource=Object) + cls.addClassCleanup(lambda: KANAAL_REGISTRY.remove(kanaal)) + + service, _ = Service.objects.update_or_create( + api_root="https://notificaties-api.vng.cloud/api/v1/", + defaults=dict( + api_type=APITypes.nrc, + client_id="test", + secret="test", + user_id="test", + user_representation="Test", + ), + ) + config = NotificationsConfig.get_solo() + config.notifications_api_service = service + config.save() + + @patch("notifications_api_common.models.NotificationsConfig.get_client") def test_kanaal_create_with_name(self, mock_get_client): """ Test is request to create kanaal is send with specified kanaal name """ client = mock_get_client.return_value client.list.return_value = [] - # ensure this is added to the registry - Kanaal(label="kanaal_test", main_resource=Object) stdout = StringIO() call_command( - "register_kanaal", - "kanaal_test", - notificaties_api_root="https://example.com/api/v1", + "register_kanalen", + kanalen=["kanaal_test"], stdout=stdout, ) @@ -49,9 +64,7 @@ def test_kanaal_create_with_name(self, mock_get_client): }, ) - @patch( - "vng_api_common.notifications.management.commands.register_kanaal.get_client" - ) + @patch("notifications_api_common.models.NotificationsConfig.get_client") @override_settings(NOTIFICATIONS_KANAAL="dummy-kanaal") def test_kanaal_create_without_name(self, mock_get_client): """ @@ -59,21 +72,18 @@ def test_kanaal_create_without_name(self, mock_get_client): """ client = mock_get_client.return_value client.list.return_value = [] - # ensure this is added to the registry - Kanaal(label="dummy-kanaal", main_resource=Object) stdout = StringIO() call_command( - "register_kanaal", - notificaties_api_root="https://example.com/api/v1", + "register_kanalen", stdout=stdout, ) client.create.assert_called_once_with( "kanaal", { - "naam": "dummy-kanaal", - "documentatieLink": "https://example.com/ref/kanalen/#dummy-kanaal", + "naam": "kanaal_test", + "documentatieLink": "https://example.com/ref/kanalen/#kanaal_test", "filters": [], }, ) diff --git a/src/objects/tests/v2/test_notifications_send.py b/src/objects/tests/v2/test_notifications_send.py index 75d4884f..43621815 100644 --- a/src/objects/tests/v2/test_notifications_send.py +++ b/src/objects/tests/v2/test_notifications_send.py @@ -4,9 +4,9 @@ import requests_mock from freezegun import freeze_time +from notifications_api_common.models import NotificationsConfig from rest_framework import status from rest_framework.test import APITestCase -from vng_api_common.notifications.models import NotificationsConfig from zgw_consumers.constants import APITypes from zgw_consumers.models import Service @@ -20,12 +20,7 @@ from objects.utils.test import TokenAuthMixin from ..constants import GEO_WRITE_KWARGS -from ..utils import ( - mock_objecttype, - mock_objecttype_version, - mock_service_oas_get, - notifications_client_mock, -) +from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get from .utils import reverse OBJECT_TYPES_API = "https://example.com/objecttypes/v1/" @@ -49,9 +44,8 @@ def setUpTestData(cls): def setUp(self): super().setUp() - config = NotificationsConfig.get_solo() - Service.objects.update_or_create( - api_root=config.api_root, + service, _ = Service.objects.update_or_create( + api_root="https://notificaties-api.vng.cloud/api/v1/", defaults=dict( api_type=APITypes.nrc, client_id="test", @@ -60,13 +54,15 @@ def setUp(self): user_representation="Test", ), ) + config = NotificationsConfig.get_solo() + config.notifications_api_service = service + config.save() - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_create_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_create_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -95,8 +91,7 @@ def test_send_notif_create_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -110,12 +105,11 @@ def test_send_notif_create_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_update_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_update_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -147,8 +141,7 @@ def test_send_notif_update_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -162,12 +155,11 @@ def test_send_notif_update_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_partial_update_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_partial_update_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -199,8 +191,7 @@ def test_send_notif_partial_update_object(self, mocker, mock_client): data = response.json() - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": data["url"], @@ -214,12 +205,11 @@ def test_send_notif_partial_update_object(self, mocker, mock_client): }, ) - @patch("zds_client.Client.from_url", side_effect=notifications_client_mock) - def test_send_notif_delete_object(self, mocker, mock_client): + @patch("notifications_api_common.viewsets.send_notification.delay") + def test_send_notif_delete_object(self, mocker, mock_task): """ Check if notifications will be send when Object is created """ - client = mock_client.return_value mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes") mocker.get( f"{self.object_type.url}/versions/1", @@ -238,8 +228,7 @@ def test_send_notif_delete_object(self, mocker, mock_client): response.status_code, status.HTTP_204_NO_CONTENT, response.data ) - client.create.assert_called_once_with( - "notificaties", + mock_task.assert_called_once_with( { "kanaal": "objecten", "hoofdObject": full_url, diff --git a/src/objects/tests/v2/test_schema.py b/src/objects/tests/v2/test_schema.py new file mode 100644 index 00000000..e0b07335 --- /dev/null +++ b/src/objects/tests/v2/test_schema.py @@ -0,0 +1,10 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from .utils import reverse + + +class APISchemaTest(APITestCase): + def test_schema_endoint(self): + response = self.client.get(reverse("schema-redoc")) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/objects/urls.py b/src/objects/urls.py index 4413dcb8..93bd2f34 100644 --- a/src/objects/urls.py +++ b/src/objects/urls.py @@ -7,6 +7,8 @@ from django.urls import include, path from django.views.generic.base import TemplateView +from maykin_2fa import monkeypatch_admin +from maykin_2fa.urls import urlpatterns as maykin_2fa_urlpatterns from rest_framework.settings import api_settings handler500 = "objects.utils.views.server_error" @@ -14,6 +16,8 @@ admin.site.site_title = "objects admin" admin.site.index_title = "Welcome to the objects admin" +monkeypatch_admin() + urlpatterns = [ path( "admin/password_reset/", @@ -25,6 +29,7 @@ auth_views.PasswordResetDoneView.as_view(), name="password_reset_done", ), + path("admin/", include((maykin_2fa_urlpatterns, "maykin_2fa"))), path("admin/", admin.site.urls), path( "reset///", @@ -45,7 +50,7 @@ ), ), path("ref/", include("vng_api_common.urls")), - path("ref/", include("vng_api_common.notifications.urls")), + path("ref/", include("notifications_api_common.urls")), path("oidc/", include("mozilla_django_oidc.urls")), path("api/", include("objects.api.urls")), ] diff --git a/src/objects/utils/autoschema.py b/src/objects/utils/autoschema.py index bd12b493..a20d29ec 100644 --- a/src/objects/utils/autoschema.py +++ b/src/objects/utils/autoschema.py @@ -40,9 +40,13 @@ def _get_filter_parameters(self): return [] return super()._get_filter_parameters() - def _get_response_for_code(self, serializer, status_code, media_types=None): + def _get_response_for_code( + self, serializer, status_code, media_types=None, direction="response" + ): """add default description to the response""" - response = super()._get_response_for_code(serializer, status_code, media_types) + response = super()._get_response_for_code( + serializer, status_code, media_types, direction + ) if not response.get("description"): response["description"] = HTTP_STATUS_CODE_TITLES.get(int(status_code)) @@ -149,9 +153,9 @@ def get_fields_params(self) -> list[OpenApiParameter]: return [] - def _get_request_body(self): + def _get_request_body(self, direction="request"): """update search request body with filter parameters""" - request_body = super()._get_request_body() + request_body = super()._get_request_body(direction) if self.view.action == "search": filter_params = self.get_filter_params_for_search()