Merged in feature/VBV-692-keycloak (pull request #339)
Noch nicht mergen: Feature/VBV-692 keycloak Approved-by: Daniel Egger
This commit is contained in:
commit
527da8d779
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
script_path = os.path.abspath(__file__)
|
||||
script_dir = os.path.dirname(script_path)
|
||||
|
||||
dev_env = dotenv_values(f"{script_dir}/../../../env_secrets/caprover_vbv-develop.env")
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
|
||||
takeover_settings = [
|
||||
"OAUTH_SIGNIN_CLIENT_ID",
|
||||
"OAUTH_SIGNIN_CLIENT_SECRET",
|
||||
"OAUTH_SIGNIN_URL",
|
||||
"OAUTH_SIGNIN_REALM",
|
||||
"OAUTH_SIGNIN_ADMIN_CLIENT_ID",
|
||||
"OAUTH_SIGNIN_ADMIN_CLIENT_SECRET",
|
||||
"OAUTH_SIGNIN_SERVER_METADATA_URL",
|
||||
]
|
||||
|
||||
for setting in takeover_settings:
|
||||
os.environ[setting] = dev_env.get(setting)
|
||||
|
||||
os.environ["OAUTH_SIGNUP_REDIRECT_URI"] = "http://localhost:8001/sso/login"
|
||||
os.environ["OAUTH_SIGNIN_REDIRECT_URI"] = "http://localhost:8001/sso/callback"
|
||||
os.environ["OAUTH_SYNC_ROLES"] = "true"
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
|
||||
"AWS_S3_SECRET_ACCESS_KEY",
|
||||
"!!!default_for_quieting_cypress_within_pycharm!!!",
|
||||
)
|
||||
|
||||
from .test_cypress import * # noqa
|
||||
|
||||
DATATRANS_API_ENDPOINT = "https://api.sandbox.datatrans.com"
|
||||
DATATRANS_PAY_URL = "https://pay.sandbox.datatrans.com"
|
||||
|
|
@ -2,6 +2,11 @@ import pytest
|
|||
from _pytest.runner import runtestprotocol
|
||||
|
||||
|
||||
def pytest_ignore_collect(path, config):
|
||||
if path.basename.startswith("test_cypress_"):
|
||||
return True
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
parallel_items = []
|
||||
|
|
|
|||
|
|
@ -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,148 @@ 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
|
||||
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
|
||||
# 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
|
||||
# via msal
|
||||
pylint==2.17.5
|
||||
# via
|
||||
# msal
|
||||
# pyjwt
|
||||
pylint==3.2.3
|
||||
# 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
|
||||
pytest-order==1.2.1
|
||||
# via -r requirements-dev.in
|
||||
pytest-sugar==0.9.7
|
||||
# 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 +473,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 +513,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 +554,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 +563,7 @@ trailrunner==1.4.0
|
|||
# via
|
||||
# ufmt
|
||||
# usort
|
||||
traitlets==5.9.0
|
||||
traitlets==5.14.3
|
||||
# via
|
||||
# ipython
|
||||
# matplotlib-inline
|
||||
|
|
@ -548,82 +571,82 @@ 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
|
||||
watchfiles==0.19.0
|
||||
# via uvicorn
|
||||
wcwidth==0.2.6
|
||||
watchfiles==0.22.0
|
||||
# via
|
||||
# django-watchfiles
|
||||
# uvicorn
|
||||
wcwidth==0.2.13
|
||||
# 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
|
||||
willow[heif]==1.6.1
|
||||
# via wagtail
|
||||
wrapt==1.15.0
|
||||
# via astroid
|
||||
willow[heif]==1.6.3
|
||||
# via
|
||||
# wagtail
|
||||
# willow
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
|
|
|||
|
|
@ -52,3 +52,5 @@ azure-identity
|
|||
boto3
|
||||
openpyxl
|
||||
newrelic
|
||||
python-keycloak
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import uuid
|
||||
from typing import Any, Dict
|
||||
|
||||
import structlog
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
|
@ -6,6 +7,8 @@ from django.db import models
|
|||
from django.db.models import JSONField, Max
|
||||
from django.urls import reverse
|
||||
|
||||
from vbv_lernwelt.core.utils import sanitize_json_data_input
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -140,6 +143,16 @@ class User(AbstractUser):
|
|||
logger.warn("could not create avatar url", label="security", exc_info=True)
|
||||
return "/static/avatars/myvbv-default-avatar.png"
|
||||
|
||||
def update_additional_json_data(self, data: Dict[str, Any]):
|
||||
self.additional_json_data = (
|
||||
self.additional_json_data
|
||||
| sanitize_json_data_input(
|
||||
{
|
||||
**data,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
return self.create_avatar_url()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from datetime import date, datetime, time
|
||||
from unittest import skip
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.utils import sanitize_json_data_input
|
||||
|
||||
|
||||
class SimpleTestCase(TestCase):
|
||||
def test_simple(self):
|
||||
|
|
@ -10,3 +13,32 @@ class SimpleTestCase(TestCase):
|
|||
@skip("Do not fail in pipelines")
|
||||
def test_fail(self):
|
||||
self.assertEqual(1, 2)
|
||||
|
||||
|
||||
class SanitizerTestCase(TestCase):
|
||||
def test_date(self):
|
||||
a_date = date(2021, 1, 1)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
||||
def test_datetime(self):
|
||||
a_date = datetime(2021, 1, 1)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
||||
def test_time(self):
|
||||
a_date = time(23, 59, 59)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
import re
|
||||
from datetime import date, datetime, time
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
from rest_framework import serializers
|
||||
|
|
@ -65,3 +67,20 @@ def safe_deque_popleft(deq, default=None):
|
|||
return deq.popleft()
|
||||
except IndexError:
|
||||
return default
|
||||
|
||||
|
||||
def sanitize_json_data_input(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Saving additional_json_data fails if the data contains datetime objects.
|
||||
This is a quick and dirty fix to convert datetime objects to iso strings.
|
||||
"""
|
||||
for key, value in data.items():
|
||||
if isinstance(value, datetime):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, date):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, time):
|
||||
data[key] = value.isoformat()
|
||||
else:
|
||||
data[key] = value
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -192,6 +192,12 @@ class CourseCompletionStatus(Enum):
|
|||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class CourseCompletionStatusChoices(models.TextChoices):
|
||||
SUCCESS = CourseCompletionStatus.SUCCESS.value, "Success"
|
||||
FAIL = CourseCompletionStatus.FAIL.value, "Fail"
|
||||
UNKNOWN = CourseCompletionStatus.UNKNOWN.value, "Unknown"
|
||||
|
||||
|
||||
class CourseCompletion(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
|
|
@ -210,8 +216,8 @@ class CourseCompletion(models.Model):
|
|||
|
||||
completion_status = models.CharField(
|
||||
max_length=255,
|
||||
choices=[(status, status.value) for status in CourseCompletionStatus],
|
||||
default=CourseCompletionStatus.UNKNOWN.value,
|
||||
choices=CourseCompletionStatusChoices.choices,
|
||||
default=CourseCompletionStatus.UNKNOWN,
|
||||
)
|
||||
additional_json_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
|||
|
||||
|
||||
@admin.register(CourseSessionGroup)
|
||||
class CourseSessionAssignmentAdmin(admin.ModelAdmin):
|
||||
...
|
||||
class CourseSessionAssignmentAdmin(admin.ModelAdmin): ...
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import date, datetime, time
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import structlog
|
||||
|
|
@ -25,6 +25,7 @@ from vbv_lernwelt.learnpath.models import (
|
|||
LearningContentEdoniqTest,
|
||||
)
|
||||
from vbv_lernwelt.notify.models import NotificationCategory
|
||||
from vbv_lernwelt.sso.role_sync.services import create_and_update_user, create_user
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -493,6 +494,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 +539,10 @@ 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.update_additional_json_data({"intermediate_sso_id": intermediate_sso_id})
|
||||
init_notification_settings(user)
|
||||
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
|
|
@ -835,6 +841,10 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"
|
|||
last_name=data["Name"],
|
||||
)
|
||||
user.language = data["Sprache"]
|
||||
|
||||
# create user in intermediate sso i.e. Keycloak
|
||||
create_and_update_user(user)
|
||||
init_notification_settings(user)
|
||||
user.save()
|
||||
|
||||
# As the is never set this is the only way to determine the correct course
|
||||
|
|
@ -939,7 +949,9 @@ def create_or_update_student(data: Dict[str, Any]):
|
|||
)
|
||||
|
||||
user.language = data["Sprache"]
|
||||
update_user_json_data(user, data)
|
||||
|
||||
data["intermediate_sso_id"] = create_user(user)
|
||||
user.update_additional_json_data(data)
|
||||
user.save()
|
||||
|
||||
# general expert handling
|
||||
|
|
@ -993,32 +1005,12 @@ def sync_students_from_t2l(data):
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
update_user_json_data(user, data)
|
||||
user.update_additional_json_data(data)
|
||||
user.save()
|
||||
|
||||
|
||||
def update_user_json_data(user: User, data: Dict[str, Any]):
|
||||
# Set E-Mail notification settings for new users
|
||||
user.additional_json_data = user.additional_json_data | sanitize_json_data_input(
|
||||
{
|
||||
**data,
|
||||
"email_notification_categories": [str(NotificationCategory.INFORMATION)],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def sanitize_json_data_input(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Saving additional_json_data fails if the data contains datetime objects.
|
||||
This is a quick and dirty fix to convert datetime objects to iso strings.
|
||||
"""
|
||||
for key, value in data.items():
|
||||
if isinstance(value, datetime):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, date):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, time):
|
||||
data[key] = value.isoformat()
|
||||
else:
|
||||
data[key] = value
|
||||
return data
|
||||
def init_notification_settings(user: User):
|
||||
data = {
|
||||
"email_notification_categories": [str(NotificationCategory.INFORMATION)],
|
||||
}
|
||||
user.update_additional_json_data(data)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class CreateOrUpdateStudentTestCase(TestCase):
|
|||
"Tel. Privat": "079 593 83 43",
|
||||
"Geburtsdatum": "01.01.2000",
|
||||
"email_notification_categories": ["INFORMATION"],
|
||||
"intermediate_sso_id": "",
|
||||
}
|
||||
|
||||
def test_create_student(self):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
|
@ -7,7 +6,6 @@ from vbv_lernwelt.course.creators.test_course import create_test_course
|
|||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.importer.services import (
|
||||
create_or_update_student,
|
||||
sanitize_json_data_input,
|
||||
sync_students_from_t2l,
|
||||
)
|
||||
|
||||
|
|
@ -151,32 +149,3 @@ class SyncT2lTestCase(TestCase):
|
|||
self.fail(
|
||||
f"SyncT2lTestCase.test_ignors_wrong_contract_number: An exception was unexpectedly raised: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
class SanitizerTestCase(TestCase):
|
||||
def test_date(self):
|
||||
a_date = date(2021, 1, 1)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
||||
def test_datetime(self):
|
||||
a_date = datetime(2021, 1, 1)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
||||
def test_time(self):
|
||||
a_date = time(23, 59, 59)
|
||||
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||
|
||||
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||
|
||||
sanitized_data = sanitize_json_data_input(user_dict)
|
||||
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -147,14 +147,14 @@ def render_invoice_xml(
|
|||
SubElement(sales_order_header_fields, "PaymentCode").text = "9999"
|
||||
|
||||
# Skender: Bestellzeitpunkt
|
||||
SubElement(
|
||||
sales_order_header_fields, "PurchaseOrderDate"
|
||||
).text = order_date.isoformat()
|
||||
SubElement(sales_order_header_fields, "PurchaseOrderDate").text = (
|
||||
order_date.isoformat()
|
||||
)
|
||||
|
||||
# Skender: ePayment: TRANSACTION-ID von Datatrans in Bestellreferenz
|
||||
SubElement(
|
||||
sales_order_header_fields, "ReferencePurchaseOrder"
|
||||
).text = datatrans_transaction_id
|
||||
SubElement(sales_order_header_fields, "ReferencePurchaseOrder").text = (
|
||||
datatrans_transaction_id
|
||||
)
|
||||
|
||||
# Skender: ePayment: OrderID. max 10 Ziffern, erste Ziffer abhängig von der Plattform (4 = LMS, 6 = myVBV, 7 = EduManager)
|
||||
SubElement(sales_order_header_fields, "GroupingNumberAscii1").text = str(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,139 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth import admin as auth_admin, get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from keycloak.exceptions import KeycloakDeleteError, KeycloakPostError
|
||||
|
||||
# Register your models here.
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.sso.models import SsoSyncError, SsoUser
|
||||
from vbv_lernwelt.sso.role_sync.services import (
|
||||
create_and_update_user,
|
||||
sync_roles_for_user,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def create_sso_user_from_admin(user: User, request):
|
||||
try:
|
||||
create_and_update_user(user) # noqa
|
||||
user.save()
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, f"Der Bentuzer wurde in Keycloak erstellt."
|
||||
)
|
||||
except KeycloakPostError as e:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
f"Der Benutzer {user} konnte nicht in Keycloak erstellt werden: {e}",
|
||||
)
|
||||
|
||||
|
||||
def sync_sso_roles_from_admin(user: User, request):
|
||||
course_roles = [
|
||||
(csu.course_session.course.slug, csu.role)
|
||||
for csu in CourseSessionUser.objects.filter(user=user)
|
||||
]
|
||||
|
||||
course_roles += [
|
||||
(lm.course_session.course.slug, "LEARNING_MENTOR")
|
||||
for lm in LearningMentor.objects.filter(mentor=user)
|
||||
]
|
||||
|
||||
for csg in CourseSessionGroup.objects.filter(supervisor=user):
|
||||
for course_session in csg.course_session.all():
|
||||
course_roles.append((course_session.course.slug, "SUPERVISOR"))
|
||||
|
||||
try:
|
||||
sync_roles_for_user(user, course_roles)
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, f"Die Daten wurden mit Keycloak synchronisiert."
|
||||
)
|
||||
except KeycloakDeleteError as e:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
f"Die bestehenden Rollen für Benutzer ({user}) konnten in Keycloak nicht gelöscht werden: {e}",
|
||||
)
|
||||
except KeycloakPostError as e:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
f"Die neuen Rollen für Benutzer ({user}) konnten in Keycloak nicht erstellt werden: {e}",
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description="KEYCLOAK: Sync SSO Roles")
|
||||
def sync_sso_roles(modeladmin, request, queryset):
|
||||
for user in queryset:
|
||||
sync_sso_roles_from_admin(user, request)
|
||||
|
||||
|
||||
@admin.action(description="KEYCLOAK: Create User")
|
||||
def create_sso_user(modeladmin, request, queryset):
|
||||
for user in queryset:
|
||||
create_sso_user_from_admin(user, request)
|
||||
|
||||
|
||||
@admin.register(SsoUser)
|
||||
class SsoUserAdmin(auth_admin.UserAdmin):
|
||||
fieldsets = (
|
||||
(
|
||||
_("Personal info"),
|
||||
{"fields": ("first_name", "last_name", "email", "sso_id")},
|
||||
),
|
||||
(_("Additional data"), {"fields": ("additional_json_data",)}),
|
||||
)
|
||||
list_display = [
|
||||
"username",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"sso_id",
|
||||
"intermedia_sso_id",
|
||||
]
|
||||
search_fields = ["first_name", "last_name", "email", "username", "sso_id"]
|
||||
actions = [sync_sso_roles, create_sso_user]
|
||||
|
||||
# Make fields read-only
|
||||
readonly_fields = (
|
||||
"username",
|
||||
"password",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"additional_json_data",
|
||||
)
|
||||
|
||||
# Disable delete action
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if "delete_selected" in actions:
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
def intermedia_sso_id(self, obj):
|
||||
return obj.additional_json_data.get("intermediate_sso_id", "")
|
||||
|
||||
intermedia_sso_id.short_description = "Keycloak SSO ID"
|
||||
|
||||
|
||||
@admin.register(SsoSyncError)
|
||||
class SsoSyncErrorAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"created_at",
|
||||
"user",
|
||||
"action",
|
||||
"data",
|
||||
]
|
||||
raw_id_fields = [
|
||||
"user",
|
||||
]
|
||||
search_fields = [
|
||||
"user.email",
|
||||
"user.username",
|
||||
]
|
||||
list_filter = ("action",)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,10 @@ from django.apps import AppConfig
|
|||
class SsoConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.sso"
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
# pylint: disable=unused-import,import-outside-toplevel
|
||||
import vbv_lernwelt.sso.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
# Generated by Django 3.2.25 on 2024-06-26 15:34
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("core", "0007_auto_20240220_1058"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SsoUser",
|
||||
fields=[],
|
||||
options={
|
||||
"proxy": True,
|
||||
"indexes": [],
|
||||
"constraints": [],
|
||||
},
|
||||
bases=("core.user",),
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SsoSyncError",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"action",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ADD", "Add"),
|
||||
("REMOVE", "Remove"),
|
||||
("CREATE", "Create"),
|
||||
],
|
||||
default="ADD",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("data", models.JSONField(blank=True, default=dict)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from django.db import models
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
||||
class SsoUser(User):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class SsoSyncError(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
class Action(models.TextChoices):
|
||||
ADD = "ADD", "Add"
|
||||
REMOVE = "REMOVE", "Remove"
|
||||
CREATE = "CREATE", "Create"
|
||||
|
||||
action = models.CharField(
|
||||
choices=Action.choices, max_length=255, default=Action.ADD
|
||||
)
|
||||
data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} ({self.action})"
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
from django.conf import settings
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
if settings.APP_ENVIRONMENT.startswith("prod"):
|
||||
# PROD
|
||||
# https://sso.b.lernetz.host/auth/admin/vbv/console/#/vbv/roles
|
||||
ROLE_IDS = {
|
||||
"myvbv-uberbetriebliche-kurse-member": "e1acc4bb-46c7-43ae-b109-318380d3e3fa",
|
||||
"myvbv-uberbetriebliche-kurse-expert": "49d9d279-3d61-4f85-9b1d-1f53a97426dd",
|
||||
"myvbv-uberbetriebliche-kurse-supervisor": "4e4230c9-e120-44dd-b7e2-3810e3af9cb9",
|
||||
"myvbv-uberbetriebliche-kurse-mentor": "754258f5-fd36-4a21-8152-78cc890d545a",
|
||||
"myvbv-cours-interentreprises-member": "1b7c978d-f563-4779-a639-2087e7b585c3",
|
||||
"myvbv-cours-interentreprises-expert": "76d5b848-260a-4a06-a5ee-96c82bea6168",
|
||||
"myvbv-cours-interentreprises-supervisor": "bad083cb-5088-4742-8484-fa1146388c5f",
|
||||
"myvbv-cours-interentreprises-mentor": "a794f2eb-9e99-4332-bdf9-68e9a277212e",
|
||||
"myvbv-corso-interaziendale-member": "e1f4ea73-730d-4191-96a1-36d33d9f4ebb",
|
||||
"myvbv-corso-interaziendale-expert": "0a330fd3-a7d2-4e98-a575-2c30585cf576",
|
||||
"myvbv-corso-interaziendale-supervisor": "c514094f-5450-4f6c-bd52-d6cfa34b8892",
|
||||
"myvbv-corso-interaziendale-mentor": "255fe575-6191-40ca-8cda-7e2595926ce5",
|
||||
"myvbv-versicherungsvermittler-in-member": "cac4c013-da20-4f8f-854f-c0fa271500d6",
|
||||
"myvbv-versicherungsvermittler-in-mentor": "18f12a31-082d-45cf-9560-879ca927552a",
|
||||
"myvbv-intermediaire-dassurance-member": "35c6071a-dc9a-4071-bbbb-a1a5323ec962",
|
||||
"myvbv-intermediaire-dassurance-mentor": "d1d40f42-61b2-4ae4-b130-fc3c82492125",
|
||||
"myvbv-intermediarioa-assicurativoa-member": "1188e214-8aee-4b43-88e9-663e939a4af8",
|
||||
"myvbv-intermediarioa-assicurativoa-mentor": "5e2ecfc8-c0a8-408f-a78c-4d2fd94cb4fb",
|
||||
}
|
||||
else:
|
||||
# STAGE
|
||||
# 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",
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import unicodedata
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import structlog
|
||||
from django.conf import settings
|
||||
from keycloak import KeycloakAdmin, KeycloakOpenIDConnection
|
||||
from keycloak.exceptions import KeycloakDeleteError, KeycloakPostError
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.sso.models import SsoSyncError
|
||||
from vbv_lernwelt.sso.role_sync.roles import ROLE_IDS, SSO_ROLES
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
CourseRolesType = List[Tuple[str, str]]
|
||||
KeyCloakRolesType = List[Dict[str, str]]
|
||||
|
||||
keycloak_admin = None # Needed for pytest
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def add_roles_to_user(user: User, course_roles: CourseRolesType):
|
||||
return _handle_add_remove_action(
|
||||
user=user, course_roles=course_roles, action=SsoSyncError.Action.ADD
|
||||
)
|
||||
|
||||
|
||||
def remove_roles_from_user(user: User, course_roles: CourseRolesType):
|
||||
return _handle_add_remove_action(
|
||||
user=user, course_roles=course_roles, action=SsoSyncError.Action.REMOVE
|
||||
)
|
||||
|
||||
|
||||
def _handle_add_remove_action(
|
||||
user: User,
|
||||
course_roles: CourseRolesType,
|
||||
action: SsoSyncError.Action,
|
||||
):
|
||||
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)
|
||||
if not request_roles:
|
||||
return False
|
||||
|
||||
if action == SsoSyncError.Action.ADD:
|
||||
_kc_assign_realm_roles(user, user_id, request_roles)
|
||||
elif action == SsoSyncError.Action.REMOVE:
|
||||
_kc_delete_realm_roles(user, user_id, 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:
|
||||
remove_ret_value = remove_roles_from_user(user, remove_course_roles)
|
||||
add_ret_value = add_roles_to_user(user, add_course_roles)
|
||||
return remove_ret_value and add_ret_value
|
||||
return False
|
||||
|
||||
|
||||
def sync_roles_for_user(user: User, course_roles: CourseRolesType):
|
||||
if settings.OAUTH_SYNC_ROLES:
|
||||
user_id = user.additional_json_data.get("intermediate_sso_id", "")
|
||||
if user_id:
|
||||
assigned_roles = _filter_non_myvbv_roles(
|
||||
keycloak_admin.get_realm_roles_of_user(user_id=user_id)
|
||||
)
|
||||
|
||||
if assigned_roles:
|
||||
_kc_delete_realm_roles(user, user_id, assigned_roles)
|
||||
|
||||
roles = _get_role_request_data(course_roles)
|
||||
keycloak_admin.assign_realm_roles(user_id=user_id, roles=roles)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def create_user(user: User):
|
||||
if keycloak_admin:
|
||||
return _kc_create_user(user)
|
||||
return ""
|
||||
|
||||
|
||||
def create_and_update_user(user: User, save=False):
|
||||
sso_data = {"intermediate_sso_id": create_user(user)}
|
||||
user.update_additional_json_data(sso_data)
|
||||
if save:
|
||||
user.save()
|
||||
|
||||
|
||||
def get_roles_for_user(user_id: str):
|
||||
if keycloak_admin:
|
||||
return keycloak_admin.get_realm_roles_of_user(
|
||||
user_id=user_id,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
# Keycloak wrappers
|
||||
def _kc_assign_realm_roles(user: User, user_id: str, roles: List[KeyCloakRolesType]):
|
||||
try:
|
||||
keycloak_admin.assign_realm_roles(user_id=user_id, roles=roles)
|
||||
except KeycloakPostError as e:
|
||||
SsoSyncError.objects.create(
|
||||
user=user, action=SsoSyncError.Action.ADD, data=roles
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def _kc_delete_realm_roles(user: User, user_id: str, roles: List[KeyCloakRolesType]):
|
||||
try:
|
||||
keycloak_admin.delete_realm_roles_of_user(user_id=user_id, roles=roles)
|
||||
except KeycloakDeleteError as e:
|
||||
SsoSyncError.objects.create(
|
||||
user=user, action=SsoSyncError.Action.REMOVE, data=roles
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def _kc_create_user(user: User) -> str:
|
||||
user_data = {
|
||||
"username": user.email,
|
||||
"email": user.email,
|
||||
"enabled": True,
|
||||
"firstName": user.first_name,
|
||||
"lastName": user.last_name,
|
||||
}
|
||||
try:
|
||||
return keycloak_admin.create_user(user_data, exist_ok=True)
|
||||
except KeycloakPostError as e:
|
||||
SsoSyncError.objects.create(user=user, action=SsoSyncError.Action.ADD, data={})
|
||||
raise e
|
||||
|
||||
|
||||
def _get_role_request_data(course_roles: CourseRolesType) -> List[KeyCloakRolesType]:
|
||||
request_roles = []
|
||||
for item in course_roles:
|
||||
course_slug, role = item
|
||||
sanitized_course_slug = _remove_accents(course_slug)
|
||||
try:
|
||||
oauth_role = _create_role_name(sanitized_course_slug, role)
|
||||
request_roles.append({"id": ROLE_IDS[oauth_role], "name": oauth_role})
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
"Role or course not found in SSO_ROLES",
|
||||
course_slug=course_slug,
|
||||
role=role,
|
||||
label="role_sync",
|
||||
)
|
||||
return request_roles
|
||||
|
||||
|
||||
def _create_role_name(course_slug: str, role: str) -> List[str]:
|
||||
return SSO_ROLES[course_slug][role]
|
||||
|
||||
|
||||
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)])
|
||||
|
||||
|
||||
def _filter_non_myvbv_roles(roles: List[KeyCloakRolesType]) -> List[KeyCloakRolesType]:
|
||||
return [role for role in roles if role["name"].startswith("myvbv-")]
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import structlog
|
||||
from django.db.models.signals import m2m_changed, post_delete, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
from keycloak.exceptions import KeycloakDeleteError, KeycloakError, KeycloakPostError
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.sso.role_sync.services import (
|
||||
add_roles_to_user,
|
||||
remove_roles_from_user,
|
||||
update_roles_for_user,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# CourseSessionUser
|
||||
@receiver(post_delete, sender=CourseSessionUser, dispatch_uid="delete_sso_roles_in_cs")
|
||||
def remove_sso_roles_in_cs(sender, instance, **kwargs):
|
||||
# check if the user has any other roles in the course
|
||||
if not CourseSessionUser.objects.filter(
|
||||
user=instance.user, course_session__course=instance.course_session.course
|
||||
).exists():
|
||||
_remove_sso_role(
|
||||
instance.user, instance.course_session.course.slug, instance.role
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=CourseSessionUser, dispatch_uid="update_sso_roles_in_cs")
|
||||
def update_sso_roles_in_cs(sender, instance: CourseSessionUser, **kwargs):
|
||||
if not instance.created_at:
|
||||
_add_sso_role(instance.user, instance.course_session.course.slug, instance.role)
|
||||
else:
|
||||
old_csu = CourseSessionUser.objects.get(pk=instance.pk)
|
||||
if (
|
||||
old_csu.role != instance.role
|
||||
or old_csu.course_session.course != instance.course_session.course
|
||||
):
|
||||
try:
|
||||
update_roles_for_user(
|
||||
instance.user,
|
||||
add_course_roles=[
|
||||
(instance.course_session.course.slug, instance.role)
|
||||
],
|
||||
remove_course_roles=[
|
||||
(old_csu.course_session.course.slug, old_csu.role)
|
||||
],
|
||||
)
|
||||
except KeycloakError:
|
||||
# fail silently, error object is being created in the service
|
||||
pass
|
||||
|
||||
|
||||
# CourseSessionGroup
|
||||
@receiver(pre_delete, sender=CourseSessionGroup, dispatch_uid="delete_sso_roles_in_csg")
|
||||
def remove_sso_roles_in_csg(sender, instance: CourseSessionGroup, **kwargs):
|
||||
for user in instance.supervisor.all():
|
||||
_remove_sso_role(user, instance.course.slug, "SUPERVISOR")
|
||||
|
||||
|
||||
@receiver(
|
||||
m2m_changed,
|
||||
sender=CourseSessionGroup.supervisor.through,
|
||||
dispatch_uid="update_sso_roles_in_csg",
|
||||
)
|
||||
def update_sso_roles_in_csg(sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||
if action == "pre_add":
|
||||
added_supervisors = model.objects.filter(pk__in=pk_set)
|
||||
for user in added_supervisors:
|
||||
_add_sso_role(user, instance.course.slug, "SUPERVISOR")
|
||||
|
||||
elif action == "pre_remove":
|
||||
removed_supervisors = model.objects.filter(pk__in=pk_set)
|
||||
for user in removed_supervisors:
|
||||
_remove_sso_role(user, instance.course.slug, "SUPERVISOR")
|
||||
|
||||
|
||||
# LearningMentor
|
||||
@receiver(post_delete, sender=LearningMentor, dispatch_uid="delete_sso_roles_in_lm")
|
||||
def remove_sso_roles_in_lm(sender, instance: LearningMentor, **kwargs):
|
||||
if not LearningMentor.objects.filter(
|
||||
mentor=instance.mentor, course_session__course=instance.course_session.course
|
||||
).exists():
|
||||
_remove_sso_role(
|
||||
instance.mentor, instance.course_session.course.slug, "LEARNING_MENTOR"
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=LearningMentor, dispatch_uid="update_sso_roles_in_lm")
|
||||
def update_sso_roles_in_lm(sender, instance: LearningMentor, **kwargs):
|
||||
if not instance.pk:
|
||||
_add_sso_role(
|
||||
instance.mentor, instance.course_session.course.slug, "LEARNING_MENTOR"
|
||||
)
|
||||
|
||||
|
||||
def _remove_sso_role(user: User, course_slug: str, role: str):
|
||||
try:
|
||||
logger.debug(
|
||||
f"Removing {role} role from user",
|
||||
user=user,
|
||||
course=course_slug,
|
||||
label="role_sync",
|
||||
)
|
||||
remove_roles_from_user(user, [(course_slug, role)])
|
||||
except KeycloakDeleteError:
|
||||
# fail silently, error object is being created in the service
|
||||
pass
|
||||
|
||||
|
||||
def _add_sso_role(user: User, course_slug: str, role: str):
|
||||
try:
|
||||
logger.debug(
|
||||
f"Adding {role} role to user",
|
||||
user=user,
|
||||
course=course_slug,
|
||||
label="role_sync",
|
||||
)
|
||||
add_roles_to_user(user, [(course_slug, role)])
|
||||
except KeycloakPostError:
|
||||
# fail silently, error object is being created in the service
|
||||
pass
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.test import override_settings, TestCase
|
||||
from keycloak.exceptions import KeycloakDeleteError, KeycloakPostError
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.sso.models import SsoSyncError
|
||||
from vbv_lernwelt.sso.role_sync.services import (
|
||||
_filter_non_myvbv_roles,
|
||||
_remove_accents,
|
||||
add_roles_to_user,
|
||||
create_user,
|
||||
remove_roles_from_user,
|
||||
sync_roles_for_user,
|
||||
update_roles_for_user,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(OAUTH_SYNC_ROLES=True)
|
||||
class ApiTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User(email="test@example.com", first_name="Test", last_name="User")
|
||||
self.user.additional_json_data = {"intermediate_sso_id": "1234"}
|
||||
self.user.save()
|
||||
|
||||
self.course_roles = [
|
||||
("überbetriebliche-kurse", "EXPERT"),
|
||||
("versicherungsvermittler-in", "MEMBER"),
|
||||
]
|
||||
self.expected_roles = [
|
||||
{
|
||||
"name": "myvbv-uberbetriebliche-kurse-expert",
|
||||
"id": "c7e33cb6-d227-4764-9b8e-d42af79fb46d",
|
||||
},
|
||||
{
|
||||
"name": "myvbv-versicherungsvermittler-in-member",
|
||||
"id": "3ab4eab2-7d7c-43bb-a927-4cf54f24ccc2",
|
||||
},
|
||||
]
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_add_roles_to_user_success(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.assign_realm_roles.return_value = None
|
||||
|
||||
result = add_roles_to_user(self.user, self.course_roles)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_keycloak_admin.assign_realm_roles.assert_called_once_with(
|
||||
user_id="1234", roles=self.expected_roles
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_add_roles_to_user_keycloak_post_error(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.assign_realm_roles.side_effect = KeycloakPostError
|
||||
|
||||
with self.assertRaises(KeycloakPostError) as cm:
|
||||
add_roles_to_user(self.user, self.course_roles)
|
||||
|
||||
exception = cm.exception
|
||||
self.assertIsInstance(exception, KeycloakPostError)
|
||||
error_obj = SsoSyncError.objects.get(user=self.user)
|
||||
self.assertEqual(error_obj.data, self.expected_roles)
|
||||
self.assertEqual(error_obj.action, SsoSyncError.Action.ADD)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_remove_roles_to_user_success(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.delete_realm_roles_of_user.return_value = None
|
||||
|
||||
result = remove_roles_from_user(self.user, self.course_roles)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_keycloak_admin.delete_realm_roles_of_user.assert_called_once_with(
|
||||
user_id="1234", roles=self.expected_roles
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_remove_roles_to_user_keycloak_delete_error(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.delete_realm_roles_of_user.side_effect = KeycloakDeleteError
|
||||
|
||||
with self.assertRaises(KeycloakDeleteError) as cm:
|
||||
remove_roles_from_user(self.user, self.course_roles)
|
||||
|
||||
exception = cm.exception
|
||||
self.assertIsInstance(exception, KeycloakDeleteError)
|
||||
error_obj = SsoSyncError.objects.get(user=self.user)
|
||||
self.assertEqual(error_obj.data, self.expected_roles)
|
||||
self.assertEqual(error_obj.action, SsoSyncError.Action.REMOVE)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.remove_roles_from_user")
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.add_roles_to_user")
|
||||
def test_update_roles_to_user(
|
||||
self, mock_add_roles_to_user, mock_remove_roles_from_user
|
||||
):
|
||||
mock_add_roles_to_user.return_value = True
|
||||
mock_remove_roles_from_user.return_value = True
|
||||
|
||||
update_roles_for_user(self.user, self.course_roles, self.course_roles)
|
||||
mock_add_roles_to_user.assert_called_once()
|
||||
mock_remove_roles_from_user.assert_called_once()
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_sync_roles_to_user(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.get_realm_roles_of_user.return_value = (
|
||||
self.expected_roles
|
||||
) # just use them here as well
|
||||
mock_keycloak_admin.delete_realm_roles_of_user.return_value = True
|
||||
mock_keycloak_admin.assign_realm_roles.return_value = None
|
||||
|
||||
sync_roles_for_user(self.user, self.course_roles)
|
||||
mock_keycloak_admin.get_realm_roles_of_user.assert_called_once_with(
|
||||
user_id="1234"
|
||||
)
|
||||
mock_keycloak_admin.delete_realm_roles_of_user.assert_called_once_with(
|
||||
user_id="1234", roles=self.expected_roles
|
||||
)
|
||||
mock_keycloak_admin.assign_realm_roles.assert_called_once_with(
|
||||
user_id="1234", roles=self.expected_roles
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_create_user(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.create_user.return_value = "im-an-uuid-1234"
|
||||
|
||||
user_data = {
|
||||
"username": self.user.email,
|
||||
"email": self.user.email,
|
||||
"enabled": True,
|
||||
"firstName": self.user.first_name,
|
||||
"lastName": self.user.last_name,
|
||||
}
|
||||
|
||||
create_user(self.user)
|
||||
mock_keycloak_admin.create_user.assert_called_once_with(
|
||||
user_data, exist_ok=True
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.role_sync.services.keycloak_admin")
|
||||
def test_ignore_missing_course(self, mock_keycloak_admin):
|
||||
mock_keycloak_admin.assign_realm_roles.return_value = None
|
||||
|
||||
course_roles = [
|
||||
("blabla-kurse", "EXPERT"),
|
||||
]
|
||||
result = add_roles_to_user(self.user, course_roles)
|
||||
|
||||
self.assertFalse(result)
|
||||
mock_keycloak_admin.assign_realm_roles.assert_not_called()
|
||||
|
||||
|
||||
class HelpersTestCase(TestCase):
|
||||
def test_remove_accents(self):
|
||||
no_accents = _remove_accents("äüöéèà")
|
||||
self.assertEqual(no_accents, "auoeea")
|
||||
|
||||
def test_filter_non_myvbv_roles(self):
|
||||
roles = [
|
||||
{"name": "myvbv-uberbetriebliche-kurse-expert"},
|
||||
{"name": "myvbv-versicherungsvermittler-in-member"},
|
||||
{"name": "other-role"},
|
||||
]
|
||||
filtered_roles = [
|
||||
{"name": "myvbv-uberbetriebliche-kurse-expert"},
|
||||
{"name": "myvbv-versicherungsvermittler-in-member"},
|
||||
]
|
||||
result = _filter_non_myvbv_roles(roles)
|
||||
self.assertEqual(result, filtered_roles)
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
from unittest.mock import call, patch
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import Signal
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.constants import (
|
||||
TEST_COURSE_SESSION_BERN_ID,
|
||||
TEST_COURSE_SESSION_ZURICH_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_SUPERVISOR1_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
add_course_session_user,
|
||||
create_course,
|
||||
create_course_session,
|
||||
create_user,
|
||||
)
|
||||
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.sso.signals import update_sso_roles_in_cs
|
||||
|
||||
|
||||
class CourseSessionUserTests(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
create_test_course(include_uk=True, with_sessions=True)
|
||||
|
||||
self.student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||
self.csu1_student1 = CourseSessionUser.objects.get(
|
||||
user=self.student1, course_session__id=TEST_COURSE_SESSION_BERN_ID
|
||||
)
|
||||
self.student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
|
||||
self.csu1_student2 = CourseSessionUser.objects.get(
|
||||
user=self.student2, course_session__id=TEST_COURSE_SESSION_ZURICH_ID
|
||||
)
|
||||
|
||||
# Disconnect the actual signal handler to avoid side effects during testing
|
||||
pre_save.disconnect(receiver=update_sso_roles_in_cs, sender=CourseSessionUser)
|
||||
|
||||
# Connect a mock signal handler
|
||||
self.mock_pre_save_signal = Signal()
|
||||
self.mock_pre_save_signal.connect(
|
||||
receiver=update_sso_roles_in_cs, sender=CourseSessionUser
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
def test_remove_roles_for_single_role_in_cs(self, mock_remove_roles_from_user):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
|
||||
self.csu1_student1.delete()
|
||||
|
||||
self.assertEqual(mock_remove_roles_from_user.call_count, 1)
|
||||
|
||||
mock_remove_roles_from_user.assert_called_with(
|
||||
self.student1, [(self.csu1_student1.course_session.course.slug, "MEMBER")]
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
def test_dont_remove_roles_for_multiple_roles_in_cs(
|
||||
self, mock_remove_roles_from_user
|
||||
):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
|
||||
self.csu1_student2.delete()
|
||||
|
||||
self.assertFalse(mock_remove_roles_from_user.called)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.add_roles_to_user")
|
||||
def test_add_role_for_user_on_creation(self, mock_add_roles_from_user):
|
||||
mock_add_roles_from_user.return_value = None
|
||||
self.csu1_student1.delete()
|
||||
csu = CourseSessionUser.objects.create(
|
||||
user=self.student1,
|
||||
course_session=self.csu1_student1.course_session,
|
||||
role="MEMBER",
|
||||
)
|
||||
|
||||
self.mock_pre_save_signal.send(sender=CourseSessionUser, instance=csu)
|
||||
|
||||
self.assertEqual(mock_add_roles_from_user.call_count, 1)
|
||||
|
||||
mock_add_roles_from_user.assert_called_with(
|
||||
self.student1, [(self.csu1_student1.course_session.course.slug, "MEMBER")]
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.update_roles_for_user")
|
||||
def test_update_role_for_user_on_update_with_role_change(
|
||||
self, mock_update_roles_for_user
|
||||
):
|
||||
mock_update_roles_for_user.return_value = None
|
||||
self.csu1_student1.role = "TRAINER"
|
||||
|
||||
self.mock_pre_save_signal.send(
|
||||
sender=CourseSessionUser, instance=self.csu1_student1
|
||||
)
|
||||
|
||||
self.assertEqual(mock_update_roles_for_user.call_count, 1)
|
||||
|
||||
mock_update_roles_for_user.assert_called_with(
|
||||
self.student1,
|
||||
add_course_roles=[
|
||||
(self.csu1_student1.course_session.course.slug, "TRAINER")
|
||||
],
|
||||
remove_course_roles=[
|
||||
(self.csu1_student1.course_session.course.slug, "MEMBER")
|
||||
],
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.update_roles_for_user")
|
||||
def test_update_role_for_user_on_update_with_course_change(
|
||||
self, mock_update_roles_for_user
|
||||
):
|
||||
mock_update_roles_for_user.return_value = None
|
||||
course, self.course_page = create_course("Test Course")
|
||||
course_session = create_course_session(course=course, title="Test VV")
|
||||
old_course_slug = self.csu1_student1.course_session.course.slug
|
||||
|
||||
self.csu1_student1.course_session = course_session
|
||||
|
||||
self.mock_pre_save_signal.send(
|
||||
sender=CourseSessionUser, instance=self.csu1_student1
|
||||
)
|
||||
|
||||
self.assertEqual(mock_update_roles_for_user.call_count, 1)
|
||||
|
||||
mock_update_roles_for_user.assert_called_with(
|
||||
self.student1,
|
||||
add_course_roles=[
|
||||
(self.csu1_student1.course_session.course.slug, "MEMBER")
|
||||
],
|
||||
remove_course_roles=[(old_course_slug, "MEMBER")],
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.update_roles_for_user")
|
||||
def test_dont_update_role_for_user_on_update(self, mock_update_roles_for_user):
|
||||
mock_update_roles_for_user.return_value = None
|
||||
self.csu1_student1.role = "MEMBER"
|
||||
|
||||
self.mock_pre_save_signal.send(
|
||||
sender=CourseSessionUser, instance=self.csu1_student1
|
||||
)
|
||||
|
||||
self.assertEqual(mock_update_roles_for_user.call_count, 0)
|
||||
|
||||
|
||||
class CourseSessionGroupTests(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
create_test_course(include_uk=True, with_sessions=True)
|
||||
|
||||
self.csg = CourseSessionGroup.objects.get(
|
||||
name="Region 1",
|
||||
)
|
||||
course = Course.objects.get(id=COURSE_TEST_ID)
|
||||
self.slug = course.slug
|
||||
|
||||
self.trainer = User.objects.get(id=TEST_TRAINER1_USER_ID)
|
||||
self.supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
def test_remove_roles_for_csg_supervisors(self, mock_remove_roles_from_user):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
self.csg.supervisor.set([self.trainer, self.supervisor])
|
||||
self.csg.delete()
|
||||
|
||||
expected_calls = [
|
||||
call(self.supervisor, [(self.slug, "SUPERVISOR")]),
|
||||
call(self.trainer, [(self.slug, "SUPERVISOR")]),
|
||||
]
|
||||
mock_remove_roles_from_user.assert_has_calls(expected_calls, any_order=True)
|
||||
self.assertEqual(mock_remove_roles_from_user.call_count, 2)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
@patch("vbv_lernwelt.sso.signals.add_roles_to_user")
|
||||
def test_add_supervisors_to_csg(
|
||||
self, mock_add_roles_to_user, mock_remove_roles_from_user
|
||||
):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
mock_add_roles_to_user.return_value = None
|
||||
|
||||
self.csg.supervisor.add(self.trainer)
|
||||
|
||||
self.assertEqual(mock_add_roles_to_user.call_count, 1)
|
||||
self.assertEqual(mock_remove_roles_from_user.call_count, 0)
|
||||
|
||||
mock_add_roles_to_user.assert_called_with(
|
||||
self.trainer, [(self.slug, "SUPERVISOR")]
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
@patch("vbv_lernwelt.sso.signals.add_roles_to_user")
|
||||
def test_remove_supervisors_to_csg(
|
||||
self, mock_add_roles_to_user, mock_remove_roles_from_user
|
||||
):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
mock_add_roles_to_user.return_value = None
|
||||
|
||||
self.csg.supervisor.remove(self.supervisor)
|
||||
|
||||
self.assertEqual(mock_add_roles_to_user.call_count, 0)
|
||||
self.assertEqual(mock_remove_roles_from_user.call_count, 1)
|
||||
|
||||
mock_remove_roles_from_user.assert_called_with(
|
||||
self.supervisor, [(self.slug, "SUPERVISOR")]
|
||||
)
|
||||
|
||||
|
||||
class LearningMentorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.course, self.course_page = create_course("Test Course")
|
||||
self.course_session = create_course_session(course=self.course, title="Test VV")
|
||||
|
||||
self.user = create_user("mentor")
|
||||
self.mentor = LearningMentor.objects.create(
|
||||
mentor=self.user, course_session=self.course_session
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.remove_roles_from_user")
|
||||
def test_remove_roles_for_learning_mentor_on_delete(
|
||||
self, mock_remove_roles_from_user
|
||||
):
|
||||
mock_remove_roles_from_user.return_value = None
|
||||
|
||||
self.mentor.delete()
|
||||
|
||||
self.assertEqual(mock_remove_roles_from_user.call_count, 1)
|
||||
|
||||
mock_remove_roles_from_user.assert_called_with(
|
||||
self.user, [(self.course.slug, "LEARNING_MENTOR")]
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.add_roles_to_user")
|
||||
def test_add_roles_for_learning_mentor_on_create(self, mock_add_roles_from_user):
|
||||
mock_add_roles_from_user.return_value = None
|
||||
self.mentor.delete()
|
||||
|
||||
LearningMentor.objects.create(
|
||||
mentor=self.user, course_session=self.course_session
|
||||
)
|
||||
|
||||
self.assertEqual(mock_add_roles_from_user.call_count, 1)
|
||||
|
||||
mock_add_roles_from_user.assert_called_with(
|
||||
self.user, [(self.course.slug, "LEARNING_MENTOR")]
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.sso.signals.add_roles_to_user")
|
||||
def test_no_add_roles_for_learning_mentor_on_update(self, mock_add_roles_from_user):
|
||||
mock_add_roles_from_user.return_value = None
|
||||
|
||||
participant_1 = add_course_session_user(
|
||||
self.course_session,
|
||||
create_user("participant_1"),
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
mock_add_roles_from_user.reset_mock()
|
||||
self.mentor.participants.set([participant_1])
|
||||
self.assertEqual(mock_add_roles_from_user.call_count, 0)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue