2026-07-31 23:27:46 +05:00
2026-07-31 23:27:46 +05:00
2026-07-31 21:28:34 +05:00
2026-07-31 21:28:34 +05:00

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

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

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: <your API_KEY>

Missing/wrong key → 401. /, /health, and /docs are open.

UI integration guide

For a management portal the two primary resources are Devices (/device) and VLANs (/vlan). Everything else is lower-level raw-table access.

Base URL (staging): http://10.0.1.235:8000 Every request: header X-API-Key: <API_KEY> (except /health). All bodies: JSON with Content-Type: application/json.

Response shapes

GET /device/paginated envelope:

{
  "total": 4,
  "limit": 50,
  "offset": 0,
  "items": [
    { "mac_address": "AA-BB-CC-DD-EE-11", "group": "residents", "status": "paid",
      "name": "Sara Ib", "phone": "9998887", "alias": "Living Room TV" }
  ]
}

GET /device/{mac}, POST /device/add, POST /device/edit — a single device object:

{ "mac_address": "AA-BB-CC-DD-EE-11", "group": "residents", "status": "paid",
  "name": "Sara Ib", "phone": "9998887", "alias": "Living Room TV" }

GET /vlan/plain array (not paginated):

[ { "alias": "residents", "vlanid": 51 }, { "alias": "staff", "vlanid": 55 } ]
  • status is always one of "new", "paid", "unpaid".
  • group, name, phone, alias may be null on older rows.
  • DELETE returns 204 with an empty body (nothing to parse).

Error shapes

Application errors (400, 401, 404, 409) return a string detail:

{ "detail": "Device 'AA-BB-CC-DD-EE-11' already exists" }

Validation errors (422, bad/missing fields) return a FastAPI array detail:

{ "detail": [ { "type": "missing", "loc": ["body","phone"], "msg": "Field required" } ] }

So in the UI: read detail directly when it's a string; when it's an array, join each entry's msg (and loc) for field-level messages.

Typical portal flows

  • Provision a device: POST /device/add with mac_address, group (an existing VLAN alias), name, phone, optional alias. Handle 400 (group missing), 409 (MAC exists), 422 (bad MAC / missing name·phone).
  • Change plan state: POST /device/edit {mac_address, status}.
  • Move to another VLAN: POST /device/edit {mac_address, group}.
  • Populate a VLAN dropdown: GET /vlan/ → map alias (value sent as group).

Endpoints

All list endpoints return a paginated envelope and accept ?limit=&offset= plus per-resource filters:

{ "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=<vlanid>.

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 14094.
  • 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 + human metadata). MAC input is normalized to uppercase, hyphen-separated (AA-BB-CC-DD-EE-FF); colons and lowercase are accepted.

The customers table also carries human-only metadata that RADIUS never reads: name, phone, and device_alias. These are collected at device creation.

Action Request
List devices GET /device/{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}
Get one device GET /device/{mac_address}
Add a device POST /device/add body {"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}
Edit a device POST /device/edit body {"mac_address":"...", group?, status?, name?, phone?, alias?}
Delete a device DELETE /device/{mac_address} (removes all 3 rows)

On add, name and phone are required; alias is optional. They are stored in customers.name, customers.phone, customers.device_alias and returned on every device response (alias mirrors the device_alias column).

  • 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 any subset of group, status, name, phone, alias (at least one required); omitted fields are left unchanged. 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 and alias in customers.device_alias.
  • Adding a device whose MAC already exists → 409.

Examples

Replace host/key to match your deployment.

# 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 + name/phone required, alias optional)
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","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}' -s | jq

# Edit a device (any subset of group/status/name/phone/alias)
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","alias":"Living Room TV"}' -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)
400 Bad request (e.g. referenced group/VLAN doesn't exist)
401 Missing/invalid X-API-Key
404 Row / device / VLAN 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
S
Description
RADIUS Admin. API and UI for SAR Link admins to mange devices and Radius on the network
Readme
366 KiB
Languages
TypeScript 55.8%
Python 42.1%
CSS 1.3%
HTML 0.4%
Dockerfile 0.3%