Skip to content

Commit a6088af

Browse files
tridgeclaude
andcommitted
README: web admin UI section, new keydb.py flag commands, runner usage
Add a Web Admin UI section covering install, first-admin bootstrap, standalone gunicorn, and Apache ProxyPass deployment, plus an env-var configuration table and a pointer to scripts/run_webui.sh for local testing. Document the new keydb.py setflag / clearflag / flags subcommands (used by the web UI for the admin and bidi_sign role flags), and note the existing resettimestamp command. Replace the Testing section with the current run_tests.py usage: -j N parallelism, -j 0 for one-worker-per-test, --list, --timing, and pytest selector pass-through for targeting specific tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 662d58a commit a6088af

1 file changed

Lines changed: 143 additions & 9 deletions

File tree

README.md

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ The `keydb.py` script provides comprehensive database management:
255255
./keydb.py setpass PORT2 NewPassPhrase # Change passphrase
256256
./keydb.py setport1 PORT2 NewPORT1 # Change user port
257257

258+
# Reset signing replay-protection timestamp (e.g. after clock skew)
259+
./keydb.py resettimestamp PORT2
260+
261+
# Flag bits (used by the web admin UI to mark admins)
262+
./keydb.py setflag PORT2 admin # Grant admin to entry
263+
./keydb.py clearflag PORT2 admin # Revoke admin from entry
264+
./keydb.py flags PORT2 # Show flags currently set
265+
258266
# Examples:
259267
./keydb.py add 10006 10007 'Engineering' SecurePass123
260268
./keydb.py setname 10007 'QA Team'
@@ -275,6 +283,119 @@ The `keydb.py` script provides comprehensive database management:
275283
- **Firewall**: Configure firewall rules to allow only necessary ports
276284
- **Access Control**: Limit access to the server and keydb.py script
277285

286+
## Web Admin UI
287+
288+
The `webadmin/` directory contains a small Flask app that lets users
289+
manage their own entry through the browser, and lets users with the
290+
`admin` flag manage every entry. It writes to the same `keys.tdb` the
291+
running `udpproxy` reads, so changes go live within ~5 seconds with no
292+
proxy restart.
293+
294+
### Roles and login
295+
296+
- Anyone can log in by entering **either `port1` or `port2`** plus the
297+
passphrase set with `keydb.py`.
298+
- Users with `KEY_FLAG_ADMIN` set on their entry get the admin UI (list
299+
all entries, edit any, grant/revoke admin, add/delete entries).
300+
- Everyone else gets the self-service UI (rename their own entry, rotate
301+
their own passphrase, reset their signing timestamp).
302+
303+
### Install dependencies
304+
305+
```bash
306+
source venv/bin/activate
307+
pip install Flask Flask-WTF gunicorn
308+
```
309+
310+
### Bootstrap the first admin
311+
312+
The web UI has no out-of-band admin; you promote an existing entry to
313+
admin from the CLI on the server:
314+
315+
```bash
316+
./keydb.py setflag 10007 admin
317+
```
318+
319+
That entry can then log in to the web UI as an admin and grant the flag
320+
to others through the web interface.
321+
322+
### Run standalone
323+
324+
```bash
325+
# Set a stable secret so sessions survive restarts
326+
export WEBADMIN_SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(32))')
327+
328+
# Path to keys.tdb (defaults to ./keys.tdb relative to cwd)
329+
export WEBADMIN_KEYDB_PATH=/path/to/proxy/keys.tdb
330+
331+
# Bind to localhost or a public address. Use --certfile/--keyfile for TLS.
332+
gunicorn -w 2 -b 127.0.0.1:8080 webadmin.wsgi:application
333+
```
334+
335+
For local HTTP-only development, also set `WEBADMIN_INSECURE_COOKIES=1`
336+
so the session cookie is sent without the `Secure` flag. **Do not set
337+
this in production.**
338+
339+
For local development from `test/` (or any directory containing a
340+
`keys.tdb`), there's a helper that wires the env vars up for you:
341+
342+
```bash
343+
cd test
344+
../scripts/run_webui.sh # http://127.0.0.1:8080
345+
WEBADMIN_PORT=9000 ../scripts/run_webui.sh
346+
WEBADMIN_TLS=1 ../scripts/run_webui.sh # uses fullchain.pem / privkey.pem in cwd
347+
```
348+
349+
It uses `gunicorn` if available and falls back to Flask's dev server
350+
otherwise.
351+
352+
### Run behind Apache (`ProxyPass`)
353+
354+
The intended deployment is for the web UI to listen on
355+
`127.0.0.1:8080` and have an existing Apache vhost forward an
356+
externally-managed URL to it. Apache terminates TLS; the app trusts
357+
`X-Forwarded-*` headers from Apache when `WEBADMIN_BEHIND_PROXY=1`:
358+
359+
```apache
360+
<VirtualHost *:443>
361+
ServerName support.example.org
362+
SSLEngine on
363+
SSLCertificateFile /etc/letsencrypt/live/support.example.org/fullchain.pem
364+
SSLCertificateKeyFile /etc/letsencrypt/live/support.example.org/privkey.pem
365+
366+
ProxyPreserveHost On
367+
ProxyPass /admin/ http://127.0.0.1:8080/
368+
ProxyPassReverse /admin/ http://127.0.0.1:8080/
369+
RequestHeader set X-Forwarded-Prefix /admin
370+
RequestHeader set X-Forwarded-Proto https
371+
</VirtualHost>
372+
```
373+
374+
Required Apache modules: `mod_proxy`, `mod_proxy_http`, `mod_headers`,
375+
`mod_ssl`. Then run gunicorn with the proxy flag set:
376+
377+
```bash
378+
export WEBADMIN_BEHIND_PROXY=1
379+
export WEBADMIN_SECRET_KEY=...
380+
export WEBADMIN_KEYDB_PATH=/path/to/proxy/keys.tdb
381+
gunicorn -w 2 -b 127.0.0.1:8080 webadmin.wsgi:application
382+
```
383+
384+
The browser hits `https://support.example.org/admin/` and Apache forwards
385+
to the local gunicorn. URL-building inside the app uses
386+
`X-Forwarded-Prefix` so links resolve correctly under `/admin/`.
387+
388+
### Configuration reference
389+
390+
All settings can be overridden via environment variables:
391+
392+
| Variable | Default | Purpose |
393+
|---|---|---|
394+
| `WEBADMIN_SECRET_KEY` | random per process | Flask session + CSRF signing key. Set a stable value in production. |
395+
| `WEBADMIN_KEYDB_PATH` | `keys.tdb` (cwd-relative) | Path to the TDB file. |
396+
| `WEBADMIN_BEHIND_PROXY` | unset | Set to `1` when fronted by Apache to honour `X-Forwarded-*` headers. |
397+
| `WEBADMIN_INSECURE_COOKIES` | unset | Set to `1` only for local HTTP dev; allows session cookie without `Secure` flag. |
398+
278399
## Troubleshooting
279400

