This commit is contained in:
+106
-15
@@ -19,16 +19,21 @@ The two `radadmin_*` tables hold only human labels — RADIUS never reads them.
|
||||
- **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
|
||||
- Per-user auth: username/password **login sessions** (`Authorization: Bearer`)
|
||||
plus admin-issued **`X-API-Key`** keys for programmatic access
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
cp .env.example .env # then edit credentials + API_KEY
|
||||
cp .env.example .env # then edit DB credentials
|
||||
```
|
||||
|
||||
Import `radadmin_schema.sql` (after the FreeRADIUS `schema.sql`) so the auth
|
||||
tables exist. On first startup the API seeds a default **`admin` / `admin`**
|
||||
login and forces a password change on first sign-in.
|
||||
|
||||
### `.env`
|
||||
|
||||
| Var | Meaning |
|
||||
@@ -37,7 +42,6 @@ cp .env.example .env # then edit credentials + API_KEY
|
||||
| `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 |
|
||||
|
||||
@@ -58,13 +62,63 @@ venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
|
||||
## Auth
|
||||
|
||||
Every resource endpoint requires the header:
|
||||
Auth is **per-user**, not a shared secret. There are two ways to authenticate a
|
||||
request, both resolving to a user in `radadmin_admins`:
|
||||
|
||||
```
|
||||
X-API-Key: <your API_KEY>
|
||||
```
|
||||
1. **Login session** (the web UI) — `POST /auth/login` with `{username, password}`
|
||||
returns a `token`. Send it on every request as:
|
||||
|
||||
Missing/wrong key → `401`. `/`, `/health`, and `/docs` are open.
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
2. **API key** (scripts / integrations) — an **admin** mints a key in the UI
|
||||
(`POST /apikeys`). The raw key is shown once; send it as:
|
||||
|
||||
```
|
||||
X-API-Key: <key>
|
||||
```
|
||||
|
||||
Missing/invalid credentials → `401`. An action that needs admin rights when
|
||||
you're a regular user → `403`. `/`, `/health`, `/docs`, and `POST /auth/login`
|
||||
are open.
|
||||
|
||||
### Roles
|
||||
|
||||
- **admin** (`is_admin = 1`) — everything below, plus manage users, mint/revoke
|
||||
API keys, and read the activity log.
|
||||
- **user** (regular) — full access to the RADIUS resources (clients, VLANs,
|
||||
devices, …) and can change **their own** password, but cannot create/delete
|
||||
users, reset anyone else's password, manage API keys, or view the log.
|
||||
|
||||
### First deploy
|
||||
|
||||
The database seeds `admin` / `admin` on first run with a forced password change.
|
||||
Sign in, change the password when prompted, then create the real users.
|
||||
|
||||
### Auth & admin endpoints
|
||||
|
||||
| Path | Method | Who | Purpose |
|
||||
|-------------------------------|--------|-------|-------------------------------------------|
|
||||
| `/auth/login` | POST | open | `{username,password}` → `{token, username, is_admin, must_change_password}` |
|
||||
| `/auth/logout` | POST | any | Invalidate the current session token |
|
||||
| `/auth/me` | GET | any | Current user `{id, username, is_admin, must_change_password}` |
|
||||
| `/auth/change-password` | POST | any | Change **your own** password `{current_password, new_password}` |
|
||||
| `/users` | GET | admin | List admin-portal users |
|
||||
| `/users` | POST | admin | Create a user `{username, password, is_admin, force_password_change}` (`force_password_change` defaults to `true`) |
|
||||
| `/users/{id}` | DELETE | admin | Delete a user (not self, not last admin) |
|
||||
| `/users/{id}/reset-password` | POST | admin | Reset another user's password `{new_password}` |
|
||||
| `/apikeys` | GET | admin | List API keys (hashes never returned) |
|
||||
| `/apikeys` | POST | admin | Create a key `{name}` → response includes raw `key` once |
|
||||
| `/apikeys/{id}` | DELETE | admin | Revoke a key |
|
||||
| `/logs` | GET | admin | Paginated activity log (`?username=&action=`) |
|
||||
|
||||
### Activity log
|
||||
|
||||
Every **state-changing** request (`POST`/`PUT`/`PATCH`/`DELETE`) and every auth
|
||||
event (login, logout, password change, user & key management) is recorded to
|
||||
`radadmin_logs` with the acting username, action, a detail string, the client IP,
|
||||
and a timestamp. Admins read it at `GET /logs`.
|
||||
|
||||
## UI integration guide
|
||||
|
||||
@@ -73,7 +127,9 @@ For a management portal the primary resources are **Clients** (`/client`),
|
||||
raw-table access.
|
||||
|
||||
**Base URL (staging):** `http://10.0.1.235:8000`
|
||||
**Every request:** header `X-API-Key: <API_KEY>` (except `/health`).
|
||||
**Every request:** an auth header — `Authorization: Bearer <token>` from
|
||||
`POST /auth/login`, or `X-API-Key: <key>` for an admin-issued key (except
|
||||
`/health` and `/auth/login`).
|
||||
**All bodies:** JSON with `Content-Type: application/json`.
|
||||
|
||||
### Response shapes
|
||||
@@ -254,10 +310,39 @@ standalone **`radadmin_devices`** table (keyed by AP MAC), which FreeRADIUS neve
|
||||
|
||||
## Examples
|
||||
|
||||
Replace host/key to match your deployment.
|
||||
Replace host to match your deployment. The `X-API-Key` header below stands for an
|
||||
**admin-issued key** — or swap any of these for `-H "Authorization: Bearer $TOKEN"`
|
||||
using a token from `/auth/login`.
|
||||
|
||||
```bash
|
||||
# List customers
|
||||
# Log in (default seeded creds on first deploy: admin / admin)
|
||||
curl http://10.0.1.235:8000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin"}' -s | jq
|
||||
|
||||
# Change your own password (required after the first login)
|
||||
curl http://10.0.1.235:8000/auth/change-password \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"current_password":"admin","new_password":"a-better-password"}' -s | jq
|
||||
|
||||
# Create a user (admin only)
|
||||
curl http://10.0.1.235:8000/users \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"ops","password":"changeme","is_admin":false}' -s | jq
|
||||
|
||||
# Mint an API key (admin only) — the raw "key" is shown once in the response
|
||||
curl http://10.0.1.235:8000/apikeys \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"billing-cron"}' -s | jq
|
||||
|
||||
# Read the activity log (admin only)
|
||||
curl http://10.0.1.235:8000/logs?limit=50 \
|
||||
-H "Authorization: Bearer $TOKEN" -s | jq
|
||||
|
||||
# List customers (using an admin-issued API key)
|
||||
curl http://10.0.1.235:8000/customers?limit=50 \
|
||||
-H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq
|
||||
|
||||
@@ -349,7 +434,8 @@ curl http://10.0.1.235:8000/health -s | jq
|
||||
| 201 | Created |
|
||||
| 204 | Deleted (no content) |
|
||||
| 400 | Bad request (e.g. referenced group/VLAN doesn't exist) |
|
||||
| 401 | Missing/invalid `X-API-Key` |
|
||||
| 401 | Missing/invalid credentials (Bearer token or API key) |
|
||||
| 403 | Authenticated but not an admin for an admin-only action |
|
||||
| 404 | Row / client / device / VLAN not found |
|
||||
| 409 | Duplicate / integrity conflict |
|
||||
| 422 | Request body failed validation |
|
||||
@@ -358,16 +444,21 @@ curl http://10.0.1.235:8000/health -s | jq
|
||||
|
||||
```
|
||||
app/
|
||||
main.py FastAPI app, router wiring, auth + CORS
|
||||
main.py FastAPI app, router wiring, activity-log middleware, CORS
|
||||
config.py env-driven settings (pydantic-settings)
|
||||
database.py SQLAlchemy engine/session
|
||||
auth.py X-API-Key dependency
|
||||
auth.py password hashing, sessions, API keys, auth dependencies
|
||||
bootstrap.py first-run seeding of the default admin/admin account
|
||||
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 (incl. radadmin_clients, radadmin_devices)
|
||||
models.py SQLAlchemy models (incl. radadmin_admins/sessions/api_keys/logs)
|
||||
schemas.py Pydantic request/response models
|
||||
routers/ one module per resource
|
||||
auth.py login / logout / me / change-password
|
||||
users.py admin user management (create/delete/reset-password)
|
||||
apikeys.py admin API-key management (create/list/revoke)
|
||||
logs.py admin activity-log view
|
||||
client.py Clients — MAC view over radcheck+radusergroup+customers
|
||||
device.py Devices — AP MACs from radacct + radadmin_devices alias
|
||||
vlan.py VLANs — view over radgroupreply
|
||||
|
||||
Reference in New Issue
Block a user