Skip to content

Autoliv Safety REST API

A Python-based Azure Functions REST API for the Autoliv Safety Platform, providing secure endpoints for safety-critical automotive systems.

Current version: 0.13.1 Β· Python: 3.11 Β· Runtime: Azure Functions (Python)

πŸ“‹ Table of Contents


πŸ†• Latest Updates

v0.13.1 β€” Current

  • MSS-1465 Added Portal Comments that can be attached to items in Users, Events, Devices, and FirmwareVersion.

v0.13.0

MSS-764 - [Cloud][portal] List Devices in User information MSS-763 - [Cloud][portal] List Users in device information

v0.12.4

  • MSS-1147: Added organizationName and deviceId fields to crash Events and updated Service Bus connections.
  • GET /v1/users (admin) now returns organizationName and deviceId per user via the updated UserListItemResponse schema.

How Releases Work

Releases are automated via Commitizen. To trigger a release from the development branch, include bump: release in a commit message. The pipeline will run cz bump, update CHANGELOG.md, create an annotated Git tag, and deploy to QA. See CI/CD Pipeline for the full flow.


πŸ—οΈ Architecture

System Architecture Diagram

``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLIENT APPLICATIONS β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Web App β”‚ Mobile App β”‚ IoT Devices β”‚ Admin Dashboard β”‚ β”‚ (React/JS) β”‚ (Kotlin) β”‚ (Embedded) β”‚ (PTW Admin Portal) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AUTHENTICATION LAYER β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ FusionAuth β”‚ Auth UI Helper β”‚ External OAuth β”‚ Token Validation β”‚ β”‚ :9011 β”‚ :3000 β”‚ (Google/Apple) β”‚ (JWT RS256) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API GATEWAY LAYER β”‚ β”‚ Azure Functions (:7071) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ─ β”‚ Auth β”‚ Users β”‚ Device β”‚ Events β”‚ FOTA β”‚ UserSettings β”‚ β”‚ /v1/auth β”‚ /v1/usersβ”‚ /v1/deviβ”‚ /v1/add β”‚ /v1/Firmβ”‚ /v1/userEmergency β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BUSINESS LOGIC LAYER β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ AuthService β”‚ Notification β”‚ TokenService β”‚ BlobStorage / DB Layer β”‚ β”‚ (Google/Apple)β”‚ Service + SNS β”‚ (Key Vault) β”‚ (pyodbc β†’ MSSQL) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DATA LAYER β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ SQL Server β”‚ Azure Blob β”‚ Azure Service β”‚ Azure Key Vault β”‚ β”‚ :1433 β”‚ Storage β”‚ Bus (AMQP) β”‚ (JWT signing key) β”‚ β”‚ dbo.Users β”‚ fota-updates/ β”‚ Notification β”‚ β”‚ β”‚ dbo.Devices β”‚ crash-logs/ β”‚ Queue β”‚ β”‚ β”‚ dbo.CrashEventsβ”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Authentication Flow:
Client β†’ FusionAuth β†’ Google/Apple OIDC β†’ Autoliv JWT + Refresh Token

API Request Flow:
Client β†’ Azure Functions β†’ @validate_request β†’ Blueprint β†’ DB/Storage β†’ Response

Crash Event Flow:
IoT Device β†’ POST /v1/addEvent β†’ Service Bus (grace period) β†’ SNS SMS to contacts

FOTA Update Flow:
Admin β†’ POST /v1/uploadFirmware (PTW role required) β†’ Blob Storage β†’ Device polls

