🏗️(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
|
*.log
|
||||||
|
|
||||||
# Development/test cache & configurations
|
# Development/test cache & configurations
|
||||||
data
|
|
||||||
.cache
|
.cache
|
||||||
.circleci
|
.circleci
|
||||||
.git
|
.git
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: src/backend
|
working-directory: src/satosa
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
- name: Install development dependencies
|
- name: Install development dependencies
|
||||||
run: |
|
run: |
|
||||||
# Python's xmlsec requirement
|
# 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]
|
pip install --user .[dev]
|
||||||
- name: Check code formatting with ruff
|
- name: Check code formatting with ruff
|
||||||
run: ~/.local/bin/ruff format . --diff
|
run: ~/.local/bin/ruff format . --diff
|
||||||
@@ -86,21 +86,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: src/backend
|
working-directory: src/satosa
|
||||||
|
|
||||||
env:
|
|
||||||
DJANGO_CONFIGURATION: Test
|
|
||||||
DJANGO_SETTINGS_MODULE: oidc2fer.settings
|
|
||||||
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
|
||||||
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Create writable /data
|
|
||||||
run: |
|
|
||||||
sudo mkdir -p /data/media && \
|
|
||||||
sudo mkdir -p /data/static
|
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
@@ -108,7 +98,7 @@ jobs:
|
|||||||
- name: Install development dependencies
|
- name: Install development dependencies
|
||||||
run: |
|
run: |
|
||||||
# Python's xmlsec requirement
|
# 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]
|
pip install --user .[dev]
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ~/.local/bin/pytest -n 2
|
run: ~/.local/bin/pytest
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ env.d/terraform
|
|||||||
# npm
|
# npm
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Mails
|
|
||||||
src/backend/core/templates/mail/
|
|
||||||
|
|
||||||
# Typescript client
|
|
||||||
src/frontend/tsclient
|
|
||||||
|
|
||||||
# Swagger
|
# Swagger
|
||||||
**/swagger.json
|
**/swagger.json
|
||||||
|
|
||||||
@@ -71,9 +65,6 @@ src/frontend/tsclient
|
|||||||
db.sqlite3
|
db.sqlite3
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
|
|
||||||
# Site media
|
|
||||||
/data/
|
|
||||||
|
|
||||||
# IDEs
|
# IDEs
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
+18
-53
@@ -1,5 +1,3 @@
|
|||||||
# Django OIDC2FER
|
|
||||||
|
|
||||||
# ---- base image to inherit from ----
|
# ---- base image to inherit from ----
|
||||||
FROM python:3.11-slim-bookworm as base
|
FROM python:3.11-slim-bookworm as base
|
||||||
|
|
||||||
@@ -10,6 +8,7 @@ RUN apt-get update && \
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
gcc \
|
gcc \
|
||||||
|
xmlsec1 \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxmlsec1-dev \
|
libxmlsec1-dev \
|
||||||
libxmlsec1-openssl && \
|
libxmlsec1-openssl && \
|
||||||
@@ -24,37 +23,11 @@ FROM base as back-builder
|
|||||||
WORKDIR /builder
|
WORKDIR /builder
|
||||||
|
|
||||||
# Copy required python dependencies
|
# Copy required python dependencies
|
||||||
COPY ./src/backend /builder
|
COPY ./src/satosa /builder
|
||||||
|
|
||||||
RUN mkdir /install && \
|
RUN mkdir /install && \
|
||||||
pip install --prefix=/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 ----
|
# ---- Core application image ----
|
||||||
FROM base as core
|
FROM base as core
|
||||||
|
|
||||||
@@ -78,7 +51,7 @@ RUN chmod g=u /etc/passwd
|
|||||||
COPY --from=back-builder /install /usr/local
|
COPY --from=back-builder /install /usr/local
|
||||||
|
|
||||||
# Copy oidc2fer application (see .dockerignore)
|
# Copy oidc2fer application (see .dockerignore)
|
||||||
COPY ./src/backend /app/
|
COPY ./src/satosa /app/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -87,8 +60,22 @@ WORKDIR /app
|
|||||||
# ID.
|
# ID.
|
||||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
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 ----
|
# ---- Development image ----
|
||||||
FROM core as development
|
FROM production as development
|
||||||
|
|
||||||
# Switch back to the root user to install development dependencies
|
# Switch back to the root user to install development dependencies
|
||||||
USER root:root
|
USER root:root
|
||||||
@@ -101,25 +88,3 @@ RUN pip install -e .[dev]
|
|||||||
# Restore the un-privileged user running the application
|
# Restore the un-privileged user running the application
|
||||||
ARG DOCKER_USER
|
ARG DOCKER_USER
|
||||||
USER ${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 = $(COMPOSE) run --rm
|
||||||
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
|
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
|
||||||
|
|
||||||
# -- Backend
|
|
||||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RULES
|
# RULES
|
||||||
|
|
||||||
default: help
|
default: help
|
||||||
|
|
||||||
data/media:
|
|
||||||
@mkdir -p data/media
|
|
||||||
|
|
||||||
data/static:
|
|
||||||
@mkdir -p data/static
|
|
||||||
|
|
||||||
# -- Project
|
# -- Project
|
||||||
|
|
||||||
create-env-files: ## Copy the dist env files to env files
|
create-env-files: ## Copy the dist env files to env files
|
||||||
create-env-files: \
|
create-env-files: \
|
||||||
env.d/development/common
|
env.d/development/common \
|
||||||
|
env.d/development/satosa
|
||||||
.PHONY: create-env-files
|
.PHONY: create-env-files
|
||||||
|
|
||||||
bootstrap: ## Prepare Docker images for the project
|
bootstrap: ## Prepare Docker images for the project
|
||||||
bootstrap: \
|
bootstrap: \
|
||||||
data/media \
|
|
||||||
data/static \
|
|
||||||
create-env-files \
|
create-env-files \
|
||||||
build \
|
build \
|
||||||
run \
|
run
|
||||||
migrate
|
|
||||||
.PHONY: bootstrap
|
.PHONY: bootstrap
|
||||||
|
|
||||||
# -- Docker/compose
|
# -- Docker/compose
|
||||||
@@ -122,7 +111,7 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
|
|||||||
.PHONY: lint-pylint
|
.PHONY: lint-pylint
|
||||||
|
|
||||||
test: ## run project tests
|
test: ## run project tests
|
||||||
@$(MAKE) test-back-parallel
|
@$(MAKE) test-back
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
||||||
test-back: ## run back-end tests
|
test-back: ## run back-end tests
|
||||||
@@ -130,34 +119,9 @@ test-back: ## run back-end tests
|
|||||||
bin/pytest $${args:-${1}}
|
bin/pytest $${args:-${1}}
|
||||||
.PHONY: test-back
|
.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:
|
env.d/development/common:
|
||||||
cp -n env.d/development/common.dist 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
|
# -- Misc
|
||||||
clean: ## restore repository state as it was freshly cloned
|
clean: ## restore repository state as it was freshly cloned
|
||||||
|
|||||||
@@ -47,17 +47,6 @@ Finally, you can check all available Make rules using:
|
|||||||
$ make help
|
$ 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
|
## Contributing
|
||||||
|
|
||||||
This project is intended to be community-driven, so please, do not hesitate to
|
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 "$@"
|
_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
|
# _set_openstack_project: select an OpenStack project from the openrc files defined in the
|
||||||
# terraform directory.
|
# 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
|
done
|
||||||
|
|
||||||
if [[ -n "${diff_from}" ]]; then
|
if [[ -n "${diff_from}" ]]; then
|
||||||
# Run pylint only on modified files located in src/backend
|
# Run pylint only on modified files located in src/satosa
|
||||||
# (excluding deleted files and migration files)
|
# (excluding deleted files)
|
||||||
# shellcheck disable=SC2207
|
# 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
|
fi
|
||||||
|
|
||||||
# Fix docker vs local path when project sources are mounted as a volume
|
# 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[@]}"
|
_dc_run app-dev pylint "${paths[@]}" "${args[@]}"
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||||
|
|
||||||
_dc_run \
|
_dc_run \
|
||||||
-e DJANGO_CONFIGURATION=Test \
|
|
||||||
app-dev \
|
app-dev \
|
||||||
pytest "$@"
|
pytest "$@"
|
||||||
|
|||||||
+5
-15
@@ -9,16 +9,12 @@ services:
|
|||||||
image: oidc2fer:development
|
image: oidc2fer:development
|
||||||
environment:
|
environment:
|
||||||
- PYLINTHOME=/app/.pylint.d
|
- PYLINTHOME=/app/.pylint.d
|
||||||
- DJANGO_CONFIGURATION=Development
|
|
||||||
env_file:
|
env_file:
|
||||||
- env.d/development/common
|
- env.d/development/common
|
||||||
ports:
|
- env.d/development/satosa
|
||||||
- "8071:8000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/backend:/app
|
- ./src/satosa:/app
|
||||||
- ./data/media:/data/media
|
|
||||||
- ./data/static:/data/static
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -27,20 +23,14 @@ services:
|
|||||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||||
user: ${DOCKER_USER:-1000}
|
user: ${DOCKER_USER:-1000}
|
||||||
image: oidc2fer:production
|
image: oidc2fer:production
|
||||||
environment:
|
|
||||||
- DJANGO_CONFIGURATION=Demo
|
|
||||||
env_file:
|
env_file:
|
||||||
- env.d/development/common
|
- env.d/development/common
|
||||||
volumes:
|
- env.d/development/satosa
|
||||||
- ./data/media:/data/media
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.25
|
image: nginx:1.25
|
||||||
ports:
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
- "8082:8082"
|
- "8082:8082"
|
||||||
- "8088:8088"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
- ./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;
|
listen 8082;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
||||||
location /media {
|
|
||||||
alias /data/media;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://app:8000;
|
set $backend "http://app-dev:8000";
|
||||||
proxy_set_header Host $host;
|
proxy_pass $backend;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
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
|
||||||
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: ${*}"
|
echo "🐳(entrypoint) running your command: ${*}"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
# Gunicorn-django settings
|
|
||||||
bind = ["0.0.0.0:8000"]
|
bind = ["0.0.0.0:8000"]
|
||||||
name = "oidc2fer"
|
name = "satosa"
|
||||||
python_path = "/app"
|
python_path = "/app"
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
@@ -1,8 +1,2 @@
|
|||||||
# Django
|
|
||||||
DJANGO_ALLOWED_HOSTS=*
|
|
||||||
DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
|
|
||||||
DJANGO_SETTINGS_MODULE=oidc2fer.settings
|
|
||||||
DJANGO_SUPERUSER_PASSWORD=admin
|
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
PYTHONPATH=/app
|
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,
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
# usually to register additional checkers.
|
# 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.
|
# Pickle collected data for later comparisons.
|
||||||
persistent=yes
|
persistent=yes
|
||||||
@@ -57,7 +57,6 @@ confidence=
|
|||||||
# --disable=W"
|
# --disable=W"
|
||||||
disable=bad-inline-option,
|
disable=bad-inline-option,
|
||||||
deprecated-pragma,
|
deprecated-pragma,
|
||||||
django-not-configured,
|
|
||||||
file-ignored,
|
file-ignored,
|
||||||
locally-disabled,
|
locally-disabled,
|
||||||
no-self-use,
|
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" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Framework :: Django",
|
|
||||||
"Framework :: Django :: 5",
|
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Natural Language :: English",
|
"Natural Language :: English",
|
||||||
@@ -20,25 +18,13 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
]
|
]
|
||||||
description = "An application to handle contacts and teams."
|
description = "An application to handle contacts and teams."
|
||||||
keywords = ["Django", "OIDC", "SAML", "Shibboleth", "FER", "RENATER"]
|
keywords = ["OIDC", "SAML", "Shibboleth", "FER", "RENATER"]
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django-configurations==2.5",
|
"SATOSA==8.4.0",
|
||||||
"django-cors-headers==4.3.1",
|
|
||||||
"django==5.0.2",
|
|
||||||
"django-timezone-field>=5.1",
|
|
||||||
"dockerflow==2024.2.0",
|
|
||||||
"factory_boy==3.3.0",
|
|
||||||
"gunicorn==21.2.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]
|
[project.urls]
|
||||||
@@ -49,20 +35,10 @@ dependencies = [
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
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",
|
"pylint==3.1.0",
|
||||||
"pytest-cov==4.1.0",
|
"pytest-cov==4.1.0",
|
||||||
"pytest-django==4.8.0",
|
|
||||||
"pytest==8.0.2",
|
"pytest==8.0.2",
|
||||||
"pytest-icdiff==0.9",
|
|
||||||
"pytest-xdist==3.5.0",
|
|
||||||
"responses==0.25.0",
|
|
||||||
"ruff==0.2.2",
|
"ruff==0.2.2",
|
||||||
"types-requests==2.31.0.20240218",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
@@ -79,7 +55,6 @@ exclude = [
|
|||||||
"build",
|
"build",
|
||||||
"venv",
|
"venv",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
"*/migrations/*",
|
|
||||||
]
|
]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
|
||||||
@@ -90,7 +65,6 @@ select = [
|
|||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"BLE", # flake8-blind-except
|
"BLE", # flake8-blind-except
|
||||||
"C4", # flake8-comprehensions
|
"C4", # flake8-comprehensions
|
||||||
"DJ", # flake8-django
|
|
||||||
"I", # isort
|
"I", # isort
|
||||||
"PLC", # pylint-convention
|
"PLC", # pylint-convention
|
||||||
"PLE", # pylint-error
|
"PLE", # pylint-error
|
||||||
@@ -104,8 +78,8 @@ select = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
section-order = ["future","standard-library","django","third-party","oidc2fer","first-party","local-folder"]
|
section-order = ["future","standard-library","third-party","oidc2fer","first-party","local-folder"]
|
||||||
sections = { oidc2fer=["core"], django=["django"] }
|
sections = { oidc2fer=["core"] }
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"**/tests/*" = ["S", "SLF"]
|
"**/tests/*" = ["S", "SLF"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_dummy():
|
||||||
|
actual = "hello"
|
||||||
|
assert actual == "hello"
|
||||||
Reference in New Issue
Block a user