Adopted oAuth2 with JWT, which is the standard for trusted machine-to-machine (M2M) interaction.
https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
Smaller platforms can opt for delegating SDEP API-invocation to third-parties.
In such case, the platform arranges data submission with their party; the party becomes registered in SDEP.
To log "who did what, where, when, from where, and with what result", plus enough context to reconstruct important actions.
- For technical management purposes only (troubleshooting security, performance, ...)
- No sensitive data
- Middleware-based: A Starlette
BaseHTTPMiddlewareintercepts every request/response cycle - Async background writes: Audit records are written via
asyncio.create_task()so they never block the response - Append-only: The
audit_logtable is insert-only; no updates or deletes - Error-resilient: Audit write failures are logged but never break the request
- Structured JSON to stdout: Each audit record is also emitted as a single-line JSON object to stdout, providing three complementary access paths:
- Database audit log — quick queries and easy access with shorter, application-managed retention
- Stdout — real-time observability (e.g.
kubectl logs, container log streams) - Stdout → external log management — ship to ELK, Loki, Splunk etc. for longer retention than the application's configured retention period
For each request that matters, capture:
| Field | Source | Description | Answers |
|---|---|---|---|
| timestamp | Server clock | UTC, server default now() |
When |
| requestId | Generated | UUID4 correlation ID | — |
| roles | JWT realm_access.roles |
Comma-separated role list (nullable) | Who |
| resourceType | Derived from path | Entity type, e.g. area, activity |
Where |
| action | Derived from method + path | Semantic action verb, e.g. create |
What |
| httpMethod | Request | HTTP method (GET, POST, DELETE) |
What |
| path | Request | Request path, e.g. /api/v0/ca/areas |
Where |
| httpStatusCode | Response | HTTP status code | Result |
| statusCode | Derived from httpStatusCode | OK if httpStatusCode < 400, else NOK |
Result |
| durationMs | Calculated | Request processing time in milliseconds | — |
The middleware derives a semantic action and resource type from the HTTP method and request path:
| Method | Path pattern | Resource type | Action |
|---|---|---|---|
| POST | /*/ca/areas |
area |
create |
| GET | /*/ca/areas |
area |
list |
| GET | /*/ca/areas/count |
area |
count |
| GET | /*/ca/areas/{id} |
area |
read |
| DELETE | /*/ca/areas/{id} |
area |
delete |
| POST | /*/str/activities/bulk |
activity |
create_bulk |
| POST | /*/str/activities |
activity |
create |
| GET | /*/str/areas |
area |
list |
| GET | /*/str/areas/count |
area |
count |
| GET | /*/str/areas/{id} |
area |
read |
| GET | /*/ca/activities |
activity |
list |
| GET | /*/ca/activities/count |
activity |
count |
| POST | /*/auth/token |
auth |
token |
| GET | /*/ping |
system |
ping |
Unmatched paths fall back to action unknown.
| id | timestamp | request_id | roles | resource_type | action | http_method | path | http_status_code | status_code | duration_ms |
| --- | ----------------------------- | ------------ | ---------------------------- | ------------- | ------ | ----------- | ---------------- | ---------------- | ----------- | ----------- |
| 20 | 2026-03-23 15:03:38.519686+00 | a34e8a0e-... | sdep_write,sdep_ca,sdep_read | system | ping | GET | /api/v0/ping | 200 | OK | 1 |
| 21 | 2026-03-23 15:03:39.864974+00 | 7bccb30b-... | sdep_write,sdep_ca,sdep_read | area | create | POST | /api/v0/ca/areas | 201 | OK | 33 |
| 22 | 2026-03-23 15:03:39.947615+00 | f357d78c-... | sdep_write,sdep_ca,sdep_read | area | create | POST | /api/v0/ca/areas | 201 | OK | 27 |
| 23 | 2026-03-23 15:03:40.02963+00 | 02294cf4-... | sdep_write,sdep_ca,sdep_read | area | create | POST | /api/v0/ca/areas | 201 | OK | 18 |
The following paths are not audited (high-frequency, low-value):
/(root)/api/health/api/v0/openapi.json/api/v0/docs/api/v0/redoc
Expired audit log rows are automatically deleted by a background task that runs every hour.
- The retention period is configurable via the
AUDITLOG_RETENTIONenvironment variable (default: 1 day). - Deletion is batched (1.000 rows per batch) to avoid long-running transactions.
The retention logic in audit_retention.py is split into two functions with distinct responsibilities:
delete_old_audit_logsdoes the actual work;audit_log_cleanup_loopis the scheduler that ensures that work runs repeatedly for the lifetime of the application.
| Function | Responsibility | Invocation |
|---|---|---|
delete_old_audit_logs(retention_days) |
One-shot deletion. Deletes all audit log rows older than retention_days in batches of 1.000. Returns the total number of deleted rows. This is a pure async function that runs to completion and then returns — it does not loop or sleep. |
Called by audit_log_cleanup_loop on each cycle. Can also be called standalone in scripts, tests, or one-off maintenance tasks. |
audit_log_cleanup_loop(retention_days, interval_seconds) |
Infinite scheduling loop. Calls delete_old_audit_logs once, then sleeps for interval_seconds (default 3.600 s = 1 hour), and repeats indefinitely until the task is cancelled. Catches and logs any exceptions so that a single failed cycle does not kill the loop. |
Created as an asyncio.Task inside the FastAPI lifespan context manager in main.py. The task starts when the application boots and is cancelled (via task.cancel()) when the application shuts down. |