```

Technology Stack

Component Technology Version / Details
Runtime Azure Functions (Python) Python 3.11, Functions v4
API Framework azure-functions blueprints 1.21.3
Database Microsoft SQL Server MSSQL 2019+, pyodbc 5.1.0
ORM / Query Raw T-SQL via pyodbc + PLY parser Custom SQL query builder
Request Models Pydantic v2 β‰₯2.10.0, strict mode
Auth (login) Google / Apple OIDC + PyJWT PyJWT 2.9.0, RS256 via Azure Key Vault
Auth (Entra) Microsoft Entra ID JWKS validation for admin endpoints
Blob Storage Azure Blob Storage azure-storage-blob 12.14.1
Messaging Azure Service Bus azure-servicebus 7.14.1, AMQP persistent
SMS AWS SNS boto3 1.37.22
Key Vault Azure Key Vault (Keys) azure-keyvault-keys β‰₯4.7.0
Type checking pyright (strict) Enforced via pre-commit
Documentation MkDocs + ReDoc + OpenAPI 3.0 Versioned via mike

πŸš€ Quick Start

Prerequisites

  • Python 3.11+
  • Docker & Docker Compose
  • Azure Functions Core Tools 4.x

Local Development Setup

  1. Clone and navigate:

bash git clone <repository-url> cd airbagconnect-api

  1. Start the full development stack:

bash docker-compose up -d

  1. Install development dependencies:

bash pip install -r requirements-dev.txt

  1. Install pre-commit hooks:

bash pre-commit install

  1. Populate the database (development data):

bash liquibase update --labels="dev"

Service URLs

Service URL Description
API http://localhost:7071 Azure Functions API endpoints
Auth Service http://localhost:9011 Local FusionAuth (OpenID Connect)
Auth UI http://localhost:3000 Token generation helper
Database localhost:1433 SQL Server (MSSQL)
Blob Storage localhost:10000 Azurite blob emulator
Documentation http://localhost:8000 MkDocs / ReDoc API docs

πŸ“ Project Structure

airbagconnect-api/ β”œβ”€β”€ .azure-pipelines/ β”‚ └── azure-pipelines.yml # CI/CD pipeline definition β”œβ”€β”€ .pre-commit-config.yaml # Pre-commit hook configuration β”œβ”€β”€ .vscode/ # VS Code settings, launch, tasks β”œβ”€β”€ azurite/ # Azure Storage emulator setup β”‚ β”œβ”€β”€ Dockerfile β”‚ └── create_container.py β”œβ”€β”€ bruno/ β”‚ └── AutolivSafety/ # Bruno API collection (Auth, Device, Event, FOTA, User, UserSettings) β”œβ”€β”€ claude_docs/ # Internal architecture docs (CONVENTIONS, DECISIONS, PROJECT_CONTEXT, REPO_MAP, RUNBOOK) β”œβ”€β”€ docs/ β”‚ β”œβ”€β”€ oas/ # OpenAPI JSON fragments + schema extractor β”‚ β”œβ”€β”€ redoc/ # ReDoc HTML template β”‚ └── mkdocs/ # MkDocs static site source β”œβ”€β”€ id_token/ # HTML tool for fetching Google/Apple ID tokens locally β”œβ”€β”€ liquibase/ β”‚ β”œβ”€β”€ changelog-root.yml β”‚ β”œβ”€β”€ changesets/ # XML changesets (v01.01.xx – v01.02.xx) β”‚ └── migrations/ # SQL scripts (v01.01.00 – v01.02.17) β”œβ”€β”€ mssql/ # SQL Server Docker image & init scripts β”œβ”€β”€ pyproject.toml # Tooling config (pylint, pyright, pytest, sqlfluff, commitizen) β”œβ”€β”€ requirements-dev.txt # Dev Python dependencies β”œβ”€β”€ docker-compose.yaml # Full local development stack β”œβ”€β”€ tox.ini β”œβ”€β”€ src/ β”‚ β”œβ”€β”€ function_app.py # Azure Functions entrypoint β€” registers all blueprints β”‚ β”œβ”€β”€ host.json # Azure Functions host config β”‚ β”œβ”€β”€ local.settings.json # Local env vars (not committed) β”‚ β”œβ”€β”€ requirements.txt # Production Python dependencies β”‚ β”œβ”€β”€ Dockerfile # Production container image β”‚ └── app/ β”‚ β”œβ”€β”€ __init__.py # Startup: Service Bus warmup, atexit shutdown hook β”‚ β”œβ”€β”€ auth.py # JWT validation (Autoliv_Safety + Entra ID), UserContext β”‚ β”œβ”€β”€ decorators.py # @validate_request, @handle_exceptions, @require_ptw_admin_portal_issuer, @require_fota_upload_permission β”‚ β”œβ”€β”€ exceptions.py # AuthenticationException β”‚ β”œβ”€β”€ servicebus.py # ServiceBusClientSingleton (AMQP connection pool + warm-keep) β”‚ β”œβ”€β”€ settings.py # Pydantic Settings β€” reads from env / local.settings.json β”‚ β”œβ”€β”€ storage.py # BlobStorage (upload, SAS token generation) β”‚ β”œβ”€β”€ blueprint/ β”‚ β”‚ β”œβ”€β”€ auth.py # /v1/auth/login, /v1/auth/refresh β”‚ β”‚ β”œβ”€β”€ crash_event.py # /v1/addEvent, abort, status, all events, blob upload, SAS, test SMS β”‚ β”‚ β”œβ”€β”€ device.py # /v1/device/{id}, /v1/inflator, /v1/devices β”‚ β”‚ β”œβ”€β”€ fota_service.py # /v1/uploadFirmware, versions, check, get, delete β”‚ β”‚ β”œβ”€β”€ servicebus_processor.py # Timer warmup + Service Bus queue trigger β”‚ β”‚ β”œβ”€β”€ users.py # /v1/users, /v1/users/me β”‚ β”‚ └── usersettings.py # /v1/userEmergencyContacts, /v1/emergencyContacts/{ids} β”‚ β”œβ”€β”€ db/ β”‚ β”‚ β”œβ”€β”€ cnxn.py # DB connection helpers (fetch_one, fetch_object, execute, execute_scalar) β”‚ β”‚ β”œβ”€β”€ crash_event.py β”‚ β”‚ β”œβ”€β”€ device.py β”‚ β”‚ β”œβ”€β”€ fota_service.py β”‚ β”‚ β”œβ”€β”€ notification.py β”‚ β”‚ β”œβ”€β”€ organization.py β”‚ β”‚ β”œβ”€β”€ permission.py β”‚ β”‚ β”œβ”€β”€ refresh_token.py β”‚ β”‚ β”œβ”€β”€ tables.py # Table/view name constants β”‚ β”‚ β”œβ”€β”€ users.py β”‚ β”‚ β”œβ”€β”€ usersettings.py β”‚ β”‚ └── sql/ # PLY-based SQL query builder β”‚ β”œβ”€β”€ schema/ β”‚ β”‚ β”œβ”€β”€ auth.py # LoginRequest, TokenResponse, LoginResponse, RefreshTokenRequest β”‚ β”‚ β”œβ”€β”€ common.py β”‚ β”‚ β”œβ”€β”€ crash_event.py # CrashNotificationRequest, EventType, GeoLocation, NotificationStatusType β”‚ β”‚ β”œβ”€β”€ device.py # DeviceUpdateRequest, DeviceResponse, InflatorValidationResponse β”‚ β”‚ β”œβ”€β”€ fota.py # FotaUploadRequest/Response, FirmwareVersionResponse, FotaDeleteRequest β”‚ β”‚ └── users.py # UserRequest, UserResponse, UserListItemResponse, UserEmergencyContactRequest/Response β”‚ └── services/ β”‚ β”œβ”€β”€ auth_service.py # Google/Apple ID token validation, UserContextFactory β”‚ β”œβ”€β”€ notification_handler.py # SMS dispatch via AWS SNS β”‚ β”œβ”€β”€ notification_service.py # Grace period scheduling, abort, process_notification β”‚ └── token_service.py # JWT minting/verification (Azure Key Vault RS256) └── tests/ β”œβ”€β”€ data.py # Shared test fixtures β”œβ”€β”€ unit/ β”‚ β”œβ”€β”€ blueprint/ # Per-blueprint unit tests (auth, crash_event, device, fota, users, usersettings, servicebus_processor) β”‚ β”œβ”€β”€ db/ # Per-db-module unit tests β”‚ β”œβ”€β”€ service_tests/ # Auth service, notification, token service, refresh token β”‚ β”œβ”€β”€ test_auth.py β”‚ β”œβ”€β”€ test_decorators.py β”‚ β”œβ”€β”€ test_servicebus.py β”‚ └── test_storage.py └── integration/ β”œβ”€β”€ api/ # Typed API wrappers per domain β”œβ”€β”€ conftest.py └── test_service_*.py # Full API tests: auth, crash_event, device, fota, user, usersettings


🌐 API Endpoints

All endpoints require a Bearer JWT in Authorization or X-ZUMO-AUTH header except where noted. Every request is validated by the @validate_request decorator which extracts a typed UserContext.

Authentication

Method Route Auth Description
POST /v1/auth/login ❌ Login with Google or Apple ID token. Returns access_token + refresh_token.
POST /v1/auth/refresh ❌ Single-use refresh token rotation. Returns a new token pair.

Users

Method Route Auth Description
GET /v1/users/me βœ… Get current user's profile.
DELETE /v1/users/me βœ… Anonymise account β€” clears PII, deletes emergency contacts & refresh tokens.
POST /v1/users βœ… Create / upsert a user profile tied to the JWT identity.
GET /v1/users βœ… PTW Admin List all users with organizationName and deviceId. Admin issuer required.

Emergency Contacts (UserSettings)

Method Route Auth Description
GET /v1/userEmergencyContacts βœ… Get current user's ICE contacts (max 5).
POST /v1/userEmergencyContacts βœ… Add a new ICE contact (validates phone, language, limit, duplicates).
DELETE /v1/emergencyContacts/{contactIds} βœ… Delete contacts by comma-separated UUIDs.

Device

Method Route Auth Description
GET /v1/device/{deviceId} βœ… Get device info for the authenticated user.
PUT /v1/device/{deviceId} βœ… Upsert device record and assign to current user.
GET /v1/inflator/{inflatorId} βœ… Validate inflator ID and check if already in use.
POST /v1/inflator βœ… Associate an inflator with the current user's device.
GET /v1/devices βœ… PTW Admin List all devices. Admin issuer required.

Crash Events

Method Route Auth Description
POST /v1/addEvent βœ… Report a crash event. DEPLOY events are queued to Service Bus with a grace period; others are stored only.
POST /v1/abortNotification/{eventId} βœ… Cancel a notification still within the grace period.
GET /v1/notificationStatus/{eventId} βœ… Get the notification status of a specific event.
GET /v1/getAllEvents βœ… PTW Admin Retrieve all crash events. Admin issuer required.
POST /v1/addEventInformation/{eventId}/data βœ… Upload binary EDR data (application/octet-stream) to Azure Blob Storage.
POST /v1/getBlobSasToken βœ… Generate a 1-hour SAS URL to download a blob by path.
POST /v1/testSms βœ… Send a test SMS directly via AWS SNS.

Firmware Over-The-Air (FOTA)

Method Route Auth Description
POST /v1/uploadFirmware βœ… FOTA role Upload firmware binary + metadata. Requires PTWAdminPortalRoles (e.g. SafetyPortal.Admin).
GET /v1/getFirmwareVersions βœ… PTW Admin List all firmware versions.
GET /v1/checkFirmwareUpdate/{deviceId} βœ… Check if a newer firmware version exists for the device.
GET /v1/getFirmwareUpdate/{deviceId} βœ… Download firmware binary (application/octet-stream) with X-Version header.
DELETE /v1/deleteFirmware βœ… FOTA role Delete a firmware version. Returns 409 if referenced by other versions or active devices.

Portal Comments

These endpoints are for PTW Admin Portal users only and require @require_ptw_admin_portal_issuer.

Method Route Auth Description
POST /v1/portal/comments PTW Admin Create a comment or reply on a User, Event, Device, or FirmwareVersion.
GET /v1/portal/comments/{targetType}/{targetId} PTW Admin List active comments for a target as a flat list ordered by createdAt.
PATCH /v1/portal/comments/{commentId} PTW Admin Update comment body and set updatedAt.
DELETE /v1/portal/comments/{commentId} PTW Admin Soft delete a comment by setting deletedAt.

Create request body:

json { "targetType": "Device", "targetId": "ABC123", "parentCommentId": null, "body": "Internal note" }

Notes:

  • targetType is allow-listed to User, Event, Device, FirmwareVersion.
  • Replies use parentCommentId; the parent must belong to the same target.
  • authorId and authorName are taken from the signed-in token, not from the request body.

Service Bus Triggers (background, non-HTTP)

Trigger Type Schedule / Queue Description
Timer Every 4 min (0 */4 * * * *), on startup servicebus-warmup β€” keeps the AMQP link alive. No message sent.
Service Bus %NotificationQueueName% servicebus-notification-processor β€” processes deferred crash notifications after grace period, dispatches SNS SMS.

🐳 Container Services

docker-compose.yaml defines six services:

Service Image / Build Port(s) Description Depends On
app build: src 7071 β†’ 80 Azure Functions Python API mssql, azurite, auth
mssql build: mssql 1433 β†’ 1433 Microsoft SQL Server β€”
azurite build: azurite 10000–10002 Azure Storage emulator (Blob/Queue/Table) β€”
auth FusionAuth 1.45.1 9011 β†’ 9011 Local OpenID Connect provider auth-db (healthy)
auth-db postgres:12.9 (internal) PostgreSQL backing store for FusionAuth β€”
docs build: docs/ 8000 β†’ 8000 MkDocs documentation server (replicas: 0 by default) β€”

Named volumes: mssql-data, azurite-data, auth-data, auth-db-data.

Service Management

```bash

