added multi user support, move api key to database
build-and-push / build (push) Failing after 35s

This commit is contained in:
2026-08-01 13:16:54 +05:00
parent a7c5fe739a
commit 66073c7891
23 changed files with 2123 additions and 103 deletions
+106 -15
View File
@@ -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