From 3e71b2166c2ba389b195286605cdc80287d77b5a Mon Sep 17 00:00:00 2001 From: Jonathan Perret Date: Wed, 17 Apr 2024 14:35:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F(common)=20replace=20Djang?= =?UTF-8?q?o=20app=20with=20SATOSA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We mostly import SATOSA and launch its WSGI app. --- .dockerignore | 1 - .github/workflows/oidc2fer.yml | 20 +- .gitignore | 9 - Dockerfile | 71 +-- Makefile | 46 +- README.md | 11 - bin/_config.sh | 9 - bin/manage | 6 - bin/pylint | 8 +- bin/pytest | 1 - docker-compose.yml | 20 +- docker/files/etc/nginx/conf.d/default.conf | 30 +- docker/files/usr/local/bin/entrypoint | 8 + .../etc/gunicorn/{oidc2fer.py => satosa.py} | 3 +- env.d/development/common.dist | 6 - env.d/development/satosa.dist | 27 ++ src/backend/MANIFEST.in | 3 - src/backend/core/__init__.py | 0 src/backend/core/admin.py | 61 --- src/backend/core/factories.py | 24 - src/backend/core/migrations/0001_initial.py | 45 -- src/backend/core/migrations/__init__.py | 0 src/backend/core/models.py | 100 ----- src/backend/core/tests/__init__.py | 0 src/backend/demo/__init__.py | 0 src/backend/demo/management/__init__.py | 0 .../demo/management/commands/__init__.py | 0 .../management/commands/createsuperuser.py | 46 -- src/backend/manage.py | 14 - src/backend/oidc2fer/__init__.py | 0 src/backend/oidc2fer/settings.py | 422 ------------------ src/backend/oidc2fer/urls.py | 18 - src/backend/oidc2fer/wsgi.py | 17 - src/{backend => satosa}/.pylintrc | 3 +- src/{backend => satosa}/__init__.py | 0 src/satosa/internal_attributes.yaml | 46 ++ .../plugins/backends/saml2_backend.yaml | 53 +++ .../frontends/openid_connect_frontend.yaml | 44 ++ .../plugins/frontends/ping_frontend.yaml | 3 + .../microservices/static_attributes.yaml | 13 + src/satosa/proxy_conf.yaml | 41 ++ src/{backend => satosa}/pyproject.toml | 34 +- src/{backend => satosa}/setup.py | 0 src/satosa/tests/test_dummy.py | 6 + 44 files changed, 305 insertions(+), 964 deletions(-) delete mode 100755 bin/manage rename docker/files/usr/local/etc/gunicorn/{oidc2fer.py => satosa.py} (87%) create mode 100644 env.d/development/satosa.dist delete mode 100644 src/backend/MANIFEST.in delete mode 100644 src/backend/core/__init__.py delete mode 100644 src/backend/core/admin.py delete mode 100644 src/backend/core/factories.py delete mode 100644 src/backend/core/migrations/0001_initial.py delete mode 100644 src/backend/core/migrations/__init__.py delete mode 100644 src/backend/core/models.py delete mode 100644 src/backend/core/tests/__init__.py delete mode 100644 src/backend/demo/__init__.py delete mode 100644 src/backend/demo/management/__init__.py delete mode 100644 src/backend/demo/management/commands/__init__.py delete mode 100644 src/backend/demo/management/commands/createsuperuser.py delete mode 100644 src/backend/manage.py delete mode 100644 src/backend/oidc2fer/__init__.py delete mode 100755 src/backend/oidc2fer/settings.py delete mode 100644 src/backend/oidc2fer/urls.py delete mode 100644 src/backend/oidc2fer/wsgi.py rename src/{backend => satosa}/.pylintrc (99%) rename src/{backend => satosa}/__init__.py (100%) create mode 100644 src/satosa/internal_attributes.yaml create mode 100644 src/satosa/plugins/backends/saml2_backend.yaml create mode 100644 src/satosa/plugins/frontends/openid_connect_frontend.yaml create mode 100644 src/satosa/plugins/frontends/ping_frontend.yaml create mode 100644 src/satosa/plugins/microservices/static_attributes.yaml create mode 100644 src/satosa/proxy_conf.yaml rename src/{backend => satosa}/pyproject.toml (67%) rename src/{backend => satosa}/setup.py (100%) create mode 100644 src/satosa/tests/test_dummy.py diff --git a/.dockerignore b/.dockerignore index dc97222..bb0246c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,7 +20,6 @@ docs *.log # Development/test cache & configurations -data .cache .circleci .git diff --git a/.github/workflows/oidc2fer.yml b/.github/workflows/oidc2fer.yml index 3d01143..4d6059a 100644 --- a/.github/workflows/oidc2fer.yml +++ b/.github/workflows/oidc2fer.yml @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: src/backend + working-directory: src/satosa steps: - name: Checkout repository uses: actions/checkout@v2 @@ -73,7 +73,7 @@ jobs: - name: Install development dependencies run: | # Python's xmlsec requirement - sudo apt-get update -y -q && sudo apt-get install -y -q libxmlsec1-dev + sudo apt-get update -y -q && sudo apt-get install -y -q xmlsec1 libxmlsec1-dev pip install --user .[dev] - name: Check code formatting with ruff run: ~/.local/bin/ruff format . --diff @@ -86,21 +86,11 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: src/backend - - env: - DJANGO_CONFIGURATION: Test - DJANGO_SETTINGS_MODULE: oidc2fer.settings - DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly - OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only + working-directory: src/satosa steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Create writable /data - run: | - sudo mkdir -p /data/media && \ - sudo mkdir -p /data/static - name: Install Python uses: actions/setup-python@v3 with: @@ -108,7 +98,7 @@ jobs: - name: Install development dependencies run: | # Python's xmlsec requirement - sudo apt-get update -y -q && sudo apt-get install -y -q libxmlsec1-dev + sudo apt-get update -y -q && sudo apt-get install -y -q xmlsec1 libxmlsec1-dev pip install --user .[dev] - name: Run tests - run: ~/.local/bin/pytest -n 2 + run: ~/.local/bin/pytest diff --git a/.gitignore b/.gitignore index ccedbc8..86f3cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -47,12 +47,6 @@ env.d/terraform # npm node_modules -# Mails -src/backend/core/templates/mail/ - -# Typescript client -src/frontend/tsclient - # Swagger **/swagger.json @@ -71,9 +65,6 @@ src/frontend/tsclient db.sqlite3 .mypy_cache -# Site media -/data/ - # IDEs .idea/ .vscode/ diff --git a/Dockerfile b/Dockerfile index 20fbe62..d934a30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,3 @@ -# Django OIDC2FER - # ---- base image to inherit from ---- FROM python:3.11-slim-bookworm as base @@ -10,6 +8,7 @@ RUN apt-get update && \ apt-get install -y \ pkg-config \ gcc \ + xmlsec1 \ libxml2-dev \ libxmlsec1-dev \ libxmlsec1-openssl && \ @@ -24,37 +23,11 @@ FROM base as back-builder WORKDIR /builder # Copy required python dependencies -COPY ./src/backend /builder +COPY ./src/satosa /builder RUN mkdir /install && \ pip install --prefix=/install . -# ---- static link collector ---- -FROM base as link-collector -ARG OIDC2FER_STATIC_ROOT=/data/static - -# Install rdfind -RUN apt-get update && \ - apt-get install -y \ - rdfind && \ - rm -rf /var/lib/apt/lists/* - -# Copy installed python dependencies -COPY --from=back-builder /install /usr/local - -# Copy oidc2fer application (see .dockerignore) -COPY ./src/backend /app/ - -WORKDIR /app - -# collectstatic -RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \ - python manage.py collectstatic --noinput - -# Replace duplicated file by a symlink to decrease the overall size of the -# final image -RUN rdfind -makesymlinks true -followsymlinks true -makeresultsfile false ${OIDC2FER_STATIC_ROOT} - # ---- Core application image ---- FROM base as core @@ -78,7 +51,7 @@ RUN chmod g=u /etc/passwd COPY --from=back-builder /install /usr/local # Copy oidc2fer application (see .dockerignore) -COPY ./src/backend /app/ +COPY ./src/satosa /app/ WORKDIR /app @@ -87,8 +60,22 @@ WORKDIR /app # ID. ENTRYPOINT [ "/usr/local/bin/entrypoint" ] +# ---- Production image ---- +FROM core as production + +# Gunicorn +RUN mkdir -p /usr/local/etc/gunicorn +COPY docker/files/usr/local/etc/gunicorn/satosa.py /usr/local/etc/gunicorn/satosa.py + +# Un-privileged user running the application +ARG DOCKER_USER +USER ${DOCKER_USER} + +# The default command runs gunicorn WSGI server in satosa's main module +CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/satosa.py", "satosa.wsgi:app"] + # ---- Development image ---- -FROM core as development +FROM production as development # Switch back to the root user to install development dependencies USER root:root @@ -101,25 +88,3 @@ RUN pip install -e .[dev] # Restore the un-privileged user running the application ARG DOCKER_USER USER ${DOCKER_USER} - -# Run django development server -CMD python manage.py runserver 0.0.0.0:8000 - -# ---- Production image ---- -FROM core as production - -ARG OIDC2FER_STATIC_ROOT=/data/static - -# Gunicorn -RUN mkdir -p /usr/local/etc/gunicorn -COPY docker/files/usr/local/etc/gunicorn/oidc2fer.py /usr/local/etc/gunicorn/oidc2fer.py - -# Un-privileged user running the application -ARG DOCKER_USER -USER ${DOCKER_USER} - -# Copy statics -COPY --from=link-collector ${OIDC2FER_STATIC_ROOT} ${OIDC2FER_STATIC_ROOT} - -# The default command runs gunicorn WSGI server in oidc2fer's main module -CMD gunicorn -c /usr/local/etc/gunicorn/oidc2fer.py oidc2fer.wsgi:application diff --git a/Makefile b/Makefile index 199a501..4aee6c5 100644 --- a/Makefile +++ b/Makefile @@ -39,35 +39,24 @@ COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev COMPOSE_RUN = $(COMPOSE) run --rm COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev -# -- Backend -MANAGE = $(COMPOSE_RUN_APP) python manage.py - # ============================================================================== # RULES default: help -data/media: - @mkdir -p data/media - -data/static: - @mkdir -p data/static - # -- Project create-env-files: ## Copy the dist env files to env files create-env-files: \ - env.d/development/common + env.d/development/common \ + env.d/development/satosa .PHONY: create-env-files bootstrap: ## Prepare Docker images for the project bootstrap: \ - data/media \ - data/static \ create-env-files \ build \ - run \ - migrate + run .PHONY: bootstrap # -- Docker/compose @@ -122,7 +111,7 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f .PHONY: lint-pylint test: ## run project tests - @$(MAKE) test-back-parallel + @$(MAKE) test-back .PHONY: test test-back: ## run back-end tests @@ -130,34 +119,9 @@ test-back: ## run back-end tests bin/pytest $${args:-${1}} .PHONY: test-back -test-back-parallel: ## run all back-end tests in parallel - @args="$(filter-out $@,$(MAKECMDGOALS))" && \ - bin/pytest -n auto $${args:-${1}} -.PHONY: test-back-parallel - -superuser: ## Create an admin superuser with password "admin" - @echo "$(BOLD)Creating a Django superuser$(RESET)" - @$(MANAGE) createsuperuser --email admin@example.com --password admin -.PHONY: superuser - -shell: ## connect to database shell - @$(MANAGE) shell #_plus -.PHONY: dbshell - -# -- Database - -dbshell: ## connect to database shell - docker compose exec app-dev python manage.py dbshell -.PHONY: dbshell - -resetdb: ## flush database and create a superuser "admin" - @echo "$(BOLD)Flush database$(RESET)" - @$(MANAGE) flush - @${MAKE} superuser -.PHONY: resetdb - env.d/development/common: cp -n env.d/development/common.dist env.d/development/common + cp -n env.d/development/satosa.dist env.d/development/satosa # -- Misc clean: ## restore repository state as it was freshly cloned diff --git a/README.md b/README.md index e002d06..2a23a4b 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,6 @@ Finally, you can check all available Make rules using: $ make help ``` -### Django admin - -You can access the Django admin site at -[http://localhost:8071/admin](http://localhost:8071/admin). - -You first need to create a superuser account: - -```bash -$ make superuser -``` - ## Contributing This project is intended to be community-driven, so please, do not hesitate to diff --git a/bin/_config.sh b/bin/_config.sh index 766a7bf..7fda912 100644 --- a/bin/_config.sh +++ b/bin/_config.sh @@ -84,15 +84,6 @@ function _dc_exec() { _docker_compose exec $user_args "$@" } -# _django_manage: wrap django's manage.py command with docker compose -# -# usage : _django_manage [ARGS...] -# -# ARGS : django's manage.py command arguments -function _django_manage() { - _dc_run "app-dev" python manage.py "$@" -} - # _set_openstack_project: select an OpenStack project from the openrc files defined in the # terraform directory. # diff --git a/bin/manage b/bin/manage deleted file mode 100755 index b6c82d9..0000000 --- a/bin/manage +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck source=bin/_config.sh -source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" - -_django_manage "$@" diff --git a/bin/pylint b/bin/pylint index 8053c7c..924e1da 100755 --- a/bin/pylint +++ b/bin/pylint @@ -27,12 +27,12 @@ do done if [[ -n "${diff_from}" ]]; then - # Run pylint only on modified files located in src/backend - # (excluding deleted files and migration files) + # Run pylint only on modified files located in src/satosa + # (excluding deleted files) # shellcheck disable=SC2207 - paths=($(git diff "${diff_from}" --name-only --diff-filter=d -- src/backend ':!**/migrations/*.py' | grep -E '^src/backend/.*\.py$')) + paths=($(git diff "${diff_from}" --name-only --diff-filter=d -- src/satosa | grep -E '^src/satosa/.*\.py$')) fi # Fix docker vs local path when project sources are mounted as a volume -read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")" +read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/satosa/||g")" _dc_run app-dev pylint "${paths[@]}" "${args[@]}" diff --git a/bin/pytest b/bin/pytest index 8ed0d30..b450535 100755 --- a/bin/pytest +++ b/bin/pytest @@ -3,6 +3,5 @@ source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" _dc_run \ - -e DJANGO_CONFIGURATION=Test \ app-dev \ pytest "$@" diff --git a/docker-compose.yml b/docker-compose.yml index b650b96..98cb47b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,12 @@ services: image: oidc2fer:development environment: - PYLINTHOME=/app/.pylint.d - - DJANGO_CONFIGURATION=Development env_file: - env.d/development/common - ports: - - "8071:8000" + - env.d/development/satosa volumes: - - ./src/backend:/app - - ./data/media:/data/media - - ./data/static:/data/static - + - ./src/satosa:/app + app: build: context: . @@ -27,20 +23,14 @@ services: DOCKER_USER: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000} image: oidc2fer:production - environment: - - DJANGO_CONFIGURATION=Demo env_file: - env.d/development/common - volumes: - - ./data/media:/data/media + - env.d/development/satosa nginx: image: nginx:1.25 ports: + - "8081:8081" - "8082:8082" - - "8088:8088" volumes: - ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro - - ./data/media:/data/media:ro - depends_on: - - app diff --git a/docker/files/etc/nginx/conf.d/default.conf b/docker/files/etc/nginx/conf.d/default.conf index 4bc32ff..08f88fc 100644 --- a/docker/files/etc/nginx/conf.d/default.conf +++ b/docker/files/etc/nginx/conf.d/default.conf @@ -1,17 +1,29 @@ -server { +resolver 127.0.0.11; +server { + listen 8081; + server_name localhost; + charset utf-8; + + location / { + set $backend "http://app:8000"; + proxy_pass $backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} + +server { listen 8082; server_name localhost; charset utf-8; - location /media { - alias /data/media; - } - location / { - proxy_pass http://app:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + set $backend "http://app-dev:8000"; + proxy_pass $backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } diff --git a/docker/files/usr/local/bin/entrypoint b/docker/files/usr/local/bin/entrypoint index aa8e332..1becadd 100755 --- a/docker/files/usr/local/bin/entrypoint +++ b/docker/files/usr/local/bin/entrypoint @@ -31,5 +31,13 @@ if ! whoami > /dev/null 2>&1; then fi fi +echo "🐳(entrypoint) creating config files from environment..." +mkdir -p /tmp +# These files are referenced by the Satosa config files +printenv SAML2_BACKEND_CERT > /tmp/backend.crt +printenv SAML2_BACKEND_KEY > /tmp/backend.key +printenv OIDC_FRONTEND_KEY > /tmp/frontend.key +printenv CLIENT_DB_JSON > /tmp/client_db.json + echo "🐳(entrypoint) running your command: ${*}" exec "$@" diff --git a/docker/files/usr/local/etc/gunicorn/oidc2fer.py b/docker/files/usr/local/etc/gunicorn/satosa.py similarity index 87% rename from docker/files/usr/local/etc/gunicorn/oidc2fer.py rename to docker/files/usr/local/etc/gunicorn/satosa.py index 682bedc..9df1508 100644 --- a/docker/files/usr/local/etc/gunicorn/oidc2fer.py +++ b/docker/files/usr/local/etc/gunicorn/satosa.py @@ -1,6 +1,5 @@ -# Gunicorn-django settings bind = ["0.0.0.0:8000"] -name = "oidc2fer" +name = "satosa" python_path = "/app" # Run diff --git a/env.d/development/common.dist b/env.d/development/common.dist index 3b4ccf5..930ab1f 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -1,8 +1,2 @@ -# Django -DJANGO_ALLOWED_HOSTS=* -DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly -DJANGO_SETTINGS_MODULE=oidc2fer.settings -DJANGO_SUPERUSER_PASSWORD=admin - # Python PYTHONPATH=/app diff --git a/env.d/development/satosa.dist b/env.d/development/satosa.dist new file mode 100644 index 0000000..be718b4 --- /dev/null +++ b/env.d/development/satosa.dist @@ -0,0 +1,27 @@ +STATE_ENCRYPTION_KEY= +SAML2_BACKEND_CERT="-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +" +SAML2_BACKEND_KEY="-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY----- +" +OIDC_FRONTEND_KEY="-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY----- +" +CLIENT_DB_JSON=" + { + \"oidc-test-client\": { + \"response_types\": [ + \"id_token\", + \"code\" + ], + \"redirect_uris\": [ + \"https://oidc-test-client.traefik.me/auth/callback\" + ], + \"client_secret\": \"oidc-test-secret\" + } + } +" diff --git a/src/backend/MANIFEST.in b/src/backend/MANIFEST.in deleted file mode 100644 index db1365e..0000000 --- a/src/backend/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include LICENSE -include README.md -recursive-include src/backend/oidc2fer *.html *.png *.gif *.css *.ico *.jpg *.jpeg *.po *.mo *.eot *.svg *.ttf *.woff *.woff2 diff --git a/src/backend/core/__init__.py b/src/backend/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/core/admin.py b/src/backend/core/admin.py deleted file mode 100644 index bad7348..0000000 --- a/src/backend/core/admin.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Admin classes and registrations for core app.""" -from django.contrib import admin -from django.contrib.auth import admin as auth_admin -from django.utils.translation import gettext_lazy as _ - -from . import models - - -@admin.register(models.User) -class UserAdmin(auth_admin.UserAdmin): - """Admin class for the User model""" - - fieldsets = ( - ( - None, - { - "fields": ( - "id", - "password", - ) - }, - ), - (_("Personal info"), {"fields": ("email", "language", "timezone")}), - ( - _("Permissions"), - { - "fields": ( - "is_active", - "is_device", - "is_staff", - "is_superuser", - "groups", - "user_permissions", - ), - }, - ), - (_("Important dates"), {"fields": ("created_at", "updated_at")}), - ) - add_fieldsets = ( - ( - None, - { - "classes": ("wide",), - "fields": ("email", "password1", "password2"), - }, - ), - ) - list_display = ( - "id", - "email", - "is_active", - "is_staff", - "is_superuser", - "is_device", - "created_at", - "updated_at", - ) - list_filter = ("is_staff", "is_superuser", "is_device", "is_active") - ordering = ("is_active", "-is_superuser", "-is_staff", "-is_device", "-updated_at") - readonly_fields = ("id", "email", "created_at", "updated_at") - search_fields = ("id", "email") diff --git a/src/backend/core/factories.py b/src/backend/core/factories.py deleted file mode 100644 index 42c9ccc..0000000 --- a/src/backend/core/factories.py +++ /dev/null @@ -1,24 +0,0 @@ -# ruff: noqa: S311 -""" -Core application factories -""" -from django.conf import settings -from django.contrib.auth.hashers import make_password - -import factory.fuzzy -from faker import Faker - -from core import models - -fake = Faker() - - -class UserFactory(factory.django.DjangoModelFactory): - """A factory to random users for testing purposes.""" - - class Meta: - model = models.User - - email = factory.Faker("email") - language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES]) - password = make_password("password") diff --git a/src/backend/core/migrations/0001_initial.py b/src/backend/core/migrations/0001_initial.py deleted file mode 100644 index 2aa42db..0000000 --- a/src/backend/core/migrations/0001_initial.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-09 22:04 - -import django.contrib.auth.models -import timezone_field.fields -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), - ('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')), - ('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)), - ('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')), - ('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'db_table': 'oidc2fer_user', - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/src/backend/core/migrations/__init__.py b/src/backend/core/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/core/models.py b/src/backend/core/models.py deleted file mode 100644 index 653c8d8..0000000 --- a/src/backend/core/models.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Declare and configure the models for the oidc2fer core application -""" -import uuid - -from django.conf import settings -from django.contrib.auth import models as auth_models -from django.contrib.auth.base_user import AbstractBaseUser -from django.db import models -from django.utils.functional import lazy -from django.utils.translation import gettext_lazy as _ - -from timezone_field import TimeZoneField - - -class BaseModel(models.Model): - """ - Serves as an abstract base model for other models, ensuring that records are validated - before saving as Django doesn't do it by default. - - Includes fields common to all models: a UUID primary key and creation/update timestamps. - """ - - id = models.UUIDField( - verbose_name=_("id"), - help_text=_("primary key for the record as UUID"), - primary_key=True, - default=uuid.uuid4, - editable=False, - ) - created_at = models.DateTimeField( - verbose_name=_("created on"), - help_text=_("date and time at which a record was created"), - auto_now_add=True, - editable=False, - ) - updated_at = models.DateTimeField( - verbose_name=_("updated on"), - help_text=_("date and time at which a record was last updated"), - auto_now=True, - editable=False, - ) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - """Call `full_clean` before saving.""" - self.full_clean() - super().save(*args, **kwargs) - - -class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): - """User model.""" - - email = models.EmailField(_("email address"), unique=True) - language = models.CharField( - max_length=10, - choices=lazy(lambda: settings.LANGUAGES, tuple)(), - default=settings.LANGUAGE_CODE, - verbose_name=_("language"), - help_text=_("The language in which the user wants to see the interface."), - ) - timezone = TimeZoneField( - choices_display="WITH_GMT_OFFSET", - use_pytz=False, - default=settings.TIME_ZONE, - help_text=_("The timezone in which the user wants to see times."), - ) - is_device = models.BooleanField( - _("device"), - default=False, - help_text=_("Whether the user is a device or a real user."), - ) - is_staff = models.BooleanField( - _("staff status"), - default=False, - help_text=_("Whether the user can log into this admin site."), - ) - is_active = models.BooleanField( - _("active"), - default=True, - help_text=_( - "Whether this user should be treated as active. " - "Unselect this instead of deleting accounts." - ), - ) - - objects = auth_models.UserManager() - - USERNAME_FIELD = "email" - REQUIRED_FIELDS = [] - - class Meta: - db_table = "oidc2fer_user" - verbose_name = _("user") - verbose_name_plural = _("users") - - def __str__(self): - return self.email diff --git a/src/backend/core/tests/__init__.py b/src/backend/core/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/demo/__init__.py b/src/backend/demo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/demo/management/__init__.py b/src/backend/demo/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/demo/management/commands/__init__.py b/src/backend/demo/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/demo/management/commands/createsuperuser.py b/src/backend/demo/management/commands/createsuperuser.py deleted file mode 100644 index e38c314..0000000 --- a/src/backend/demo/management/commands/createsuperuser.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Management user to create a superuser.""" -from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand - -UserModel = get_user_model() - - -class Command(BaseCommand): - """Management command to create a superuser from and email and password.""" - - help = "Create a superuser with an email and a password" - - def add_arguments(self, parser): - """Define required arguments "email" and "password".""" - parser.add_argument( - "--email", - help=("Email for the user."), - ) - parser.add_argument( - "--password", - help="Password for the user.", - ) - - def handle(self, *args, **options): - """ - Given an email and a password, create a superuser or upgrade the existing - user to superuser status. - """ - email = options.get("email") - try: - user = UserModel.objects.get(email=email) - except UserModel.DoesNotExist: - user = UserModel(email=email) - message = "Superuser created successfully." - else: - if user.is_superuser and user.is_staff: - message = "Superuser already exists." - else: - message = "User already existed and was upgraded to superuser." - - user.is_superuser = True - user.is_staff = True - user.set_password(options["password"]) - user.save() - - self.stdout.write(self.style.SUCCESS(message)) diff --git a/src/backend/manage.py b/src/backend/manage.py deleted file mode 100644 index 703424d..0000000 --- a/src/backend/manage.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -""" -OIDC2FER's sandbox management script. -""" -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oidc2fer.settings") - os.environ.setdefault("DJANGO_CONFIGURATION", "Development") - - from configurations.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/src/backend/oidc2fer/__init__.py b/src/backend/oidc2fer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/oidc2fer/settings.py b/src/backend/oidc2fer/settings.py deleted file mode 100755 index c02b50f..0000000 --- a/src/backend/oidc2fer/settings.py +++ /dev/null @@ -1,422 +0,0 @@ -""" -Django's settings for OIDC2FER project. - -Generated by 'django-admin startproject' using Django 5.0.2. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.0/ref/settings/ -""" -import json -import os - -from django.utils.translation import gettext_lazy as _ - -import sentry_sdk -from configurations import Configuration, values -from sentry_sdk.integrations.django import DjangoIntegration - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DATA_DIR = os.path.join("/", "data") - - -def get_release(): - """ - Get the current release of the application - - By release, we mean the release from the version.json file à la Mozilla [1] - (if any). If this file has not been found, it defaults to "NA". - - [1] - https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md - """ - # Try to get the current release from the version.json file generated by the - # CI during the Docker image build - try: - with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version: - return json.load(version)["version"] - except FileNotFoundError: - return "NA" # Default: not available - - -class Base(Configuration): - """ - This is the base configuration every configuration (aka environment) should inherit from. It - is recommended to configure third-party applications by creating a configuration mixins in - ./configurations and compose the Base configuration with those mixins. - - It depends on an environment variable that SHOULD be defined: - - * DJANGO_SECRET_KEY - - You may also want to override default configuration by setting the following environment - variables: - - * DJANGO_SENTRY_DSN - * DB_NAME - * DB_HOST - * DB_PASSWORD - * DB_USER - """ - - DEBUG = False - - # Security - ALLOWED_HOSTS = values.ListValue([]) - SECRET_KEY = values.Value(None) - - # Application definition - ROOT_URLCONF = "oidc2fer.urls" - WSGI_APPLICATION = "oidc2fer.wsgi.application" - - # Database - DATABASES = { - "default": { - "ENGINE": values.Value( - "django.db.backends.postgresql_psycopg2", - environ_name="DB_ENGINE", - environ_prefix=None, - ), - "NAME": values.Value( - "oidc2fer", environ_name="DB_NAME", environ_prefix=None - ), - "USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None), - "PASSWORD": values.Value( - "pass", environ_name="DB_PASSWORD", environ_prefix=None - ), - "HOST": values.Value( - "localhost", environ_name="DB_HOST", environ_prefix=None - ), - "PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None), - } - } - DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - - # Static files (CSS, JavaScript, Images) - STATIC_URL = "/static/" - STATIC_ROOT = os.path.join(DATA_DIR, "static") - MEDIA_URL = "/media/" - MEDIA_ROOT = os.path.join(DATA_DIR, "media") - - SITE_ID = 1 - - STORAGES = { - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - # Internationalization - # https://docs.djangoproject.com/en/3.1/topics/i18n/ - - # Languages - LANGUAGE_CODE = values.Value("en-us") - - # Careful! Languages should be ordered by priority, as this tuple is used to get - # fallback/default languages throughout the app. - LANGUAGES = values.SingleNestedTupleValue( - ( - ("en-us", _("English")), - ("fr-fr", _("French")), - ) - ) - - LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) - - TIME_ZONE = "UTC" - USE_I18N = True - USE_TZ = True - - # Templates - TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], - "OPTIONS": { - "context_processors": [ - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.csrf", - "django.template.context_processors.debug", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.request", - "django.template.context_processors.tz", - ], - "loaders": [ - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", - ], - }, - }, - ] - - MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.locale.LocaleMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "corsheaders.middleware.CorsMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "dockerflow.django.middleware.DockerflowMiddleware", - ] - - AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - ] - - # Django's applications from the highest priority to the lowest - INSTALLED_APPS = [ - # People - "core", - "demo", - # Third party apps - "social_edu_federation.django.apps.PythonSocialEduFedAuthConfig", - "corsheaders", - "dockerflow.django", - # Django - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.postgres", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.staticfiles", - ] - - # Cache - CACHES = { - "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, - } - - AUTH_USER_MODEL = "core.User" - - # CORS - CORS_ALLOW_CREDENTIALS = True - CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False) - CORS_ALLOWED_ORIGINS = values.ListValue([]) - CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([]) - - # Sentry - SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN") - - # Session - SESSION_ENGINE = "django.contrib.sessions.backends.cache" - SESSION_COOKIE_AGE = 60 * 60 * 12 # 12 hours to match Agent Connect - - # pylint: disable=invalid-name - @property - def ENVIRONMENT(self): - """Environment in which the application is launched.""" - return self.__class__.__name__.lower() - - # pylint: disable=invalid-name - @property - def RELEASE(self): - """ - Return the release information. - - Delegate to the module function to enable easier testing. - """ - return get_release() - - @classmethod - def post_setup(cls): - """Post setup configuration. - This is the place where you can configure settings that require other - settings to be loaded. - """ - super().post_setup() - - # The SENTRY_DSN setting should be available to activate sentry for an environment - if cls.SENTRY_DSN is not None: - sentry_sdk.init( - dsn=cls.SENTRY_DSN, - environment=cls.__name__.lower(), - release=get_release(), - integrations=[DjangoIntegration()], - ) - with sentry_sdk.configure_scope() as scope: - scope.set_extra("application", "backend") - - -class Build(Base): - """Settings used when the application is built. - - This environment should not be used to run the application. Just to build it with non-blocking - settings. - """ - - SECRET_KEY = values.Value("DummyKey") - STORAGES = { - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - "staticfiles": { - "BACKEND": values.Value( - "whitenoise.storage.CompressedManifestStaticFilesStorage", - environ_name="STORAGES_STATICFILES_BACKEND", - ), - }, - } - - -class Development(Base): - """ - Development environment settings - - We set DEBUG to True and configure the server to respond from all hosts. - """ - - ALLOWED_HOSTS = ["*"] - CORS_ALLOW_ALL_ORIGINS = True - CSRF_TRUSTED_ORIGINS = ["http://localhost:8071"] - DEBUG = True - - SESSION_COOKIE_NAME = "oidc2fer_sessionid" - - USE_SWAGGER = True - - def __init__(self): - # pylint: disable=invalid-name - self.INSTALLED_APPS += ["django_extensions"] - - -class Test(Base): - """Test environment settings""" - - LOGGING = values.DictValue( - { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "console": { - "class": "logging.StreamHandler", - }, - }, - "loggers": { - "oidc2fer": { - "handlers": ["console"], - "level": "DEBUG", - }, - }, - } - ) - PASSWORD_HASHERS = [ - "django.contrib.auth.hashers.MD5PasswordHasher", - ] - USE_SWAGGER = True - - STORAGES = { - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - -class ContinuousIntegration(Test): - """ - Continuous Integration environment settings - - nota bene: it should inherit from the Test environment. - """ - - -class Production(Base): - """ - Production environment settings - - You must define the ALLOWED_HOSTS environment variable in Production - configuration (and derived configurations): - ALLOWED_HOSTS=["foo.com", "foo.fr"] - """ - - # Security - ALLOWED_HOSTS = values.ListValue(None) - CSRF_TRUSTED_ORIGINS = values.ListValue([]) - SECURE_BROWSER_XSS_FILTER = True - SECURE_CONTENT_TYPE_NOSNIFF = True - - # SECURE_PROXY_SSL_HEADER allows to fix the scheme in Django's HttpRequest - # object when your application is behind a reverse proxy. - # - # Keep this SECURE_PROXY_SSL_HEADER configuration only if : - # - your Django app is behind a proxy. - # - your proxy strips the X-Forwarded-Proto header from all incoming requests - # - Your proxy sets the X-Forwarded-Proto header and sends it to Django - # - # In other cases, you should comment the following line to avoid security issues. - # SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - - # Modern browsers require to have the `secure` attribute on cookies with `Samesite=none` - CSRF_COOKIE_SECURE = True - SESSION_COOKIE_SECURE = True - - # For static files in production, we want to use a backend that includes a hash in - # the filename, that is calculated from the file content, so that browsers always - # get the updated version of each file. - STORAGES = { - "default": { - "BACKEND": "storages.backends.s3.S3Storage", - }, - "staticfiles": { - # For static files in production, we want to use a backend that includes a hash in - # the filename, that is calculated from the file content, so that browsers always - # get the updated version of each file. - "BACKEND": values.Value( - "whitenoise.storage.CompressedManifestStaticFilesStorage", - environ_name="STORAGES_STATICFILES_BACKEND", - ) - }, - } - - # Privacy - SECURE_REFERRER_POLICY = "same-origin" - - -class Feature(Production): - """ - Feature environment settings - - nota bene: it should inherit from the Production environment. - """ - - -class Staging(Production): - """ - Staging environment settings - - nota bene: it should inherit from the Production environment. - """ - - -class PreProduction(Production): - """ - Pre-production environment settings - - nota bene: it should inherit from the Production environment. - """ - - -class Demo(Production): - """ - Demonstration environment settings - - nota bene: it should inherit from the Production environment. - """ - - CSRF_TRUSTED_ORIGINS = ["http://localhost:8082"] - - STORAGES = { - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } diff --git a/src/backend/oidc2fer/urls.py b/src/backend/oidc2fer/urls.py deleted file mode 100644 index 0fe5937..0000000 --- a/src/backend/oidc2fer/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -"""People URL Configuration""" - -from django.conf import settings -from django.conf.urls.static import static -from django.contrib import admin -from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.urls import path - -urlpatterns = [ - path("admin/", admin.site.urls), -] - -if settings.DEBUG: - urlpatterns = ( - urlpatterns - + staticfiles_urlpatterns() - + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - ) diff --git a/src/backend/oidc2fer/wsgi.py b/src/backend/oidc2fer/wsgi.py deleted file mode 100644 index 5c16325..0000000 --- a/src/backend/oidc2fer/wsgi.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -WSGI config for the People project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ -""" - -import os - -from configurations.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oidc2fer.settings") -os.environ.setdefault("DJANGO_CONFIGURATION", "Development") - -application = get_wsgi_application() diff --git a/src/backend/.pylintrc b/src/satosa/.pylintrc similarity index 99% rename from src/backend/.pylintrc rename to src/satosa/.pylintrc index 4416e3e..ac7e7e7 100644 --- a/src/backend/.pylintrc +++ b/src/satosa/.pylintrc @@ -23,7 +23,7 @@ jobs=0 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins=pylint_django,pylint.extensions.no_self_use +load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes @@ -57,7 +57,6 @@ confidence= # --disable=W" disable=bad-inline-option, deprecated-pragma, - django-not-configured, file-ignored, locally-disabled, no-self-use, diff --git a/src/backend/__init__.py b/src/satosa/__init__.py similarity index 100% rename from src/backend/__init__.py rename to src/satosa/__init__.py diff --git a/src/satosa/internal_attributes.yaml b/src/satosa/internal_attributes.yaml new file mode 100644 index 0000000..c69401a --- /dev/null +++ b/src/satosa/internal_attributes.yaml @@ -0,0 +1,46 @@ +attributes: + address: + openid: + - address.street_address + saml: + - postaladdress + displayname: + openid: + - nickname + saml: + - displayName + edupersontargetedid: + openid: + - sub + saml: + - eduPersonTargetedID + givenname: + openid: + - given_name + saml: + - firstName + - givenName + mail: + openid: + - email + saml: + - email + - emailAddress + - mail + name: + openid: + - name + saml: + - cn + surname: + openid: + - family_name + saml: + - sn + - surname + - lastName + organization: + saml: + - o + openid: + - organization diff --git a/src/satosa/plugins/backends/saml2_backend.yaml b/src/satosa/plugins/backends/saml2_backend.yaml new file mode 100644 index 0000000..1d5964c --- /dev/null +++ b/src/satosa/plugins/backends/saml2_backend.yaml @@ -0,0 +1,53 @@ +module: satosa.backends.saml2.SAMLBackend +name: Saml2 +config: + disco_srv: https://discovery.renater.fr/test/ + entityid_endpoint: true + mirror_force_authn: 'no' + memorize_idp: 'no' + use_memorized_idp_when_force_authn: 'no' + send_requester_id: 'no' + enable_metadata_reload: 'no' + acs_selection_strategy: prefer_matching_host + sp_config: + name: Passerelle OIDC vers FER + description: SP Description + key_file: /tmp/backend.key + cert_file: /tmp/backend.crt + organization: + name: DINUM + display_name: DINUM + url: https://beta.gouv.fr/incubateurs/dinum_produits_interministeriels.html + metadata: + # mdq: + # - url: https://mdq.federation.renater.fr + remote: + - url: https://pub.federation.renater.fr/metadata/test/preview/preview-idps-test-metadata.xml + entityid: //proxy_saml2_backend.xml + accepted_time_diff: 60 + allow_unknown_attributes: true + service: + sp: + ui_info: + display_name: + - lang: fr + text: Passerelle OIDC vers FER + logo: + text: https://beta.gouv.fr/img/incubators/logo_dinum.png + width: '200' + height: '200' + authn_requests_signed: true + want_response_signed: true + allow_unsolicited: true + name_id_format: + - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + - urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified + endpoints: + assertion_consumer_service: + - - //acs/post + - urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST + discovery_response: + - - //disco + - 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol' + + name_id_format_allow_create: true diff --git a/src/satosa/plugins/frontends/openid_connect_frontend.yaml b/src/satosa/plugins/frontends/openid_connect_frontend.yaml new file mode 100644 index 0000000..9c2a3d1 --- /dev/null +++ b/src/satosa/plugins/frontends/openid_connect_frontend.yaml @@ -0,0 +1,44 @@ +module: satosa.frontends.openid_connect.OpenIDConnectFrontend +name: OIDC +config: + signing_key_path: /tmp/frontend.key + signing_key_id: frontend.key1 + + # Defines the database connection URI for the databases: + # - authz_code_db + # - access_token_db + # - refresh_token_db + # - sub_db + # - user_db + # + # supported storage backends: + # - In-memory dictionary + # - MongoDB (e.g. mongodb://db.example.com) + # - Redis (e.g. redis://example/0) + # - Stateless (eg. stateless://user:encryptionkey?alg=aes256) + # + # This configuration is optional. + # By default, the in-memory storage is used. + # db_uri: mongodb://db.example.com + + # Where to store clients. + # + # If client_db_uri is set, the database connection is used. + # Otherwise, if client_db_path is set, the JSON file is used. + # By default, an in-memory dictionary is used. + # client_db_uri: mongodb://db.example.com + client_db_path: /tmp/client_db.json + + # if not specified, it is randomly generated on every startup + sub_hash_salt: randomSALTvalue + sub_mirror_public: yes + + provider: + client_registration_supported: no + response_types_supported: ["code", "id_token token"] + subject_types_supported: ["public"] + scopes_supported: ["openid", "email"] + id_token_lifetime: 3600 + extra_id_token_claims: + oidc-test-client: + - organization diff --git a/src/satosa/plugins/frontends/ping_frontend.yaml b/src/satosa/plugins/frontends/ping_frontend.yaml new file mode 100644 index 0000000..c09b218 --- /dev/null +++ b/src/satosa/plugins/frontends/ping_frontend.yaml @@ -0,0 +1,3 @@ +module: satosa.frontends.ping.PingFrontend +name: ping +config: null diff --git a/src/satosa/plugins/microservices/static_attributes.yaml b/src/satosa/plugins/microservices/static_attributes.yaml new file mode 100644 index 0000000..d4b0732 --- /dev/null +++ b/src/satosa/plugins/microservices/static_attributes.yaml @@ -0,0 +1,13 @@ +module: satosa.micro_services.attribute_modifications.AddStaticAttributes +name: AddAttributes +config: + static_attributes: + organisation: Example Org. + schachomeorganization: example.com + schachomeorganizationtype: + - urn:schac:homeOrganizationType:eu:higherEducationInstitution + - urn:schac:homeOrganizationType:eu:educationInstitution + organizationname: Example Organization + noreduorgacronym: EO + countryname: SE + friendlycountryname: Sweden diff --git a/src/satosa/proxy_conf.yaml b/src/satosa/proxy_conf.yaml new file mode 100644 index 0000000..2dcaf6c --- /dev/null +++ b/src/satosa/proxy_conf.yaml @@ -0,0 +1,41 @@ +BASE: !ENV BASE_URL +COOKIE_STATE_NAME: SATOSA_STATE +CONTEXT_STATE_DELETE: 'yes' +STATE_ENCRYPTION_KEY: !ENV STATE_ENCRYPTION_KEY +cookies_samesite_compat: + - - SATOSA_STATE + - SATOSA_STATE_LEGACY +INTERNAL_ATTRIBUTES: internal_attributes.yaml +BACKEND_MODULES: + - plugins/backends/saml2_backend.yaml +FRONTEND_MODULES: + - plugins/frontends/openid_connect_frontend.yaml + - plugins/frontends/ping_frontend.yaml +MICRO_SERVICES: + - plugins/microservices/static_attributes.yaml +LOGGING: + version: 1 + formatters: + simple: + format: '[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s] %(message)s' + handlers: + stdout: + class: logging.StreamHandler + stream: ext://sys.stdout + level: DEBUG + formatter: simple + loggers: + satosa: + level: DEBUG + saml2: + level: DEBUG + oidcendpoint: + level: DEBUG + pyop: + level: DEBUG + oic: + level: DEBUG + root: + level: DEBUG + handlers: + - stdout diff --git a/src/backend/pyproject.toml b/src/satosa/pyproject.toml similarity index 67% rename from src/backend/pyproject.toml rename to src/satosa/pyproject.toml index 1bfc6b5..ac929bf 100644 --- a/src/backend/pyproject.toml +++ b/src/satosa/pyproject.toml @@ -11,8 +11,6 @@ version = "0.1.0" authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] classifiers = [ "Development Status :: 5 - Production/Stable", - "Framework :: Django", - "Framework :: Django :: 5", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", @@ -20,25 +18,13 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] description = "An application to handle contacts and teams." -keywords = ["Django", "OIDC", "SAML", "Shibboleth", "FER", "RENATER"] +keywords = ["OIDC", "SAML", "Shibboleth", "FER", "RENATER"] license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.11" dependencies = [ - "django-configurations==2.5", - "django-cors-headers==4.3.1", - "django==5.0.2", - "django-timezone-field>=5.1", - "dockerflow==2024.2.0", - "factory_boy==3.3.0", + "SATOSA==8.4.0", "gunicorn==21.2.0", - "psycopg[binary]==3.1.18", - "requests==2.31.0", - "sentry-sdk==1.40.6", - "social-auth-app-django==5.4.0", - "social-auth-core[saml]==4.5.3", - "social-edu-federation==2.1.1", - "whitenoise==6.6.0", ] [project.urls] @@ -49,20 +35,10 @@ dependencies = [ [project.optional-dependencies] dev = [ - "django-extensions==3.2.3", - "ipdb==0.13.13", - "ipython==8.22.1", - "pyfakefs==5.3.5", - "pylint-django==2.5.5", "pylint==3.1.0", "pytest-cov==4.1.0", - "pytest-django==4.8.0", "pytest==8.0.2", - "pytest-icdiff==0.9", - "pytest-xdist==3.5.0", - "responses==0.25.0", "ruff==0.2.2", - "types-requests==2.31.0.20240218", ] [tool.setuptools] @@ -79,7 +55,6 @@ exclude = [ "build", "venv", "__pycache__", - "*/migrations/*", ] line-length = 88 @@ -90,7 +65,6 @@ select = [ "B", # flake8-bugbear "BLE", # flake8-blind-except "C4", # flake8-comprehensions - "DJ", # flake8-django "I", # isort "PLC", # pylint-convention "PLE", # pylint-error @@ -104,8 +78,8 @@ select = [ ] [tool.ruff.lint.isort] -section-order = ["future","standard-library","django","third-party","oidc2fer","first-party","local-folder"] -sections = { oidc2fer=["core"], django=["django"] } +section-order = ["future","standard-library","third-party","oidc2fer","first-party","local-folder"] +sections = { oidc2fer=["core"] } [tool.ruff.lint.per-file-ignores] "**/tests/*" = ["S", "SLF"] diff --git a/src/backend/setup.py b/src/satosa/setup.py similarity index 100% rename from src/backend/setup.py rename to src/satosa/setup.py diff --git a/src/satosa/tests/test_dummy.py b/src/satosa/tests/test_dummy.py new file mode 100644 index 0000000..566cf95 --- /dev/null +++ b/src/satosa/tests/test_dummy.py @@ -0,0 +1,6 @@ +import pytest + + +def test_dummy(): + actual = "hello" + assert actual == "hello"