Start all services

docker-compose up -d

Start specific service

docker-compose up -d mssql

View logs

docker-compose logs -f app

Rebuild after code changes

docker-compose build app && docker-compose up -d app ```


πŸ” Authentication

How It Works

Every HTTP request goes through @validate_request (in src/app/decorators.py), which reads the X-ZUMO-AUTH or Authorization header and calls extract_user() in src/app/auth.py. The issuer (iss claim) determines the validation path:

Autoliv Safety tokens (issued by this API)

  • Signed with an RSA key managed in Azure Key Vault (JwtKeyName).
  • Verified by calling the Key Vault REST API (/keys/{name}/verify) β€” the private key never leaves Key Vault.
  • Claims: sub, iss, organization_role, global_role, organization_name, exp (checked manually).
  • Minted by TokenService at login; lifetime controlled by AccessTokenExpireInMinutes (default 1440 min = 1 day).

Microsoft Entra ID tokens

  • Validated via PyJWKClient against EntraJwksUrl, RS256, expected audience EntraAudience.
  • Role claims prefixed Safetyportal. are extracted as OrganizationRole.
  • Required for admin-only endpoints (via @require_ptw_admin_portal_issuer).

Login Flow

``` POST /v1/auth/login Body: { "id_token": "...", "provider": "google" | "apple" }

  1. AuthService validates the Google/Apple ID token against their JWKS endpoints
  2. Existing refresh tokens for this user are invalidated
  3. TokenService mints a new access token (RS256, Key Vault) + refresh token
  4. Refresh token hash is stored in dbo.RefreshTokens
  5. Returns: access_token, refresh_token, expires_in, organization_name ```

Token Refresh

Refresh tokens are single-use. On POST /v1/auth/refresh, the used token is invalidated immediately and a new pair is returned.

  • Refresh token lifetime: RefreshTokenExpireInDays (default 180 days)
  • Expired token cleanup: every TokenCleanupIntervalHours (default 24h)

Authorization Decorators

Decorator Enforces Used On
@require_ptw_admin_portal_issuer user.issuer == PTWAdminPortalIssuer GET /v1/users, GET /v1/devices, GET /v1/getAllEvents, GET /v1/getFirmwareVersions, /v1/portal/comments*
@require_fota_upload_permission Issuer match + one of PTWAdminPortalRoles (comma-separated) POST /v1/uploadFirmware, DELETE /v1/deleteFirmware

Development Authentication

Test Accounts

Account Email Role
Admin admin@auth.com Administrator
User user@auth.com Standard User

Passwords are configured via environment variables in docker-compose.yaml.

Quick Token Generation (Development Only)

Option 1: Password Grant via FusionAuth

bash curl --request POST \ --url http://localhost:9011/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=password \ --data client_id=[CLIENT_ID] \ --data client_secret=[CLIENT_SECRET] \ --data username=user@auth.com \ --data password=[PASSWORD] \ --data 'scope=openid offline_access'

Option 2: Auth UI Helper

  1. Navigate to http://localhost:3000
  2. Login with test credentials
  3. Copy the generated token

Authorization Code + PKCE Flow

  1. Generate PKCE params at PKCE Generator
  2. Initiate: http://localhost:9011/oauth2/authorize? client_id=[CLIENT_ID]& scope=openid%20offline_access& response_type=code& redirect_uri=http%3A%2F%2Flocalhost%2Foauth-redirect.html& code_challenge=[CODE_CHALLENGE]& code_challenge_method=S256
  3. Exchange: bash curl --request POST \ --url http://localhost:9011/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=authorization_code \ --data redirect_uri=http://localhost/oauth-redirect.html \ --data client_id=[CLIENT_ID] \ --data code=[CODE] \ --data code_verifier=[CODE_VERIFIER]

πŸ› οΈ Development

Prerequisites

Tool Version Purpose
Python 3.11+ Runtime environment
Docker Latest Container orchestration
Azure Functions Core Tools 4.x Local Functions runtime
Pre-commit β‰₯3.2.2 Code quality hooks
Liquibase 4.23.0 Database migrations

Development Setup

```bash