280401
### Common Issues
@@ -324,20 +445,33 @@ journalctl -f | grep udpproxy
324445

325446
## Testing
326447

327-
### Automated CI Testing
328-
329-
UDPProxy includes comprehensive CI testing to validate UDP and TCP connection functionality:
448+
The test suite covers UDP/TCP connection scenarios, the `keydb.py` CLI,
449+
and the web admin UI (auth, role guards, CSRF, concurrent writes).
330450

331451
```bash
332-
# Run all tests locally
333-
./run_tests.sh
452+
# Run everything (builds udpproxy, runs all three phases)
453+
./scripts/run_tests.sh
334454

335-
# Run specific test suites
336-
source venv/bin/activate
337-
pytest tests/test_connections.py -v
338-
pytest tests/test_authentication.py -v
455+
# Run in parallel via pytest-xdist (-j N workers, each with its own
456+
# tmpdir + port pair so they don't collide)
457+
./scripts/run_tests.sh -j 4
458+
459+
# List every test without running anything
460+
./scripts/run_tests.py --list
461+
462+
# Run specific tests (any pytest selector works). --no-build skips the
463+
# make step for fast iteration.
464+
./scripts/run_tests.py --no-build tests/webadmin/
465+
./scripts/run_tests.py --no-build tests/test_connections.py::TestUDPConnections
466+
./scripts/run_tests.py --no-build -j 4 tests/webadmin/test_admin.py::TestAdminEdit::test_cannot_revoke_last_admin
339467
```
340468

469+
When invoked with no test selectors, the runner builds and then runs
470+
three separate pytest invocations (connection tests, authentication
471+
tests, webadmin tests) — they're kept separate because each phase has
472+
different cwd / `keys.tdb` / process expectations. With selectors, it
473+
runs one pytest invocation against exactly what you passed.
474+
341475
## Contributing
342476

343477
1. **Code Style**: Follow existing C++ and Python conventions

0 commit comments

Comments
 (0)