# FreeRADIUS REST API A FastAPI service exposing RESTful CRUD over the FreeRADIUS MySQL/MariaDB schema (`customers`, `radcheck`, `radreply`, `radgroupreply`/vlans, `radusergroup`, `nas`, plus read-only `radacct`, `radpostauth`, `nasreload`). ## Stack - **FastAPI** + **Uvicorn** (ASGI) - **SQLAlchemy 2.0** ORM + **PyMySQL** driver - **Pydantic v2** request/response validation - Auth via a shared secret in the **`X-API-Key`** header ## Setup ```bash python3 -m venv venv venv/bin/pip install -r requirements.txt cp .env.example .env # then edit credentials + API_KEY ``` ### `.env` | Var | Meaning | |-----------------|------------------------------------------------| | `DB_HOST` | MySQL host (`127.0.0.1` if API runs on the DB box) | | `DB_PORT` | MySQL port (default 3306) | | `DB_USER` / `DB_PASSWORD` | DB credentials | | `DB_NAME` | Database name (`radius`) | | `API_KEY` | Shared secret required in `X-API-Key` | | `CORS_ORIGINS` | Comma-separated allowed origins (`*` for dev) | | `DEFAULT_LIMIT` / `MAX_LIMIT` | List pagination caps | > **Note:** MariaDB on the staging box binds to `127.0.0.1` only. To reach it from > another host either run the API on the RADIUS server (`DB_HOST=127.0.0.1`), open > an SSH tunnel (`ssh -N -L 13306:127.0.0.1:3306 root@HOST` then `DB_PORT=13306`), > or bind MariaDB to the LAN and grant remote access. ## Run ```bash venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` - Interactive docs (Swagger): `http://HOST:8000/docs` - OpenAPI JSON: `http://HOST:8000/openapi.json` - Health check (no auth): `GET /health` ## Auth Every resource endpoint requires the header: ``` X-API-Key: ``` Missing/wrong key → `401`. `/`, `/health`, and `/docs` are open. ## Endpoints All list endpoints return a paginated envelope and accept `?limit=&offset=` plus per-resource filters: ```json { "total": 12, "limit": 50, "offset": 0, "items": [ ... ] } ``` | Resource | Path | Methods | Filters | |-----------------|-------------------|--------------------------|----------------------------------| | Customers | `/customers` | GET, POST, PUT, DELETE | `username`, `mac_address`, `status` | | NAS clients | `/nas` | GET, POST, PUT, DELETE | `nasname`, `shortname` | | User check | `/radcheck` | GET, POST, PUT, DELETE | `username`, `attribute` | | User reply | `/radreply` | GET, POST, PUT, DELETE | `username`, `attribute` | | Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` | | Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` | | **VLANs** | `/vlan` | see below | — | | **Devices** | `/device` | see below | — | | User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` | | Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` | | Post-auth log | `/radpostauth` | GET (read-only) | `username`, `reply` | | NAS reload | `/nasreload` | GET (read-only) | — | `radacct`, `radpostauth`, and `nasreload` are **read-only** — FreeRADIUS owns writes. ### VLANs — `/vlan` A VLAN is a logical entity (`alias` + `vlanid`) backed by three `radgroupreply` rows sharing one `groupname`: `Tunnel-Type=VLAN`, `Tunnel-Medium-Type=IEEE-802`, and `Tunnel-Private-Group-Id=`. | Action | Request | |------------------|-----------------------------------------------------| | List VLANs | `GET /vlan/` → `[{"alias","vlanid"}, ...]` | | Add a VLAN | `POST /vlan/add` body `{"vlanid":55,"alias":"staff"}` (inserts the 3 rows) | | Rename alias | `POST /vlan/edit` body `{"vlanid":55,"alias":"employees"}` | | Delete a VLAN | `DELETE /vlan/{vlanid}` (removes all rows for that group) | - List returns **one row per VLAN** — only the alias and VLAN ID, not the raw `Tunnel-*` attribute rows. - Duplicate **VLAN ID** or **alias** on add → `409`. - `vlanid` must be `1–4094`. - Renaming updates `radgroupreply.groupname` only; if you also map users to groups in `radusergroup`, update those separately. ### Devices — `/device` A device is a client identified by its MAC address (used as the RADIUS `username`). One device spans three tables: `radcheck` (MAC = password), `radusergroup` (group membership), and `customers` (status). MAC input is normalized to uppercase, hyphen-separated (`AA-BB-CC-DD-EE-FF`); colons and lowercase are accepted. | Action | Request | |------------------|-----------------------------------------------------| | List devices | `GET /device/` → `{total,limit,offset,items:[{mac_address,group,status}]}` | | Get one device | `GET /device/{mac_address}` | | Add a device | `POST /device/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff"}` | | Edit a device | `POST /device/edit` body `{"mac_address":"...","group":"...","status":"..."}` | | Delete a device | `DELETE /device/{mac_address}` (removes all 3 rows) | - **Add** inserts a `radcheck` password (`Cleartext-Password := MAC`), a `radusergroup` row (`priority 1`), and a `customers` row (`status = paid`). The `group` must already exist in `radgroupreply` or you get `400`. - **Edit** accepts `group` and/or `status` — supply either or both (at least one required). A new `group` must exist in `radgroupreply` (`400` otherwise). `status` must be one of `new` / `paid` / `unpaid`. In the DB the group is stored in the `radusergroup.groupname` column. - Adding a device whose MAC already exists → `409`. ## Examples Replace host/key to match your deployment. ```bash # List customers curl http://10.0.1.235:8000/customers?limit=50 \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # List VLANs curl http://10.0.1.235:8000/vlan/ \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # Add a VLAN (alias "staff", VLAN ID 55) curl http://10.0.1.235:8000/vlan/add \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"vlanid":55,"alias":"staff"}' -s | jq # Rename a VLAN's alias curl http://10.0.1.235:8000/vlan/edit \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"vlanid":55,"alias":"employees"}' -s | jq # Delete VLAN 55 curl http://10.0.1.235:8000/vlan/55 \ -X DELETE \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # List devices curl http://10.0.1.235:8000/device/ \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # Add a device (MAC + existing group) curl http://10.0.1.235:8000/device/add \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"mac_address":"14-99-3E-74-CB-7F","group":"staff"}' -s | jq # Edit a device (any one field: groupname and/or status) curl http://10.0.1.235:8000/device/edit \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"mac_address":"14-99-3E-74-CB-7F","status":"unpaid"}' -s | jq # Delete a device curl http://10.0.1.235:8000/device/14-99-3E-74-CB-7F \ -X DELETE \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # Create a customer curl http://10.0.1.235:8000/customers \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"username":"AA-BB-CC-DD-EE-FF","mac_address":"AA-BB-CC-DD-EE-FF","status":"new"}' -s | jq # Update a customer's status curl http://10.0.1.235:8000/customers/1 \ -X PUT \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \ -H "Content-Type: application/json" \ -d '{"status":"paid"}' -s | jq # Active accounting sessions (no stop time) curl http://10.0.1.235:8000/radacct?active=true \ -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq # Health check (no key needed) curl http://10.0.1.235:8000/health -s | jq ``` ## Status codes | Code | Meaning | |------|---------------------------------------------| | 200 | OK | | 201 | Created | | 204 | Deleted (no content) | | 401 | Missing/invalid `X-API-Key` | | 404 | Row not found | | 409 | Duplicate / integrity conflict | | 422 | Request body failed validation | ## Project layout ``` app/ main.py FastAPI app, router wiring, auth + CORS config.py env-driven settings (pydantic-settings) database.py SQLAlchemy engine/session auth.py X-API-Key dependency errors.py APIError + JSON handler crud.py generic list/get/create/update/delete helpers pagination.py Page envelope + limit/offset dependency models.py SQLAlchemy models (one per table) schemas.py Pydantic request/response models routers/ one module per resource ```