Install dev dependencies

pip install -r requirements-dev.txt

Install all hooks (including pre-push unit tests)

pre-commit install

Install only commit-stage hooks (skip pre-push tests)

pre-commit install -t pre-commit ```

Code Quality β€” Pre-commit Hooks

All hooks run on every git commit. The unit_test hook runs on git push.

Hook Tool / Source Purpose
trailing-whitespace pre-commit-hooks Remove trailing whitespace
end-of-file-fixer pre-commit-hooks Ensure files end with newline
check-yaml pre-commit-hooks Validate YAML syntax
check-json pre-commit-hooks Validate JSON syntax
check-toml pre-commit-hooks Validate TOML syntax
check-added-large-files pre-commit-hooks Block large file commits
autoflake PyCQA/autoflake 2.3.1 Remove unused imports/variables
isort pycqa/isort 7.0.0 Sort imports (black profile)
black psf/black 25.12.0 Format Python code
prettier mirrors-prettier 4.0.0Ξ± Format YAML and Markdown
pretty-format-toml macisamuele 2.15.0 Auto-format TOML
shfmt-docker scop/pre-commit-shfmt Shell script formatter (-s)
sqlfluff-lint sqlfluff 4.0.0Ξ± Lint SQL (T-SQL dialect)
sqlfluff-fix sqlfluff Auto-fix SQL lint issues
pylint local (system Python) Python linter
pyright local (system Python) Strict type checking
bandit PyCQA/bandit 1.9.2 Security scanner (excludes tests/)
pydoclint jsh9/pydoclint 0.8.3 Docstring linter (Google style)
unit_test (pre-push) local run_tests.sh Run unit tests with 70% coverage threshold

Commands:

```bash

