LDAPS directory adapter with certificate verification, allow-listed reads and writes, escaped filters, password verification and change, password policy and account state, plus an in-memory fake. Adds the LDAP_* configuration, python-ldap with its image libraries, the CA file mount and an opt-in live test for the test OU. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
61 lines
3.5 KiB
Markdown
61 lines
3.5 KiB
Markdown
# Self-Service Portal
|
|
|
|
A Django 5.2 LTS container for a user self-service portal. This repository holds the project scaffold: configuration, health check, logging, security headers and the container definition.
|
|
|
|
## Configuration
|
|
|
|
All configuration comes from environment variables, validated at startup. If any variable is missing or invalid, the process prints one message that lists every problem (never the values) and exits with a non-zero code. Copy `.env.example` to `.env` and edit it.
|
|
|
|
| Variable | Required | Default | Meaning |
|
|
| --- | --- | --- | --- |
|
|
| `SECRET_KEY` | yes | | Django secret, at least 32 characters |
|
|
| `ALLOWED_HOSTS` | yes | | Comma-separated host names |
|
|
| `DATA_DIR` | no | `/data` | Existing directory for the SQLite database |
|
|
| `DEBUG` | no | `false` | `true` or `false` |
|
|
| `TRUSTED_PROXIES` | no | empty | Comma-separated IP addresses or networks. Reserved for the client-IP rule in a later entry; currently has no effect |
|
|
| `LOG_LEVEL` | no | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
|
| `LDAP_SERVER` | yes | | Directory server host name (optionally `:port`); LDAPS on 636 is always used and the name must be in the server's certificate |
|
|
| `LDAP_USER_OU_DN` | yes | | Distinguished name of the OU whose users the portal may read and change |
|
|
| `LDAP_BIND_DN` | yes | | Service account, as distinguished name or user principal name |
|
|
| `LDAP_BIND_PASSWORD` | yes | | Password of the service account |
|
|
| `LDAP_CA_FILE` | yes | | Path inside the container of the PEM file with the CA chain; certificate verification is never disabled |
|
|
| `LDAP_PUBLISHER_GROUP_DN` | yes | | Group whose direct members are Publishers |
|
|
| `LDAP_ADMIN_GROUP_DN` | yes | | Group whose direct members are Admins |
|
|
| `DEFAULT_TIMEZONE` | no | `Europe/Berlin` | IANA zone used for display; storage is UTC |
|
|
|
|
## Run in a container
|
|
|
|
The image is built and run on a Linux host from a checkout of this repository.
|
|
|
|
```sh
|
|
cp .env.example .env # then set SECRET_KEY, ALLOWED_HOSTS, the LDAP_* values and CA_FILE_HOST
|
|
docker compose up --build
|
|
curl -H "Host: <first ALLOWED_HOSTS entry>" http://localhost:8000/healthz # {"status":"ok"}
|
|
```
|
|
|
|
The container runs as a non-root user with a read-only root file system. Only the `/data` and `/backups` volumes are writable. It starts one gunicorn worker with threads after applying migrations. Session and CSRF cookies are marked secure, so use the portal through an HTTPS reverse proxy that sets `X-Forwarded-Proto`.
|
|
|
|
`/healthz` checks the database only. It returns 200 `{"status":"ok"}` or 503 `{"status":"unavailable"}`.
|
|
|
|
## Develop
|
|
|
|
Requires Python 3.13 and [uv](https://docs.astral.sh/uv/).
|
|
|
|
```sh
|
|
uv sync
|
|
uv run pytest
|
|
uv run ruff check .
|
|
```
|
|
|
|
`pytest` sets its own test environment (see `pyproject.toml`).
|
|
|
|
### Directory adapter against the test OU
|
|
|
|
`uv sync` builds `python-ldap`, which needs the OpenLDAP and SASL development libraries (the Docker image installs them). The automated tests use an in-memory fake and a stand-in for the `ldap` module, so they run anywhere. To check the real adapter against the test OU, run this on a Linux host with the libraries installed (never against the production OU):
|
|
|
|
```sh
|
|
PORTAL_LIVE_LDAP=1 LIVE_LDAP_SERVER=... LIVE_LDAP_USER_OU_DN=... LIVE_LDAP_BIND_DN=... LIVE_LDAP_BIND_PASSWORD=... LIVE_LDAP_CA_FILE=... LIVE_LDAP_PUBLISHER_GROUP_DN=... LIVE_LDAP_ADMIN_GROUP_DN=... LIVE_LDAP_TEST_PASSWORD=<current password of ptest-user1> uv run pytest portal/tests/test_directory_live.py -v
|
|
```
|
|
|
|
It restores what it writes and does not change any password.
|