🏗️(common) replace Django app with SATOSA
We mostly import SATOSA and launch its WSGI app.
This commit is contained in:
@@ -20,7 +20,6 @@ docs
|
||||
*.log
|
||||
|
||||
# Development/test cache & configurations
|
||||
data
|
||||
.cache
|
||||
.circleci
|
||||
.git
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
+18
-53
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_django_manage "$@"
|
||||
+4
-4
@@ -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[@]}"
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_dc_run \
|
||||
-e DJANGO_CONFIGURATION=Test \
|
||||
app-dev \
|
||||
pytest "$@"
|
||||
|
||||
+5
-15
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
# Gunicorn-django settings
|
||||
bind = ["0.0.0.0:8000"]
|
||||
name = "oidc2fer"
|
||||
name = "satosa"
|
||||
python_path = "/app"
|
||||
|
||||
# Run
|
||||
@@ -1,8 +1,2 @@
|
||||
# Django
|
||||
DJANGO_ALLOWED_HOSTS=*
|
||||
DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
DJANGO_SETTINGS_MODULE=oidc2fer.settings
|
||||
DJANGO_SUPERUSER_PASSWORD=admin
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
@@ -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\"
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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: <base_url>/<name>/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:
|
||||
- - <base_url>/<name>/acs/post
|
||||
- urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
|
||||
discovery_response:
|
||||
- - <base_url>/<name>/disco
|
||||
- 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol'
|
||||
|
||||
name_id_format_allow_create: true
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
module: satosa.frontends.ping.PingFrontend
|
||||
name: ping
|
||||
config: null
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_dummy():
|
||||
actual = "hello"
|
||||
assert actual == "hello"
|
||||
Reference in New Issue
Block a user