Run all hooks manually

pre-commit run -a

Run on specific files

pre-commit run --files file1.py file2.py

Skip hooks (emergency only)

git commit --no-verify ```

Database Development

Liquibase Installation

```bash

Windows

choco install liquibase

macOS

brew install liquibase

Linux (Ubuntu/Debian)

sudo apt-get install liquibase

Or: https://www.liquibase.org/download

```

Configuration: liquibase.properties (in project root, already configured) Changesets: liquibase/changesets/ (XML, naming: v01.02.XX-description.xml) SQL scripts: liquibase/migrations/ (naming: v01.02.XX-description.sql)

Common Liquibase Commands

Command Purpose
liquibase update Apply all pending migrations
liquibase update --labels="dev" Apply dev-only seed data
liquibase status See pending changesets
liquibase rollback-count 1 Undo last changeset
liquibase release-locks Clear stuck migration lock
liquibase clear-checksums Reset checksum mismatches
liquibase validate Check changelog syntax

Database Tables

All tables are in schema dbo:

Table / View Purpose
dbo.Users User accounts (linked to external identity)
dbo.UserAuth User authentication details
dbo.UsersView Denormalized user view
dbo.Devices IoT device records
dbo.UsersDevices Many-to-many user ↔ device join table
dbo.DeviceComponents Inflator/component records per device
dbo.CrashEvents Crash event records with notification state
dbo.EventNotificationDetails Per-contact notification dispatch records
dbo.FirmwareVersions FOTA firmware versions + blob paths
dbo.Comments Internal PTW Portal comments for list items
dbo.Organizations Organization/tenant records
dbo.Issuers Trusted JWT issuers
dbo.RefreshTokens Hashed single-use refresh tokens
dbo.UserEmergencyContacts ICE contacts per user (max 5)

Running SQL Locally

DBeaver (Recommended)

Step Detail
Install dbeaver.io
Host localhost:1433
Credentials Username SA, password from docker-compose.yaml
Database safeliv_dev

WSL2 users: See WSL2 Database Connection if DBeaver fails to connect from Windows.

sqlcmd (lightweight)

bash sqlcmd -S localhost,1433 -U SA -P [PASSWORD] -d safeliv_dev

Useful queries:

```sql -- List all tables SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';

