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
- ποΈ Architecture
- π Quick Start
- π Project Structure
- π API Endpoints
- π³ Container Services
- π Authentication
- π οΈ Development
- π§ͺ Testing
- π API Testing with Bruno
- π Documentation
- βοΈ Configuration
- π CI/CD Pipeline
- π References
π Latest Updates
v0.13.1 β Current
- MSS-1465 Added Portal Comments that can be attached to items in
Users,Events,Devices, andFirmwareVersion.
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
organizationNameanddeviceIdfields to crash Events and updated Service Bus connections. GET /v1/users(admin) now returnsorganizationNameanddeviceIdper user via the updatedUserListItemResponseschema.
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
- Clone and navigate:
bash
git clone <repository-url>
cd airbagconnect-api
- Start the full development stack:
bash
docker-compose up -d
- Install development dependencies:
bash
pip install -r requirements-dev.txt
- Install pre-commit hooks:
bash
pre-commit install
- 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:
targetTypeis allow-listed toUser,Event,Device,FirmwareVersion.- Replies use
parentCommentId; the parent must belong to the same target. authorIdandauthorNameare 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
TokenServiceat login; lifetime controlled byAccessTokenExpireInMinutes(default 1440 min = 1 day).
Microsoft Entra ID tokens
- Validated via
PyJWKClientagainstEntraJwksUrl, RS256, expected audienceEntraAudience. - Role claims prefixed
Safetyportal.are extracted asOrganizationRole. - Required for admin-only endpoints (via
@require_ptw_admin_portal_issuer).
Login Flow
``` POST /v1/auth/login Body: { "id_token": "...", "provider": "google" | "apple" }
- AuthService validates the Google/Apple ID token against their JWKS endpoints
- Existing refresh tokens for this user are invalidated
- TokenService mints a new access token (RS256, Key Vault) + refresh token
- Refresh token hash is stored in dbo.RefreshTokens
- 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 | 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
- Navigate to
http://localhost:3000 - Login with test credentials
- Copy the generated token
Authorization Code + PKCE Flow
- Generate PKCE params at PKCE Generator
- 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 - 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
- Install Bruno: usebruno.com
- Open Collection: In Bruno, open the
bruno/AutolivSafetyfolder - Select Environment:
dev,qa, orprod
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
- Run Login to get
access_tokenandrefresh_token - Copy
access_tokento your environment variables - When expired, use Refresh token to get a new pair
- All other requests use the stored
access_tokenautomatically
π 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
mainanddevelopmentbranches (developmentis 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
- Checkout code
- Set Python 3.11
- Cache pre-commit hooks
- Install
requirements.txt,requirements-dev.txt, pre-commit hooks - Install Liquibase 4.23.0
- Run all pre-commit hooks (linting, formatting, security checks)
- Run unit tests with 80% coverage threshold
- Publish JUnit test results to Azure DevOps
- Publish Cobertura coverage report
- SonarCloud: prepare β analyze β publish
2. Security Scan (security_scan) β non-PR builds only, after QA
- Checkout
- Datadog Static Analysis β install CLI, run secrets detection, upload SARIF
- Datadog SBOM β generate and upload Software Bill of Materials
3. Build (build) β after QA, for development/main/v* tags/feature/*
- Apply
.funcignoreβ copy filtered sources todeploy/ - Set Python 3.11
- Install packages into
.python_packages/lib/site-packages - Create ZIP archive
- 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 toazure-docsbranch, sync to docs-hub via PR
5. Create Release (release) β after dev deploy, development branch only, commit message contains bump: release
- Checkout with full history (
fetch-depth: 0) - Install Commitizen
- Fetch all tags
- Run
cz bump: - Tags exist β bump version from conventional commits
- No tags β create initial
0.1.0 - Push
development+ new annotated tag - Extract release notes from
CHANGELOG.md - 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 |