init, add/delete/edit vlans and devices
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
# 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: <your 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=<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 `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
|
||||
```
|
||||
Reference in New Issue
Block a user