-- Check migration history SELECT * FROM DATABASECHANGELOG ORDER BY DATEEXECUTED DESC; ```

WSL2 Database Connection

Option 1: Windows Firewall rule (simplest)

```powershell

Run as Administrator in Windows PowerShell

New-NetFirewallRule -DisplayName "WSL2 SQL Server" -Direction Inbound -LocalPort 1433 -Protocol TCP -Action Allow ```

Then connect DBeaver to 127.0.0.1:1433, username SA.

Option 2: Port proxy

powershell wsl hostname -I # note the WSL2 IP netsh interface portproxy add v4tov4 listenport=1433 listenaddress=127.0.0.1 connectport=1433 connectaddress=<WSL_IP>

Option 3: sqlcmd inside WSL2 (always works)

bash sudo apt-get install mssql-tools unixodbc-dev /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P '[PASSWORD]' -d safeliv_dev


πŸ§ͺ Testing

Test Architecture

Type Location Isolation Database
Unit Tests tests/unit/ Mocked DB, storage, auth None required
Integration Tests tests/integration/ Real DB, real services Docker stack

Coverage threshold: 70% at pre-push, 80% enforced in CI.

Setup

Ensure src/local.settings.json has a valid DatabaseConnection:

json { "Values": { "DatabaseConnection": "Driver={ODBC Driver 17 for SQL Server};Database=safeliv_dev;Server=localhost,1433;UID=SA;PWD=[PASSWORD];Encrypt=yes;TrustServerCertificate=yes;" } }

ODBC Driver 17+ required: Installation guide

Running Tests

```bash

Show help

./run_tests.sh -h

All tests

./run_tests.sh

Unit tests only

./run_tests.sh --unit

Integration tests only

./run_tests.sh --integration

Unit tests, skip DB spin-up (faster, reuses existing DB)

./run_tests.sh --unit --existing-db

Manual

pytest tests/unit/ pytest tests/integration/ pytest tests/unit/blueprint/test_v1_user.py::test_get_me ```

Test Structure

tests/ β”œβ”€β”€ data.py # Shared test fixtures β”œβ”€β”€ unit/ β”‚ β”œβ”€β”€ blueprint/ # Tests per HTTP blueprint (auth, crash_event, device, fota, users, usersettings, servicebus_processor) β”‚ β”œβ”€β”€ db/ # Tests per DB module (cnxn, crash_event, device, fota, notification, user, usersettings) β”‚ β”œβ”€β”€ service_tests/ # AuthService, NotificationService, TokenService, RefreshToken β”‚ β”œβ”€β”€ test_auth.py # extract_user(), validate_autoliv_safety_token(), validate_entra_id_token() β”‚ β”œβ”€β”€ test_decorators.py # All decorators β”‚ β”œβ”€β”€ test_servicebus.py # ServiceBusClientSingleton β”‚ └── test_storage.py # BlobStorage └── integration/ β”œβ”€β”€ api/ # Typed API wrappers per domain β”œβ”€β”€ conftest.py └── test_service_{auth,crash_event,device,fota_service,user,usersettings}.py


πŸ”Œ API Testing with Bruno

This project includes a Bruno API collection at bruno/AutolivSafety/. Bruno is a fast, Git-friendly open-source API client.

Getting Started

  1. Install Bruno: usebruno.com
  2. Open Collection: In Bruno, open the bruno/AutolivSafety folder
  3. Select Environment: dev, qa, or prod

Available Environments

Environment Purpose
dev Local development
qa Pre-production testing
prod Live system

Bruno Environment Variables

Variable Description Example
host API base URL https://meuw-safety-dv-fn01.azurewebsites.net
v1 API version path /api/v1
code Azure Functions auth code (from Azure portal)

Collection Structure

Module Endpoints
Auth Login (Google/Apple), Refresh token
Device Get/update device info, add inflator, get inflator validation
Event Add event, get all events, abort notification, get notification status, SAS token
FotaService Get versions, upload, check update, get update, delete version
User Create user, get me, get all users, delete me
UserSettings Get/add/delete emergency contacts

Authentication Flow in Bruno

  1. Run Login to get access_token and refresh_token
  2. Copy access_token to your environment variables
  3. When expired, use Refresh token to get a new pair
  4. All other requests use the stored access_token automatically

πŸ“– Documentation

Building Documentation

```bash

Build docs once

docker compose run --rm docs ./docs/generate_docs.sh

Build and serve with live reload

docker compose run --rm -p 8000:8000 docs ./docs/generate_docs.sh --serve

open http://localhost:8000 ```

Documentation Structure

docs/ β”œβ”€β”€ oas/ β”‚ β”œβ”€β”€ base.json # Base OpenAPI 3.0 template β”‚ β”œβ”€β”€ base-schema.json # Schema definitions β”‚ └── extract_schema.py # Schema extraction tool β”œβ”€β”€ mkdocs/ # MkDocs static site source └── redoc/ └── template.hbs # ReDoc HTML template

