diff --git a/server/config/settings/base.py b/server/config/settings/base.py index ee33a0b2..af6b2e95 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -631,6 +631,12 @@ OAUTH_SIGNIN_REDIRECT_URI = env( "OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback" ) +OAUTH_SIGNIN_URL = env("OAUTH_SIGNIN_URL", default="") +OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv") +OAUTH_SIGNIN_ADMIN_CLIENT_ID = env("OAUTH_SIGNIN_ADMIN_CLIENT_ID", default="") +OAUTH_SIGNIN_ADMIN_CLIENT_SECRET = env("OAUTH_SIGNIN_ADMIN_CLIENT_SECRET", default="") +OAUTH_SYNC_ROLES = env.bool("OAUTH_SYNC_ROLES", default=False) + GRAPHENE = { "SCHEMA": "vbv_lernwelt.core.schema.schema", "SCHEMA_OUTPUT": "../client/src/gql/schema.graphql", diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt index 7312ba40..2e726be0 100644 --- a/server/requirements/requirements-dev.txt +++ b/server/requirements/requirements-dev.txt @@ -2,79 +2,84 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# pip-compile requirements-dev.in # aniso8601==9.0.1 # via graphene anyascii==0.3.2 # via wagtail -anyio==3.7.1 - # via watchfiles -appnope==0.1.3 - # via ipython -argon2-cffi==21.3.0 +anyio==4.4.0 + # via + # httpx + # watchfiles +argon2-cffi==23.1.0 # via -r requirements.in argon2-cffi-bindings==21.2.0 # via argon2-cffi -asgiref==3.7.2 - # via django -astroid==2.15.6 +asgiref==3.8.1 + # via + # django + # django-cors-headers + # django-stubs +astroid==3.2.2 # via pylint -asttokens==2.2.1 +asttokens==2.4.1 # via stack-data -async-timeout==4.0.2 +async-property==0.2.2 + # via python-keycloak +async-timeout==4.0.3 # via redis -attrs==23.1.0 +attrs==23.2.0 # via # jsonschema # referencing # usort -authlib==1.2.1 +authlib==1.3.1 # via -r requirements.in -azure-core==1.29.1 +azure-core==1.30.2 # via # azure-identity # azure-storage-blob -azure-identity==1.14.0 +azure-identity==1.17.0 # via -r requirements.in -azure-storage-blob==12.17.0 +azure-storage-blob==12.20.0 # via -r requirements.in -backcall==0.2.0 - # via ipython -bcrypt==4.0.1 +bcrypt==4.1.3 # via paramiko beautifulsoup4==4.11.2 # via wagtail -black==23.7.0 +black==24.4.2 # via # -r requirements-dev.in # ufmt -boto3==1.28.23 +boto3==1.34.129 # via -r requirements.in -botocore==1.31.23 +botocore==1.34.129 # via # boto3 # s3transfer -brotli==1.0.9 +brotli==1.1.0 # via whitenoise -build==0.10.0 +build==1.2.1 # via pip-tools caprover-api @ git+https://github.com/iterativ/Caprover-API.git@5013f8fc929e8e3281b9d609e968a782e8e99530 # via -r requirements-dev.in -certifi==2023.7.22 +certifi==2024.6.2 # via + # httpcore + # httpx # requests # sentry-sdk -cffi==1.15.1 +cffi==1.16.0 # via # argon2-cffi-bindings # cryptography # pynacl -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -charset-normalizer==3.2.0 +charset-normalizer==3.3.2 # via requests -click==8.1.6 +click==8.1.7 # via # -r requirements.in # black @@ -84,17 +89,18 @@ click==8.1.6 # ufmt # usort # uvicorn -concurrent-log-handler==0.9.24 +concurrent-log-handler==0.9.25 # via -r requirements.in -coverage==7.2.7 +coverage==7.5.3 # via # -r requirements-dev.in # django-coverage-plugin -cryptography==41.0.3 +cryptography==42.0.8 # via # authlib # azure-identity # azure-storage-blob + # jwcrypto # msal # paramiko # pyjwt @@ -104,13 +110,15 @@ decorator==5.1.1 # ipython defusedxml==0.7.1 # via willow -dill==0.3.7 +deprecation==2.1.0 + # via python-keycloak +dill==0.3.8 # via pylint -distlib==0.3.7 +distlib==0.3.8 # via virtualenv -dj-database-url==2.0.0 +dj-database-url==2.2.0 # via -r requirements.in -django==3.2.20 +django==3.2.25 # via # -r requirements.in # dj-database-url @@ -137,96 +145,99 @@ django==3.2.20 # jsonfield # wagtail # wagtail-localize -django-click==2.3.0 +django-click==2.4.0 # via -r requirements.in django-constance==3.1.0 # via -r requirements.in -django-cors-headers==4.2.0 +django-cors-headers==4.3.1 # via -r requirements.in django-coverage-plugin==3.1.0 # via -r requirements-dev.in -django-csp==3.7 +django-csp==3.8 # via -r requirements.in -django-debug-toolbar==4.1.0 +django-debug-toolbar==4.3.0 # via -r requirements-dev.in django-extensions==3.2.3 # via -r requirements-dev.in -django-filter==23.2 +django-filter==23.5 # via wagtail -django-ipware==5.0.0 +django-ipware==7.0.1 # via -r requirements.in -django-jsonform==2.18.0 +django-jsonform==2.22.0 # via -r requirements.in -django-model-utils==4.3.1 +django-model-utils==4.5.1 # via # -r requirements.in # django-notifications-hq -django-modelcluster==6.0 +django-modelcluster==6.3 # via wagtail -django-notifications-hq==1.8.2 +django-notifications-hq==1.8.3 # via -r requirements.in django-permissionedforms==0.1 # via wagtail -django-picklefield==3.1 +django-picklefield==3.2 # via django-constance django-ratelimit==4.1.0 # via -r requirements.in -django-redis==5.3.0 +django-redis==5.4.0 # via -r requirements.in -django-storages==1.13.2 +django-storages==1.14.3 # via -r requirements.in -django-stubs==4.2.3 +django-stubs==5.0.2 # via # -r requirements-dev.in # djangorestframework-stubs -django-stubs-ext==4.2.2 +django-stubs-ext==5.0.2 # via django-stubs django-taggit==4.0.0 # via wagtail -django-treebeard==4.7 +django-treebeard==4.7.1 # via wagtail -djangorestframework==3.14.0 +django-watchfiles @ https://github.com/q0w/django-watchfiles/archive/issue-1.zip + # via -r requirements-dev.in +djangorestframework==3.15.1 # via # -r requirements.in # drf-spectacular # wagtail -djangorestframework-stubs==3.14.2 +djangorestframework-stubs==3.15.0 # via -r requirements-dev.in draftjs-exporter==2.1.7 # via wagtail -drf-spectacular==0.26.4 +drf-spectacular==0.27.2 # via -r requirements.in -environs==9.5.0 +environs==11.0.0 # via -r requirements.in et-xmlfile==1.1.0 # via openpyxl -exceptiongroup==1.1.2 +exceptiongroup==1.2.1 # via # anyio + # ipython # pytest -execnet==2.0.2 +execnet==2.1.1 # via pytest-xdist -executing==1.2.0 +executing==2.0.1 # via stack-data factory-boy==3.3.0 # via # -r requirements-dev.in # wagtail-factories -faker==19.3.0 +faker==25.8.0 # via factory-boy -filelock==3.12.2 +filelock==3.15.1 # via virtualenv filetype==1.2.0 # via willow -flake8==6.1.0 +flake8==7.1.0 # via # -r requirements-dev.in # flake8-isort -flake8-isort==6.0.0 +flake8-isort==6.1.1 # via -r requirements-dev.in -freezegun==1.2.2 +freezegun==1.5.1 # via -r requirements-dev.in -gitdb==4.0.10 +gitdb==4.0.11 # via gitdb2 gitdb2==4.0.2 # via gitpython @@ -234,7 +245,7 @@ gitpython==3.0.6 # via trufflehog graphene==3.3 # via graphene-django -graphene-django==3.1.5 +graphene-django==3.2.2 # via wagtail-grapple graphql-core==3.2.3 # via @@ -245,19 +256,26 @@ graphql-relay==3.2.0 # via # graphene # graphene-django -gunicorn==21.2.0 +gunicorn==22.0.0 # via -r requirements.in h11==0.14.0 - # via uvicorn + # via + # httpcore + # uvicorn html5lib==1.1 # via wagtail -httptools==0.6.0 +httpcore==1.0.5 + # via httpx +httptools==0.6.1 # via uvicorn -identify==2.5.26 +httpx==0.27.0 + # via python-keycloak +identify==2.5.36 # via pre-commit -idna==3.4 +idna==3.7 # via # anyio + # httpx # requests inflection==0.5.1 # via drf-spectacular @@ -265,15 +283,15 @@ iniconfig==2.0.0 # via pytest ipdb==0.13.13 # via -r requirements-dev.in -ipython==8.14.0 +ipython==8.25.0 # via ipdb isodate==0.6.1 # via azure-storage-blob -isort==5.12.0 +isort==5.13.2 # via # flake8-isort # pylint -jedi==0.19.0 +jedi==0.19.1 # via ipython jmespath==1.0.1 # via @@ -281,21 +299,21 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via django-notifications-hq -jsonschema==4.19.0 +jsonschema==4.22.0 # via drf-spectacular -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.12.1 # via jsonschema +jwcrypto==1.5.6 + # via python-keycloak l18n==2021.3 # via wagtail -lazy-object-proxy==1.9.0 - # via astroid -libcst==1.0.1 +libcst==1.4.0 # via # ufmt # usort -marshmallow==3.20.1 +marshmallow==3.21.3 # via environs -matplotlib-inline==0.1.6 +matplotlib-inline==0.1.7 # via ipython mccabe==0.7.0 # via @@ -305,148 +323,166 @@ moreorless==0.4.0 # via # ufmt # usort -msal==1.23.0 +msal==1.28.1 # via # azure-identity # msal-extensions -msal-extensions==1.0.0 +msal-extensions==1.1.0 # via azure-identity -mypy==1.4.1 - # via - # -r requirements-dev.in - # django-stubs - # djangorestframework-stubs +mypy==1.10.0 + # via -r requirements-dev.in mypy-extensions==1.0.0 # via # black # mypy - # typing-inspect -newrelic==8.11.0 +newrelic==9.11.0 # via -r requirements.in -nodeenv==1.8.0 +nodeenv==1.9.1 # via pre-commit -openpyxl==3.1.2 +openpyxl==3.1.4 # via # -r requirements.in # wagtail -packaging==23.1 +packaging==24.1 # via # black # build + # deprecation # gunicorn # marshmallow + # msal-extensions # pytest # pytest-sugar +<<<<<<< HEAD paramiko==3.3.1 # via # -r requirements.in # sftpserver parso==0.8.3 +======= +paramiko==3.4.0 + # via -r requirements.in +parso==0.8.4 +>>>>>>> 9e6b9a1e (wip: Add KC-client and basic methods, signal handler) # via jedi -pathspec==0.11.2 +pathspec==0.12.1 # via # black # trailrunner -pexpect==4.8.0 +pexpect==4.9.0 # via ipython -pickleshare==0.7.5 - # via ipython -pillow==10.0.0 +pillow==10.3.0 # via # -r requirements.in # pillow-heif # wagtail -pillow-heif==0.13.0 +pillow-heif==0.16.0 # via willow -pip-tools==7.3.0 +pip-tools==7.4.1 # via -r requirements-dev.in -platformdirs==3.10.0 +platformdirs==4.2.2 # via # black # pylint # virtualenv -pluggy==1.2.0 +pluggy==1.5.0 # via pytest polib==1.2.0 # via wagtail-localize -portalocker==2.7.0 +portalocker==2.8.2 # via # concurrent-log-handler # msal-extensions -pre-commit==3.3.3 +pre-commit==3.7.1 # via -r requirements-dev.in promise==2.3 # via graphene-django -prompt-toolkit==3.0.39 +prompt-toolkit==3.0.47 # via ipython -psycopg2-binary==2.9.7 +psycopg2-binary==2.9.9 # via -r requirements.in ptyprocess==0.7.0 # via pexpect pure-eval==0.2.2 # via stack-data -pycodestyle==2.11.0 +pycodestyle==2.12.0 # via flake8 -pycparser==2.21 +pycparser==2.22 # via cffi -pycryptodome==3.18.0 +pycryptodome==3.20.0 # via -r requirements.in -pyflakes==3.1.0 +pyflakes==3.2.0 # via flake8 -pygments==2.16.1 +pygments==2.18.0 # via ipython pyjwt[crypto]==2.8.0 +<<<<<<< HEAD # via msal pylint==2.17.5 +======= + # via + # msal + # pyjwt +pylint==3.2.3 +>>>>>>> 9e6b9a1e (wip: Add KC-client and basic methods, signal handler) # via # pylint-django # pylint-plugin-utils -pylint-django==2.5.3 +pylint-django==2.5.5 # via -r requirements-dev.in pylint-plugin-utils==0.8.2 # via pylint-django pynacl==1.5.0 # via paramiko -pyproject-hooks==1.0.0 - # via build -pytest==7.4.0 +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +pytest==8.2.2 # via # -r requirements-dev.in # pytest-django # pytest-order # pytest-sugar # pytest-xdist -pytest-django==4.5.2 +pytest-django==4.8.0 # via -r requirements-dev.in +<<<<<<< HEAD pytest-order==1.2.1 # via -r requirements-dev.in pytest-sugar==0.9.7 +======= +pytest-sugar==1.0.0 +>>>>>>> 9e6b9a1e (wip: Add KC-client and basic methods, signal handler) # via -r requirements-dev.in -pytest-xdist==3.5.0 +pytest-xdist==3.6.1 # via -r requirements-dev.in -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # -r requirements.in # botocore # faker # freezegun -python-dotenv==1.0.0 +python-dotenv==1.0.1 # via # environs # uvicorn python-http-client==3.3.7 # via sendgrid +python-ipware==3.0.0 + # via django-ipware python-json-logger==2.0.7 # via -r requirements.in -python-slugify==8.0.1 +python-keycloak==4.1.0 # via -r requirements.in -pytz==2023.3 +python-slugify==8.0.4 + # via -r requirements.in +pytz==2024.1 # via # -r requirements.in # django # django-modelcluster # django-notifications-hq - # djangorestframework # l18n pyyaml==6.0.1 # via @@ -455,30 +491,34 @@ pyyaml==6.0.1 # libcst # pre-commit # uvicorn -redis==4.6.0 +redis==5.0.6 # via # -r requirements.in # django-redis -referencing==0.30.2 +referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.32.3 # via # azure-core # caprover-api # djangorestframework-stubs # msal + # python-keycloak + # requests-toolbelt # wagtail -rpds-py==0.9.2 +requests-toolbelt==1.0.0 + # via python-keycloak +rpds-py==0.18.1 # via # jsonschema # referencing -s3transfer==0.6.1 +s3transfer==0.10.1 # via boto3 -sendgrid==6.10.0 +sendgrid==6.11.0 # via -r requirements.in -sentry-sdk==1.29.2 +sentry-sdk==2.5.1 # via -r requirements.in sftpserver @ git+https://github.com/lonetwin/sftpserver.git@1d16896d3f0f90d63d1caaf4e199f2a9dde6456f # via -r requirements-dev.in @@ -491,29 +531,31 @@ six==1.16.0 # l18n # promise # python-dateutil -smmap==5.0.0 +smmap==5.0.1 # via gitdb -sniffio==1.3.0 - # via anyio -soupsieve==2.4.1 +sniffio==1.3.1 + # via + # anyio + # httpx +soupsieve==2.5 # via beautifulsoup4 -sqlparse==0.4.4 +sqlparse==0.5.0 # via # django # django-debug-toolbar -stack-data==0.6.2 +stack-data==0.6.3 # via ipython starkbank-ecdsa==2.2.0 # via sendgrid -stdlibs==2022.10.9 +stdlibs==2024.5.15 # via usort -structlog==23.1.0 +structlog==24.2.0 # via -r requirements.in swapper==1.3.0 # via django-notifications-hq telepath==0.3.1 # via wagtail -termcolor==2.3.0 +termcolor==2.4.0 # via pytest-sugar text-unidecode==1.3 # via @@ -530,9 +572,8 @@ tomli==2.0.1 # mypy # pip-tools # pylint - # pyproject-hooks # pytest -tomlkit==0.12.1 +tomlkit==0.12.5 # via # pylint # ufmt @@ -540,7 +581,7 @@ trailrunner==1.4.0 # via # ufmt # usort -traitlets==5.9.0 +traitlets==5.14.3 # via # ipython # matplotlib-inline @@ -548,82 +589,95 @@ trufflehog==2.2.1 # via -r requirements-dev.in trufflehogregexes==0.0.7 # via trufflehog -types-pytz==2023.3.0.0 - # via django-stubs -types-pyyaml==6.0.12.11 +types-pyyaml==6.0.12.20240311 # via # django-stubs # djangorestframework-stubs -types-requests==2.31.0.2 +types-requests==2.32.0.20240602 # via djangorestframework-stubs -types-urllib3==1.26.25.14 - # via types-requests -typing-extensions==4.7.1 +typing-extensions==4.12.2 # via + # anyio # asgiref # astroid # azure-core + # azure-identity # azure-storage-blob + # black # dj-database-url # django-stubs # django-stubs-ext # djangorestframework-stubs - # libcst + # ipython + # jwcrypto # mypy - # typing-inspect # ufmt # uvicorn # wagtail-localize -typing-inspect==0.9.0 - # via libcst -ufmt==2.2.0 +ufmt==2.7.0 # via -r requirements-dev.in uritemplate==4.1.1 # via drf-spectacular -urllib3==1.26.16 +urllib3==2.2.2 # via # botocore # requests # sentry-sdk -usort==1.0.7 + # types-requests +usort==1.0.8.post1 # via ufmt -uvicorn[standard]==0.23.2 +uvicorn[standard]==0.30.1 # via -r requirements.in -uvloop==0.17.0 +uvloop==0.19.0 # via uvicorn -virtualenv==20.24.2 +virtualenv==20.26.2 # via pre-commit -wagtail==5.1 +wagtail==5.2.5 # via # -r requirements.in # wagtail-factories # wagtail-grapple # wagtail-headless-preview # wagtail-localize -wagtail-factories==4.1.0 +wagtail-factories==4.2.1 # via -r requirements.in -wagtail-grapple==0.20.0 +wagtail-grapple==0.25.1 # via -r requirements.in -wagtail-headless-preview==0.6.0 +wagtail-headless-preview==0.8.0 # via wagtail-grapple -wagtail-localize==1.5.1 +wagtail-localize==1.9 # via -r requirements.in +<<<<<<< HEAD watchfiles==0.19.0 # via uvicorn wcwidth==0.2.6 +======= +watchfiles==0.22.0 + # via + # django-watchfiles + # uvicorn +wcwidth==0.2.13 +>>>>>>> 9e6b9a1e (wip: Add KC-client and basic methods, signal handler) # via prompt-toolkit webencodings==0.5.1 # via html5lib -websockets==11.0.3 +websockets==12.0 # via uvicorn -wheel==0.41.1 +wheel==0.43.0 # via pip-tools -whitenoise[brotli]==6.5.0 +whitenoise[brotli]==6.6.0 # via -r requirements.in +<<<<<<< HEAD willow[heif]==1.6.1 # via wagtail wrapt==1.15.0 # via astroid +======= +willow[heif]==1.6.3 + # via + # wagtail + # willow +>>>>>>> 9e6b9a1e (wip: Add KC-client and basic methods, signal handler) # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index 3b5ae91e..56427d54 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -52,3 +52,5 @@ azure-identity boto3 openpyxl newrelic +python-keycloak + diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index 1987798b..12595315 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -2,79 +2,90 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --output-file=requirements.txt requirements.in +# pip-compile requirements.in # aniso8601==9.0.1 # via graphene anyascii==0.3.2 # via wagtail -anyio==3.7.1 - # via watchfiles -argon2-cffi==21.3.0 +anyio==4.4.0 + # via + # httpx + # watchfiles +argon2-cffi==23.1.0 # via -r requirements.in argon2-cffi-bindings==21.2.0 # via argon2-cffi -asgiref==3.7.2 - # via django -async-timeout==4.0.2 +asgiref==3.8.1 + # via + # django + # django-cors-headers +async-property==0.2.2 + # via python-keycloak +async-timeout==4.0.3 # via redis -attrs==23.1.0 +attrs==23.2.0 # via # jsonschema # referencing -authlib==1.2.1 +authlib==1.3.1 # via -r requirements.in -azure-core==1.29.1 +azure-core==1.30.2 # via # azure-identity # azure-storage-blob -azure-identity==1.14.0 +azure-identity==1.17.0 # via -r requirements.in -azure-storage-blob==12.17.0 +azure-storage-blob==12.20.0 # via -r requirements.in -bcrypt==4.0.1 +bcrypt==4.1.3 # via paramiko beautifulsoup4==4.11.2 # via wagtail -boto3==1.28.23 +boto3==1.34.129 # via -r requirements.in -botocore==1.31.23 +botocore==1.34.129 # via # boto3 # s3transfer -brotli==1.0.9 +brotli==1.1.0 # via whitenoise -certifi==2023.7.22 +certifi==2024.6.2 # via + # httpcore + # httpx # requests # sentry-sdk -cffi==1.15.1 +cffi==1.16.0 # via # argon2-cffi-bindings # cryptography # pynacl -charset-normalizer==3.2.0 +charset-normalizer==3.3.2 # via requests -click==8.1.6 +click==8.1.7 # via # -r requirements.in # django-click # uvicorn -concurrent-log-handler==0.9.24 +concurrent-log-handler==0.9.25 # via -r requirements.in -cryptography==41.0.3 +cryptography==42.0.8 # via # authlib # azure-identity # azure-storage-blob + # jwcrypto # msal # paramiko # pyjwt defusedxml==0.7.1 # via willow -dj-database-url==2.0.0 +deprecation==2.1.0 + # via python-keycloak +dj-database-url==2.2.0 # via -r requirements.in -django==3.2.20 +django==3.2.25 # via # -r requirements.in # dj-database-url @@ -97,66 +108,66 @@ django==3.2.20 # jsonfield # wagtail # wagtail-localize -django-click==2.3.0 +django-click==2.4.0 # via -r requirements.in django-constance==3.1.0 # via -r requirements.in -django-cors-headers==4.2.0 +django-cors-headers==4.3.1 # via -r requirements.in -django-csp==3.7 +django-csp==3.8 # via -r requirements.in -django-filter==23.2 +django-filter==23.5 # via wagtail -django-ipware==5.0.0 +django-ipware==7.0.1 # via -r requirements.in -django-jsonform==2.18.0 +django-jsonform==2.22.0 # via -r requirements.in -django-model-utils==4.3.1 +django-model-utils==4.5.1 # via # -r requirements.in # django-notifications-hq -django-modelcluster==6.0 +django-modelcluster==6.3 # via wagtail -django-notifications-hq==1.8.2 +django-notifications-hq==1.8.3 # via -r requirements.in django-permissionedforms==0.1 # via wagtail -django-picklefield==3.1 +django-picklefield==3.2 # via django-constance django-ratelimit==4.1.0 # via -r requirements.in -django-redis==5.3.0 +django-redis==5.4.0 # via -r requirements.in -django-storages==1.13.2 +django-storages==1.14.3 # via -r requirements.in django-taggit==4.0.0 # via wagtail -django-treebeard==4.7 +django-treebeard==4.7.1 # via wagtail -djangorestframework==3.14.0 +djangorestframework==3.15.1 # via # -r requirements.in # drf-spectacular # wagtail draftjs-exporter==2.1.7 # via wagtail -drf-spectacular==0.26.4 +drf-spectacular==0.27.2 # via -r requirements.in -environs==9.5.0 +environs==11.0.0 # via -r requirements.in et-xmlfile==1.1.0 # via openpyxl -exceptiongroup==1.1.2 +exceptiongroup==1.2.1 # via anyio factory-boy==3.3.0 # via wagtail-factories -faker==19.3.0 +faker==25.8.0 # via factory-boy filetype==1.2.0 # via willow graphene==3.3 # via graphene-django -graphene-django==3.1.5 +graphene-django==3.2.2 # via wagtail-grapple graphql-core==3.2.3 # via @@ -167,17 +178,24 @@ graphql-relay==3.2.0 # via # graphene # graphene-django -gunicorn==21.2.0 +gunicorn==22.0.0 # via -r requirements.in h11==0.14.0 - # via uvicorn + # via + # httpcore + # uvicorn html5lib==1.1 # via wagtail -httptools==0.6.0 +httpcore==1.0.5 + # via httpx +httptools==0.6.1 # via uvicorn -idna==3.4 +httpx==0.27.0 + # via python-keycloak +idna==3.7 # via # anyio + # httpx # requests inflection==0.5.1 # via drf-spectacular @@ -189,106 +207,119 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via django-notifications-hq -jsonschema==4.19.0 +jsonschema==4.22.0 # via drf-spectacular -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.12.1 # via jsonschema +jwcrypto==1.5.6 + # via python-keycloak l18n==2021.3 # via wagtail -marshmallow==3.20.1 +marshmallow==3.21.3 # via environs -msal==1.23.0 +msal==1.28.1 # via # azure-identity # msal-extensions -msal-extensions==1.0.0 +msal-extensions==1.1.0 # via azure-identity -newrelic==8.11.0 +newrelic==9.11.0 # via -r requirements.in -openpyxl==3.1.2 +openpyxl==3.1.4 # via # -r requirements.in # wagtail -packaging==23.1 +packaging==24.1 # via + # deprecation # gunicorn # marshmallow -paramiko==3.3.1 + # msal-extensions +paramiko==3.4.0 # via -r requirements.in -pillow==10.0.0 +pillow==10.3.0 # via # -r requirements.in # pillow-heif # wagtail -pillow-heif==0.13.0 +pillow-heif==0.16.0 # via willow polib==1.2.0 # via wagtail-localize -portalocker==2.7.0 +portalocker==2.8.2 # via # concurrent-log-handler # msal-extensions promise==2.3 # via graphene-django -psycopg2-binary==2.9.7 +psycopg2-binary==2.9.9 # via -r requirements.in -pycparser==2.21 +pycparser==2.22 # via cffi -pycryptodome==3.18.0 +pycryptodome==3.20.0 # via -r requirements.in pyjwt[crypto]==2.8.0 - # via msal + # via + # msal + # pyjwt pynacl==1.5.0 # via paramiko -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # -r requirements.in # botocore # faker -python-dotenv==1.0.0 +python-dotenv==1.0.1 # via # environs # uvicorn python-http-client==3.3.7 # via sendgrid +python-ipware==3.0.0 + # via django-ipware python-json-logger==2.0.7 # via -r requirements.in -python-slugify==8.0.1 +python-keycloak==4.1.0 # via -r requirements.in -pytz==2023.3 +python-slugify==8.0.4 + # via -r requirements.in +pytz==2024.1 # via # -r requirements.in # django # django-modelcluster # django-notifications-hq - # djangorestframework # l18n pyyaml==6.0.1 # via # drf-spectacular # uvicorn -redis==4.6.0 +redis==5.0.6 # via # -r requirements.in # django-redis -referencing==0.30.2 +referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.32.3 # via # azure-core # msal + # python-keycloak + # requests-toolbelt # wagtail -rpds-py==0.9.2 +requests-toolbelt==1.0.0 + # via python-keycloak +rpds-py==0.18.1 # via # jsonschema # referencing -s3transfer==0.6.1 +s3transfer==0.10.1 # via boto3 -sendgrid==6.10.0 +sendgrid==6.11.0 # via -r requirements.in -sentry-sdk==1.29.2 +sentry-sdk==2.5.1 # via -r requirements.in six==1.16.0 # via @@ -298,15 +329,17 @@ six==1.16.0 # l18n # promise # python-dateutil -sniffio==1.3.0 - # via anyio -soupsieve==2.4.1 +sniffio==1.3.1 + # via + # anyio + # httpx +soupsieve==2.5 # via beautifulsoup4 -sqlparse==0.4.4 +sqlparse==0.5.0 # via django starkbank-ecdsa==2.2.0 # via sendgrid -structlog==23.1.0 +structlog==24.2.0 # via -r requirements.in swapper==1.3.0 # via django-notifications-hq @@ -316,47 +349,52 @@ text-unidecode==1.3 # via # graphene-django # python-slugify -typing-extensions==4.7.1 +typing-extensions==4.12.2 # via + # anyio # asgiref # azure-core + # azure-identity # azure-storage-blob # dj-database-url + # jwcrypto # uvicorn # wagtail-localize uritemplate==4.1.1 # via drf-spectacular -urllib3==1.26.16 +urllib3==2.2.2 # via # botocore # requests # sentry-sdk -uvicorn[standard]==0.23.2 +uvicorn[standard]==0.30.1 # via -r requirements.in -uvloop==0.17.0 +uvloop==0.19.0 # via uvicorn -wagtail==5.1 +wagtail==5.2.5 # via # -r requirements.in # wagtail-factories # wagtail-grapple # wagtail-headless-preview # wagtail-localize -wagtail-factories==4.1.0 +wagtail-factories==4.2.1 # via -r requirements.in -wagtail-grapple==0.20.0 +wagtail-grapple==0.25.1 # via -r requirements.in -wagtail-headless-preview==0.6.0 +wagtail-headless-preview==0.8.0 # via wagtail-grapple -wagtail-localize==1.5.1 +wagtail-localize==1.9 # via -r requirements.in -watchfiles==0.19.0 +watchfiles==0.22.0 # via uvicorn webencodings==0.5.1 # via html5lib -websockets==11.0.3 +websockets==12.0 # via uvicorn -whitenoise[brotli]==6.5.0 +whitenoise[brotli]==6.6.0 # via -r requirements.in -willow[heif]==1.6.1 - # via wagtail +willow[heif]==1.6.3 + # via + # wagtail + # willow diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index aa94f21b..d6d260c1 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -13,6 +13,11 @@ from vbv_lernwelt.core.model_utils import find_available_slug from vbv_lernwelt.core.models import User from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class from vbv_lernwelt.files.models import UploadFile +from vbv_lernwelt.sso.role_sync.client import ( + add_roles_to_user, + remove_roles_from_user, + update_roles_for_user, +) class CircleContactType(Enum): @@ -293,6 +298,26 @@ class CourseSessionUser(models.Model): def __str__(self): return f"{self.user} ({self.course_session.title})" + def save(self, *args, **kwargs): + if self.created_at is None: + add_roles_to_user( + self.user, [(self.course_session.course.slug, [self.role])] + ) + else: + old_csu = CourseSessionUser.objects.get(pk=self.pk) + update_roles_for_user( + self.user, + add_course_roles=[(self.course_session.course.slug, [self.role])], + remove_course_roles=[(self.course_session.course.slug, [old_csu.role])], + ) + super().save(*args, **kwargs) + + @classmethod + def remove_sso_roles_from_user(cls, instance: "CourseSessionUser"): + remove_roles_from_user( + instance.user, [(instance.course_session.course.slug, [instance.role])] + ) + class CircleDocument(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/server/vbv_lernwelt/course/signals.py b/server/vbv_lernwelt/course/signals.py index d474c30a..14bf93b2 100644 --- a/server/vbv_lernwelt/course/signals.py +++ b/server/vbv_lernwelt/course/signals.py @@ -1,10 +1,15 @@ -from django.db.models.signals import post_save +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver -from vbv_lernwelt.course.models import Course, CourseConfiguration +from vbv_lernwelt.course.models import Course, CourseConfiguration, CourseSessionUser @receiver(post_save, sender=Course) def create_course_configuration(sender, instance, created, **kwargs): if created: CourseConfiguration.objects.create(course=instance) + + +@receiver(post_delete, sender=CourseSessionUser) +def after_delete(sender, instance, **kwargs): + CourseSessionUser.remove_sso_roles_from_user(instance) diff --git a/server/vbv_lernwelt/course_session_group/admin.py b/server/vbv_lernwelt/course_session_group/admin.py index 5bda3780..f880cfa3 100644 --- a/server/vbv_lernwelt/course_session_group/admin.py +++ b/server/vbv_lernwelt/course_session_group/admin.py @@ -4,5 +4,4 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup @admin.register(CourseSessionGroup) -class CourseSessionAssignmentAdmin(admin.ModelAdmin): - ... +class CourseSessionAssignmentAdmin(admin.ModelAdmin): ... diff --git a/server/vbv_lernwelt/edoniq_test/views.py b/server/vbv_lernwelt/edoniq_test/views.py index 00d8ff7b..e3a71ec2 100644 --- a/server/vbv_lernwelt/edoniq_test/views.py +++ b/server/vbv_lernwelt/edoniq_test/views.py @@ -153,9 +153,9 @@ def fetch_course_session_all_users(courses: List[int], excluded_domains=None): def generate_export_response(cs_users: List[User]) -> HttpResponse: response = HttpResponse(content_type="text/csv; charset=utf-8") - response[ - "Content-Disposition" - ] = f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" + response["Content-Disposition"] = ( + f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" + ) response.write("\ufeff".encode("utf8")) # UTF-8 BOM diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index d0fefe20..51602502 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -125,9 +125,9 @@ def _handle_feedback_export_action(course_seesions, file_name): response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - response[ - "Content-Disposition" - ] = f"attachment; filename={make_export_filename(file_name)}" + response["Content-Disposition"] = ( + f"attachment; filename={make_export_filename(file_name)}" + ) response.write(excel_bytes) return response diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 1db6fea8..732b840b 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -493,6 +493,7 @@ def create_or_update_user( sso_id: str = None, contract_number: str = "", date_of_birth: str = "", + intermediate_sso_id: str = "", # from keycloak ) -> User: logger.debug( "create_or_update_user", @@ -537,6 +538,9 @@ def create_or_update_user( user.first_name = first_name or user.first_name user.last_name = last_name or user.last_name user.username = email + user.additional_json_data = user.additional_json_data | { + "intermediate_sso_id": intermediate_sso_id + } user.set_unusable_password() user.save() diff --git a/server/vbv_lernwelt/notify/tests/test_service.py b/server/vbv_lernwelt/notify/tests/test_service.py index 6d8af677..1feb4bad 100644 --- a/server/vbv_lernwelt/notify/tests/test_service.py +++ b/server/vbv_lernwelt/notify/tests/test_service.py @@ -65,9 +65,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) def test_send_notification_with_email(self): - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() verb = "Anne hat deinen Auftrag bewertet" @@ -146,9 +146,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) # when the email was not sent, yet it will still send it afterwards... - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() result = self.notification_service._send_notification( @@ -188,9 +188,9 @@ class TestNotificationService(TestCase): self.assertFalse(self._has_sent_emails()) # Assert mail is sent if corresponding email notification type is enabled - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() self.notification_service._send_notification( sender=self.sender, diff --git a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py index fd24d363..0c73c3dc 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py @@ -39,9 +39,9 @@ class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer): return obj.learning_unit.get_circle().title def get_criteria(self, obj): - performance_criteria: List[ - PerformanceCriteria - ] = obj.learning_unit.performancecriteria_set.all() + performance_criteria: List[PerformanceCriteria] = ( + obj.learning_unit.performancecriteria_set.all() + ) criteria = [] diff --git a/server/vbv_lernwelt/sso/role_sync/__init__.py b/server/vbv_lernwelt/sso/role_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/sso/role_sync/client.py b/server/vbv_lernwelt/sso/role_sync/client.py new file mode 100644 index 00000000..dd5dfc2a --- /dev/null +++ b/server/vbv_lernwelt/sso/role_sync/client.py @@ -0,0 +1,92 @@ +import unicodedata +from typing import Dict, List, Tuple + +from django.conf import settings +from keycloak import KeycloakAdmin, KeycloakOpenIDConnection + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.sso.role_sync.roles import ROLE_IDS, SSO_ROLES + +CourseRolesType = List[Tuple[str, List[str]]] + +if settings.OAUTH_SYNC_ROLES: + keycloak_connection = KeycloakOpenIDConnection( + server_url=settings.OAUTH_SIGNIN_URL, + realm_name=settings.OAUTH_SIGNIN_REALM, + user_realm_name=settings.OAUTH_SIGNIN_REALM, + client_id=settings.OAUTH_SIGNIN_ADMIN_CLIENT_ID, + client_secret_key=settings.OAUTH_SIGNIN_ADMIN_CLIENT_SECRET, + verify=True, + ) + + keycloak_admin = KeycloakAdmin(connection=keycloak_connection) + + +# todo: handle errors + + +def add_roles_to_user(user: User, course_roles: CourseRolesType): + user_id = user.additional_json_data.get("intermediate_sso_id", "") + if settings.OAUTH_SYNC_ROLES and user_id: + request_roles = _get_role_request_data(course_roles) + keycloak_admin.assign_realm_roles( + user_id=user_id, + roles=request_roles, + ) + return True + return False + + +def remove_roles_from_user(user: User, course_roles: CourseRolesType): + user_id = user.additional_json_data.get("intermediate_sso_id", "") + if settings.OAUTH_SYNC_ROLES and user_id: + request_roles = _get_role_request_data(course_roles) + keycloak_admin.delete_realm_roles_of_user( + user_id=user_id, + roles=request_roles, + ) + return True + return False + + +def update_roles_for_user( + user: User, add_course_roles: CourseRolesType, remove_course_roles: CourseRolesType +): + if settings.OAUTH_SYNC_ROLES: + add_roles_to_user(user, add_course_roles) + remove_roles_from_user(user, remove_course_roles) + return True + return False + + +def get_roles_for_user(user_id: str): + if settings.OAUTH_SYNC_ROLES: + return keycloak_admin.get_realm_roles_of_user( + user_id=user_id, + ) + return [] + + +# create sso-ID user and set roles +# sync + + +def _get_role_request_data(course_roles: CourseRolesType) -> List[Dict[str, str]]: + request_roles = [] + for item in course_roles: + course_slug, roles = item + sanitized_course_slug = _remove_accents(course_slug) + oauth_roles = _create_role_names(sanitized_course_slug, roles) + return request_roles + [ + {"id": ROLE_IDS[role], "name": role} for role in oauth_roles + ] + return request_roles + + +def _create_role_names(course_slug: str, roles: list) -> List[str]: + return [SSO_ROLES[course_slug][role] for role in roles] + + +def _remove_accents(input_str) -> str: + nfkd_form = unicodedata.normalize("NFKD", input_str) + return "".join([char for char in nfkd_form if not unicodedata.combining(char)]) diff --git a/server/vbv_lernwelt/sso/role_sync/roles.py b/server/vbv_lernwelt/sso/role_sync/roles.py new file mode 100644 index 00000000..630222aa --- /dev/null +++ b/server/vbv_lernwelt/sso/role_sync/roles.py @@ -0,0 +1,54 @@ +SSO_ROLES = { + "uberbetriebliche-kurse": { + "MEMBER": "myvbv-uberbetriebliche-kurse-member", + "EXPERT": "myvbv-uberbetriebliche-kurse-expert", + "SUPERVISOR": "myvbv-uberbetriebliche-kurse-supervisor", + "LEARNING_MENTOR": "myvbv-uberbetriebliche-kurse-mentor", + }, + "cours-interentreprises": { + "MEMBER": "myvbv-cours-interentreprises-member", + "EXPERT": "myvbv-cours-interentreprises-expert", + "SUPERVISOR": "myvbv-cours-interentreprises-supervisor", + "LEARNING_MENTOR": "myvbv-cours-interentreprises-mentor", + }, + "corso-interaziendale": { + "MEMBER": "myvbv-corso-interaziendale-member", + "EXPERT": "myvbv-corso-interaziendale-expert", + "SUPERVISOR": "myvbv-corso-interaziendale-supervisor", + "LEARNING_MENTOR": "myvbv-corso-interaziendale-mentor", + }, + "versicherungsvermittler-in": { + "MEMBER": "myvbv-versicherungsvermittler-in-member", + "LEARNING_MENTOR": "myvbv-versicherungsvermittler-in-mentor", + }, + "intermediaire-dassurance": { + "MEMBER": "myvbv-intermediaire-dassurance-member", + "LEARNING_MENTOR": "myvbv-intermediaire-dassurance-mentor", + }, + "intermediarioa-assicurativoa": { + "MEMBER": "myvbv-intermediarioa-assicurativoa-member", + "LEARNING_MENTOR": "myvbv-intermediarioa-assicurativoa-mentor", + }, +} + +# https://sso.test.b.lernetz.host/auth/admin/vbv/console/#/vbv/roles +ROLE_IDS = { + "myvbv-uberbetriebliche-kurse-member": "0725f2d4-c3f3-48b7-83ec-06acfae630e6", + "myvbv-uberbetriebliche-kurse-expert": "c7e33cb6-d227-4764-9b8e-d42af79fb46d", + "myvbv-uberbetriebliche-kurse-supervisor": "d88a7486-7ff4-475c-b840-2e1e0a9decb8", + "myvbv-uberbetriebliche-kurse-mentor": "db5f0e24-9512-4752-8c51-26b3aa6f7f6a", + "myvbv-cours-interentreprises-member": "458c65f4-e969-4ba7-a546-77948641bc0b", + "myvbv-cours-interentreprises-expert": "2ef51fc6-1e5a-427c-b4a9-314249ea24db", + "myvbv-cours-interentreprises-supervisor": "23e5994e-c499-42e8-b956-bb098be793e1", + "myvbv-cours-interentreprises-mentor": "cb37a093-32a3-479b-981e-0604b6b71f5e", + "myvbv-corso-interaziendale-member": "4d9cfc61-b555-44b1-a52d-76231d12f0cd", + "myvbv-corso-interaziendale-expert": "b2da77bd-c3c8-4d1e-9757-c016eaf219e3", + "myvbv-corso-interaziendale-supervisor": "8e9ea3e4-e814-4704-906e-d39f595811eb", + "myvbv-corso-interaziendale-mentor": "36fae39d-67f0-4ed6-9a1c-7d383be9e463", + "myvbv-versicherungsvermittler-in-member": "3ab4eab2-7d7c-43bb-a927-4cf54f24ccc2", + "myvbv-versicherungsvermittler-in-mentor": "12bf374a-293b-4abe-b255-7899eae31908", + "myvbv-intermediaire-dassurance-member": "5400fdae-2c37-4738-8667-0bcb50ed3609", + "myvbv-intermediaire-dassurance-mentor": "3bd737f9-731a-4548-aaf5-4c80175f2759", + "myvbv-intermediarioa-assicurativoa-member": "9fbaaa0f-cf8c-45f2-93f6-7174cb18a982", + "myvbv-intermediarioa-assicurativoa-mentor": "46b12e54-682e-44c0-b506-eab820138b66", +} diff --git a/server/vbv_lernwelt/sso/views.py b/server/vbv_lernwelt/sso/views.py index d48a019a..a665d7af 100644 --- a/server/vbv_lernwelt/sso/views.py +++ b/server/vbv_lernwelt/sso/views.py @@ -116,6 +116,7 @@ def authorize_signin(request): sso_id=id_token.get("oid"), first_name=id_token.get("given_name", ""), last_name=id_token.get("family_name", ""), + intermediate_sso_id=id_token.get("sub"), ) dj_login(request, user)