Documentation System

Component Technology Purpose
API Docs OpenAPI 3.0 + ReDoc Interactive API docs
Dev Guides MkDocs Static developer documentation
Versioning mike Multi-version doc deployments

βš™οΈ Configuration

Configuration Files

File Purpose Committed
src/local.settings.json Local dev env vars ❌ No
docker-compose.yaml Container config βœ… Yes (test secrets only)
liquibase.properties DB migration config βœ… Yes

Environment Variables (from src/app/settings.py)

All values read from OS environment first, then from src/local.settings.json["Values"].

Variable Default Description
AzureWebJobsStorage required Azure Functions internal storage connection string
AzureWebJobsFeatureFlags required Set to EnableWorkerIndexing to enable blueprint support
FUNCTIONS_WORKER_RUNTIME required Must be "python"
PYTHON_THREADPOOL_THREAD_COUNT β€” Thread pool size (set to "10" in local settings)
DatabaseConnection required ODBC connection string for MSSQL
DatabaseName required SQL database name (e.g. safeliv_dev)
BlobStorageConnection required Azure Blob Storage connection string
BlobStorageContainerFota required Container for firmware files (e.g. fota-updates)
BlobStorageContainerEvents required Container for crash EDR blobs (e.g. crash-logs)
BlobStorageTokenExpirySeconds 21600 SAS token expiry in seconds (6 hours)
AuthOpenIDSettings required Semicolon-delimited OpenID configs: Issuer=...;ConfigUrl=...;ClientId=... (multiple: ;;)
AuthJWTCacheTTL 0 Seconds to cache OpenID config per issuer (0 = no cache)
PTWAdminPortalIssuer "" Required issuer for admin-only endpoints (Entra ID URL)
PTWAdminPortalRoles "" Comma-separated roles required for FOTA upload/delete
GoogleClientIds "" Semicolon-separated valid Google OAuth client IDs
AppleClientIds "" Semicolon-separated valid Apple client IDs
KeyVaultUrl "" Azure Key Vault URL for JWT signing key
JwtKeyName "JwtSigningKey" Key name in Key Vault for RS256 signing
EntraJwksUrl "" Microsoft Entra ID JWKS endpoint URL
EntraAudience "" Expected audience for Entra ID tokens
AccessTokenExpireInMinutes 1440 Access token lifetime (1 day)
RefreshTokenExpireInDays 180 Refresh token lifetime
TokenCleanupIntervalHours 24 Expired token cleanup frequency
ServiceBusConnectionString required Azure Service Bus connection string (AMQP)
NotificationQueueName required Service Bus queue for crash notifications
GracePeriodDuration 30 Seconds before a DEPLOY event notification is dispatched
MAX_CONTACTS_ALLOWED 5 Maximum emergency contacts per user
AwsAccessKeyId "" AWS credentials for SNS SMS
AwsSecretAccessKey "" AWS credentials for SNS SMS
AwsRegion "" AWS region for SNS
AwsSnsSenderId "ALVSAFETY" SMS sender ID
APPLICATIONINSIGHTS_CONNECTION_STRING β€” Azure Application Insights connection string
LogLevel "INFO" Root log level (TRACE/DEBUG/INFO/WARNING/ERROR)
Debug False Return error body in 500 responses instead of bare 400s

Full local.settings.json Template

json { "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=[KEY];BlobEndpoint=http://azurite:10000/devstoreaccount1;", "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", "FUNCTIONS_WORKER_RUNTIME": "python", "PYTHON_THREADPOOL_THREAD_COUNT": "10", "DatabaseConnection": "Driver={ODBC Driver 17 for SQL Server};Database=safeliv_dev;Server=mssql,1433;UID=SA;PWD=[PASSWORD];Encrypt=yes;TrustServerCertificate=yes;", "DatabaseName": "safeliv_dev", "BlobStorageConnection": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=[KEY];BlobEndpoint=http://azurite:10000/devstoreaccount1;", "BlobStorageContainerFota": "fota-updates", "BlobStorageContainerEvents": "crash-logs", "AuthOpenIDSettings": "Issuer=[ISSUER];ConfigUrl=[OPENID_CONFIG_URL];ClientId=[CLIENT_ID]", "PTWAdminPortalIssuer": "[ENTRA_ISSUER_URL]", "PTWAdminPortalRoles": "SafetyPortal.Admin,SafetyPortal.Contributor", "GoogleClientIds": "[GOOGLE_CLIENT_ID]", "AppleClientIds": "[APPLE_CLIENT_ID]", "KeyVaultUrl": "https://[VAULT_NAME].vault.azure.net/", "JwtKeyName": "JwtSigningKey", "EntraJwksUrl": "https://login.microsoftonline.com/[TENANT_ID]/discovery/v2.0/keys", "EntraAudience": "[ENTRA_APP_CLIENT_ID]", "ServiceBusConnectionString": "Endpoint=sb://[NAMESPACE].servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=...", "NotificationQueueName": "[QUEUE_NAME]", "GracePeriodDuration": "30", "AwsAccessKeyId": "[AWS_KEY]", "AwsSecretAccessKey": "[AWS_SECRET]", "AwsRegion": "eu-north-1", "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=[KEY]", "LogLevel": "DEBUG" } }

Database Connection String

Driver={ODBC Driver 17 for SQL Server}; Database=[DATABASE_NAME]; Server=[SERVER],[PORT]; UID=[USERNAME]; PWD=[PASSWORD]; Encrypt=yes; TrustServerCertificate=yes; Connection Timeout=60; LoginTimeout=60;

Security Considerations

Type Level Notes
Development secrets Low Shared in docker-compose (test only)
Production secrets High Use Azure Key Vault
JWT signing key High Stored in Key Vault, never exported
API keys / access keys High Never commit to repository
Connection strings Medium Mask passwords in logs

πŸ”„ CI/CD Pipeline

The pipeline runs on Azure Pipelines and is defined in .azure-pipelines/azure-pipelines.yml.

Triggers:

  • Commits to main and development branches (development is the primary integration branch)
  • Pull requests to development
  • Tags matching v*

Pipeline Variables

Variable Value / Purpose
pythonVersion 3.11
nodeVersion 24.x β€” for documentation tooling
testCoverage 80 β€” minimum required test coverage (%)
functionArtifactName Azure Function artifact name
functionArtifactArchiveName ZIP file name including the build ID
azureServiceConnection Service connection for development environment
azureFunctionName Development function app name
deploymentEnvironment Development deployment environment name

Pipeline Stages

1. Quality Assurance (qa) β€” always runs

  1. Checkout code
  2. Set Python 3.11
  3. Cache pre-commit hooks
  4. Install requirements.txt, requirements-dev.txt, pre-commit hooks
  5. Install Liquibase 4.23.0
  6. Run all pre-commit hooks (linting, formatting, security checks)
  7. Run unit tests with 80% coverage threshold
  8. Publish JUnit test results to Azure DevOps
  9. Publish Cobertura coverage report
  10. SonarCloud: prepare β†’ analyze β†’ publish

2. Security Scan (security_scan) β€” non-PR builds only, after QA

  1. Checkout
  2. Datadog Static Analysis β€” install CLI, run secrets detection, upload SARIF
  3. Datadog SBOM β€” generate and upload Software Bill of Materials

3. Build (build) β€” after QA, for development/main/v* tags/feature/*

  1. Apply .funcignore β€” copy filtered sources to deploy/
  2. Set Python 3.11
  3. Install packages into .python_packages/lib/site-packages
  4. Create ZIP archive
  5. Publish artifact

4. Deploy Development (deploy_dv) β€” after build

  • Deploy ZIP to Azure Function App (Linux runtime)
  • On success: build versioned docs with mike (branch + "latest" alias), push to azure-docs branch, sync to docs-hub via PR

5. Create Release (release) β€” after dev deploy, development branch only, commit message contains bump: release

  1. Checkout with full history (fetch-depth: 0)
  2. Install Commitizen
  3. Fetch all tags
  4. Run cz bump:
  5. Tags exist β†’ bump version from conventional commits
  6. No tags β†’ create initial 0.1.0
  7. Push development + new annotated tag
  8. Extract release notes from CHANGELOG.md
  9. Publish release notes as artifact

6. Deploy by Tag (deploy_by_tag) β€” when source is refs/tags/v*

Builds and deploys the tagged version to QA. Documentation published with the explicit version number.

How to Trigger a Release

bash git commit --allow-empty -m "bump: release" git push origin development

The release stage runs cz bump, creates the tag, and deploy_by_tag deploys to QA automatically.

How Commitizen Works

Reads conventional commits since the last tag to determine bump type:

Commit Type Bump Example
feat: MINOR 1.0.0 β†’ 1.1.0
fix: PATCH 1.0.0 β†’ 1.0.1
BREAKING CHANGE: MAJOR 1.0.0 β†’ 2.0.0
docs:, chore: None β€”

Configuration in pyproject.toml:

toml [tool.commitizen] name = "cz_conventional_commits" version = "0.12.4" tag_format = "v$version" update_changelog_on_bump = true

cz bump will: analyze git history β†’ determine bump β†’ update pyproject.toml + CHANGELOG.md β†’ commit with bump: version X β†’ Y β†’ create annotated tag vY.

Datadog Security Configuration

Variable Where to set
DD_API_KEY Azure DevOps pipeline secret variable
DD_APP_KEY Azure DevOps pipeline secret variable
DD_SITE Set directly in pipeline definition

Key Pipeline Features

Feature Description
Caching Pre-commit hooks cached between runs
Parallel stages Security scan runs independently of deployment pipeline
Environment protection Dev and QA environments each have approval gates
Versioned documentation mike manages multi-version doc deployments
Automated releases Commitizen handles versioning and changelog generation
Security scanning Datadog SBOM + secrets scanning
Quality gates 80% test coverage + SonarCloud analysis required

πŸ”— References