feat: IdP provisioning from YAML file + Playwright e2e tests
Add WG_IDP_CONFIG_FILE env var to seed OIDC/SAML identity providers from a YAML file at startup, enabling GitOps and IaC workflows. Providers are upserted by id (merge strategy preserves manual additions). Convert all e2e tests from NiceGUI User fixture to Playwright async API with --headed and --slowmo flags for visual debugging. Add full OIDC login flow test against the mock-oidc service.
This commit is contained in:
parent
c9ef58a244
commit
3bf6fabcff
13 changed files with 940 additions and 332 deletions
249
TODO.md
249
TODO.md
|
|
@ -3,150 +3,17 @@
|
|||
Migration of Wirezone (Elixir/Phoenix) to Python/NiceGUI.
|
||||
Source: `/home/stefanob/PycharmProjects/personal/wirezone`
|
||||
|
||||
**Test count: 174 (173 passing, 1 skipped) | Coverage: 35%**
|
||||
**Test count: 199 (164 unit + 35 E2E) | Coverage: 35%**
|
||||
**Run:** `uv run pytest` (unit) / `uv run pytest tests/e2e/` (E2E via Playwright)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation — Models, DB, Config ✅
|
||||
|
||||
- [x] `pyproject.toml` with dependencies, `uv sync`
|
||||
- [x] Package directory structure
|
||||
- [x] `wiregui/config.py` — pydantic-settings (DB, Redis, WG, auth, SMTP, logging)
|
||||
- [x] `wiregui/db.py` — async engine, sessionmaker, `init_db()`
|
||||
- [x] `wiregui/redis.py` — Valkey connection pool
|
||||
- [x] All 8 SQLModel models (User, Device, Rule, MFAMethod, OIDCConnection, ApiToken, ConnectivityCheck, Configuration)
|
||||
- [x] Alembic init + initial migration + `alembic upgrade head`
|
||||
- [x] `wiregui/main.py` — app entrypoint
|
||||
- [x] `compose.yml` — PostgreSQL 17 + Valkey 8
|
||||
- [x] `wiregui/utils/time.py` — `utcnow()` helper for naive UTC timestamps
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Auth System — Login + Sessions ✅
|
||||
|
||||
- [x] `wiregui/auth/passwords.py` — bcrypt hash/verify (direct bcrypt, not passlib)
|
||||
- [x] `wiregui/auth/jwt.py` — create/decode JWT via python-jose
|
||||
- [x] `wiregui/auth/session.py` — `authenticate_user()` email/password verification
|
||||
- [x] `wiregui/auth/middleware.py` — HTTP-level auth middleware (available for REST API)
|
||||
- [x] `wiregui/auth/seed.py` — auto-create admin on first startup
|
||||
- [x] `wiregui/pages/login.py` — login page with email/password form
|
||||
- [x] `wiregui/pages/home.py` — authenticated home (redirects to /devices)
|
||||
- [x] Auth guards via `app.storage.user` on each page
|
||||
- [x] Logout clears storage and redirects
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Device UI — User-Facing CRUD ✅
|
||||
|
||||
- [x] `wiregui/pages/layout.py` — shared sidebar + header
|
||||
- [x] `wiregui/utils/network.py` — IPv4/IPv6 allocation (random offset + scan)
|
||||
- [x] `wiregui/utils/crypto.py` — WG keypair + PSK generation via `wg` CLI
|
||||
- [x] `wiregui/utils/wg_conf.py` — WG client `.conf` builder
|
||||
- [x] `wiregui/pages/devices.py` — `/devices` list + create dialog + delete
|
||||
- [x] `/devices/{device_id}` — detail page with stats and config flags
|
||||
- [x] QR code generation + `.conf` download
|
||||
- [x] Full device create/edit form with all wirezone options (description, per-device config overrides, use_default_* toggles with bound inputs, better layout)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: WireGuard Integration ✅
|
||||
|
||||
- [x] `wiregui/services/wireguard.py` — async subprocess: ensure_interface, add/remove_peer, get_peers, set_private_key, set_listen_port
|
||||
- [x] `wiregui/services/events.py` — event bridge: device CRUD → WG + firewall
|
||||
- [x] Device create/delete in UI fires WG events
|
||||
- [x] `wiregui/tasks/__init__.py` — background task registry + cancel_all
|
||||
- [x] `wiregui/tasks/stats.py` — poll WG stats every 60s, update DB
|
||||
- [x] `wiregui/tasks/reconcile.py` — startup reconciliation (diff DB vs WG, add/remove)
|
||||
- [x] `config.py` — `wg_enabled` flag (default False for dev)
|
||||
- [x] Startup: ensure_interface → reconcile → stats_loop (when wg_enabled)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Firewall (nftables) ✅
|
||||
|
||||
- [x] `wiregui/services/firewall.py` — nft CLI: setup_base_tables, masquerade, per-user chains, jump rules, apply_rule, rebuild_all_rules
|
||||
- [x] IPv4/IPv6 aware, TCP/UDP port range support
|
||||
- [x] `wiregui/pages/admin/rules.py` — `/admin/rules` CRUD (action, CIDR, protocol, port, user)
|
||||
- [x] Events: on_rule_created/deleted, on_device_created adds jump rules
|
||||
- [x] Startup: setup_base_tables + setup_masquerade (when wg_enabled)
|
||||
- [x] Edit rule — edit dialog in admin rules page with all fields
|
||||
- [x] Full user chain rebuild on rule update/delete via `_rebuild_user_chain()` in events.py
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: REST API (v0) ✅
|
||||
|
||||
- [x] `wiregui/auth/api_token.py` — token generation (random → sha256), Bearer resolution with expiry + disabled user checks
|
||||
- [x] `wiregui/api/deps.py` — get_db, get_current_api_user, require_admin
|
||||
- [x] `wiregui/schemas/` — Pydantic schemas: UserRead/Create/Update, DeviceRead/Create/Update, RuleRead/Create/Update, ConfigurationRead/Update
|
||||
- [x] `wiregui/api/v0/users.py` — full CRUD (admin only)
|
||||
- [x] `wiregui/api/v0/devices.py` — full CRUD (owner or admin, triggers WG/firewall events)
|
||||
- [x] `wiregui/api/v0/rules.py` — full CRUD (admin only, triggers firewall events)
|
||||
- [x] `wiregui/api/v0/configuration.py` — GET/PUT (admin only, auto-creates singleton)
|
||||
- [x] Mounted on NiceGUI app at `/api/v0`
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Admin UI ✅
|
||||
|
||||
- [x] `/admin/users` — table (email, role, devices, status, last sign-in, method, created), create (email/password/role), edit (email/role/password/disabled), delete with cascading cleanup (devices → WG events, rules)
|
||||
- [x] `/admin/devices` — all devices with user filter, full create form (owner, name, description, all use_default_* toggles with bound override inputs), full edit form, delete with WG events, config + QR on creation
|
||||
- [x] `/admin/settings` — 3 tabs:
|
||||
- Client Defaults (endpoint, DNS, allowed IPs, MTU, keepalive)
|
||||
- Security (VPN session duration, local auth, unpriv device mgmt/config, OIDC auto-disable)
|
||||
- Authentication (OIDC provider CRUD with table + dialog; SAML placeholder for Phase 8)
|
||||
- [x] `/admin/diagnostics` — WG interface status, active peers, connectivity checks, system notifications with clear/clear-all
|
||||
- [x] `wiregui/services/notifications.py` — in-memory deque (capped at 100), add/clear/count/current
|
||||
- [x] Header notification bell badge (admin only, links to diagnostics)
|
||||
- [ ] **TODO:** SAML provider management in Authentication tab
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Advanced Auth (MFA, OIDC, Magic Links, SAML) ✅
|
||||
|
||||
- [x] TOTP MFA (`wiregui/auth/mfa.py`) — secret generation, URI/QR, verification with clock drift tolerance
|
||||
- [x] MFA challenge page (`/mfa`) — 6-digit code entry, multi-method support, last-used tracking
|
||||
- [x] Login page updated: checks for MFA methods after password auth, redirects to `/mfa` if present
|
||||
- [x] OIDC (`wiregui/auth/oidc.py`) — provider registry from Configuration, authlib Starlette integration
|
||||
- [x] OIDC routes (`/auth/oidc/{provider}` + `/auth/oidc/{provider}/callback`) — auth code flow, user lookup/auto-create, refresh token storage in OIDCConnection
|
||||
- [x] Login page shows OIDC provider buttons dynamically from config
|
||||
- [x] OIDC refresh task (`wiregui/tasks/oidc_refresh.py`) — every 10min, refreshes all stored tokens, creates notifications on failure, respects `disable_vpn_on_oidc_error`
|
||||
- [x] Magic links (`/auth/magic-link` + `/auth/magic/{user_id}/{token}`) — request page, signed JWT with 15min expiry, email via aiosmtplib
|
||||
- [x] Email service (`wiregui/services/email.py`) — aiosmtplib send, magic link template
|
||||
- [x] `/account` page — 3 tabs: Profile (details + password change), Two-Factor Auth (TOTP registration with QR + verification, list/delete methods), API Tokens (create with configurable expiry, list, delete)
|
||||
- [x] OIDC providers registered on startup from Configuration
|
||||
- [x] WebAuthn MFA (`wiregui/auth/webauthn.py`) — registration/authentication options generation, response verification, credential storage
|
||||
- [x] SAML (`wiregui/auth/saml.py` + `wiregui/pages/auth_saml.py`) — SP-initiated SSO, metadata endpoint, ACS callback, IdP metadata parsing, attribute mapping
|
||||
- [x] WebAuthn browser-side JS integration in account page — `ui.run_javascript()` calls `navigator.credentials.create()`, serializes response, server verifies and stores credential
|
||||
- [x] SAML provider management UI in admin settings Authentication tab — table + add/delete dialog (config ID, label, XML metadata, sign requests/metadata/assertions/envelopes toggles, auto-create users)
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Background Tasks & VPN Session Management
|
||||
|
||||
- [x] Task scheduler (`wiregui/tasks/__init__.py`) — register/cancel
|
||||
- [x] Stats polling task (Phase 4)
|
||||
- [x] OIDC refresh task (Phase 8)
|
||||
- [x] VPN session expiry task (`wiregui/tasks/vpn_session.py`) — every 60s, finds expired sessions based on `vpn_session_duration` + `last_signed_in_at`, removes WG peers, creates notifications
|
||||
- [x] Connectivity check poller (`wiregui/tasks/connectivity.py`) — fetches URL, stores result in DB, notification on failure
|
||||
- [x] Live stats push — `ui.timer(30, ...)` on `/devices` (table refresh), `/devices/{id}` (RX/TX/handshake/remote IP labels), `/admin/devices` (table refresh)
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Polish, Testing & Deployment
|
||||
|
||||
### Testing (partially done)
|
||||
- [x] pytest + pytest-asyncio setup, conftest with test DB
|
||||
- [x] test_models.py (10 tests), test_auth.py (8 tests), test_utils.py (6 tests), test_services.py (6 tests), test_firewall.py (7 tests)
|
||||
- [x] test_api.py (6 tests) — token generation, resolution, expiry, disabled user
|
||||
- [x] test_notifications.py (9 tests) — add, ordering, count, clear, max cap, to_dict
|
||||
- [x] test_admin.py (13 tests) — user CRUD, cascading deletes, config CRUD, OIDC providers, device overrides
|
||||
- [x] test_mfa.py (11 tests) — TOTP secret gen, URI, code verification (valid/invalid/wrong secret/empty), QR SVG, DB integration, multi-method
|
||||
- [x] test_magic_link.py (4 tests) — token creation/expiry/user mismatch, disabled user rejection
|
||||
- [x] test_account.py (8 tests) — password change flow, API token CRUD, OIDC connection CRUD, refresh token update
|
||||
- [x] test_integration_mfa.py (7 tests) — full TOTP registration flow, MFA blocks login, wrong code, multi-method, last-used tracking, delete allows bypass, disabled user
|
||||
- [x] test_integration_oidc.py (10 tests) — provider config loading, connection create/update, auto-create user, disabled user, refresh token, multi-provider
|
||||
- [x] test_tasks.py (6 tests) — VPN session expiry (expired/unlimited/no-config/disabled user), connectivity check (success/failure with notification)
|
||||
- [ ] HTTP-level integration tests (OIDC redirect/callback flow with respx mocking)
|
||||
|
||||
### Coverage gaps (35% overall — run `uv run pytest --cov=wiregui --cov-report=term-missing --cov-branch`)
|
||||
|
|
@ -181,54 +48,88 @@ Source: `/home/stefanob/PycharmProjects/personal/wirezone`
|
|||
- [ ] `wiregui/auth/saml.py` (0%) — needs mock SAML IdP metadata + response parsing
|
||||
- [ ] `wiregui/auth/webauthn.py` — test verify_registration, verify_authentication with mock credential data
|
||||
|
||||
**E2E page tests (via NiceGUI `User` fixture in `tests/e2e/`):**
|
||||
**E2E page tests (Playwright async API in `tests/e2e/`):**
|
||||
- [x] `tests/e2e/test_login.py` (6 tests) — valid login, invalid password, nonexistent email, disabled user, logout, unauthenticated redirect
|
||||
- [x] `tests/e2e/test_devices.py` (2 tests) — add device full flow, name validation
|
||||
- [x] `tests/e2e/test_account.py` (8 tests) — change password (success/wrong/mismatch/short), create API token, TOTP registration + invalid code, account deletion
|
||||
- [ ] E2E tests for admin pages (users, devices, rules, settings)
|
||||
- [x] `tests/e2e/test_admin_users.py` (10 tests) — page renders, create user, duplicate email, edit role/password, disable/enable, delete, cascade delete, self-delete guard
|
||||
- [x] `tests/e2e/test_idp_seed.py` (9 tests) — IdP YAML seeding (noop/missing/invalid, OIDC/SAML add, upsert, preserve), OIDC button visible, full OIDC login flow via mock-oidc
|
||||
|
||||
**E2E tests still needed:**
|
||||
|
||||
`tests/e2e/test_login.py` — Login & Auth flows (remaining):
|
||||
- [ ] Login with MFA → redirects to /mfa challenge page
|
||||
- [ ] MFA challenge: valid TOTP code → completes login
|
||||
- [ ] MFA challenge: invalid code → shows error, stays on /mfa
|
||||
- [ ] MFA challenge: cancel → returns to /login
|
||||
- [ ] Magic link request page renders, shows success on submit
|
||||
|
||||
`tests/e2e/test_admin_devices.py` — Admin Device Management:
|
||||
- [ ] List all devices across users
|
||||
- [ ] Filter by user → shows only that user's devices
|
||||
- [ ] Create device with full config overrides (DNS, endpoint, MTU, keepalive, allowed IPs)
|
||||
- [ ] Create device with defaults → use_default flags all True
|
||||
- [ ] Edit device name and description → persists
|
||||
- [ ] Edit device config overrides (toggle use_default off, set custom values)
|
||||
- [ ] Delete device → removed from table
|
||||
- [ ] Config dialog shows valid WireGuard config with real server public key
|
||||
- [ ] QR code renders in config dialog
|
||||
|
||||
`tests/e2e/test_admin_rules.py` — Admin Firewall Rules:
|
||||
- [ ] List rules → table shows action, destination, protocol, port, user
|
||||
- [ ] Create accept rule with CIDR → appears in table
|
||||
- [ ] Create drop rule with TCP port range → appears correctly
|
||||
- [ ] Create global rule (no user) → shows "Global"
|
||||
- [ ] Edit rule action (accept → drop) → persists
|
||||
- [ ] Edit rule destination → persists
|
||||
- [ ] Delete rule → removed from table
|
||||
|
||||
`tests/e2e/test_admin_settings.py` — Admin Settings:
|
||||
- [ ] Client defaults: save endpoint, DNS, MTU, keepalive, allowed IPs → persists in DB
|
||||
- [ ] Client defaults: saved values reflected on page reload
|
||||
- [ ] Security: toggle local auth → persists
|
||||
- [ ] Security: change VPN session duration → persists
|
||||
- [ ] Security: toggle unprivileged device management/configuration → persists
|
||||
- [ ] OIDC: add provider → appears in table
|
||||
- [ ] OIDC: delete provider → removed from table
|
||||
- [ ] SAML: add provider → appears in table
|
||||
- [ ] SAML: delete provider → removed from table
|
||||
|
||||
`tests/e2e/test_admin_diagnostics.py` — Admin Diagnostics:
|
||||
- [ ] Page renders WireGuard interface status
|
||||
- [ ] Active peers table shows devices with handshakes
|
||||
- [ ] Connectivity checks table shows recent results
|
||||
- [ ] Notifications list shows system notifications
|
||||
- [ ] Clear single notification → removed
|
||||
- [ ] Clear all notifications → list empty
|
||||
|
||||
`tests/e2e/test_devices_user.py` — User Device Pages:
|
||||
- [ ] Device list shows only own devices (not other users')
|
||||
- [ ] Create device → shows in table with allocated IPs
|
||||
- [ ] Device detail page shows public key, IPs, stats, active config
|
||||
- [ ] Device detail: edit name → persists
|
||||
- [ ] Device detail: toggle config overrides → custom values saved
|
||||
- [ ] Device detail: delete with confirmation → redirects to /devices
|
||||
- [ ] Auto-refresh: stats labels update after timer fires (mock timer)
|
||||
|
||||
`tests/e2e/test_account_extended.py` — Account Page (additional):
|
||||
- [ ] SSO providers section shows connected providers
|
||||
- [ ] SSO providers section shows "No SSO providers" when empty
|
||||
- [ ] MFA: add security key (WebAuthn) → method appears in table (mock navigator.credentials)
|
||||
- [ ] MFA: delete method with confirmation → removed from table
|
||||
- [ ] API tokens: expired token shows "Expired" badge
|
||||
- [ ] API tokens: delete token → removed from table
|
||||
- [ ] API tokens: copy button calls clipboard API
|
||||
- [ ] Danger zone: disabled when only admin
|
||||
- [ ] Danger zone: wrong email in confirmation → shows error
|
||||
|
||||
### Logging (done)
|
||||
- [x] Loguru configured (wiregui/logging.py), no print statements
|
||||
- [x] File logging to `logs/` when `WG_LOG_TO_FILE=true`
|
||||
|
||||
### Deployment ✅
|
||||
- [x] Dockerfile (multi-stage python:3.13-slim)
|
||||
- [x] compose.prod.yml (bridge networking, NET_ADMIN, nftables)
|
||||
- [x] Health endpoint `GET /api/health`
|
||||
- [x] Forgejo CI: test → semver → Docker registry push
|
||||
- [x] AGPL-3.0-or-later license
|
||||
- [x] README.md with features, quick start, env vars, anti-enshittification manifesto
|
||||
|
||||
- [ ] First-run CLI setup command
|
||||
|
||||
---
|
||||
|
||||
## UI Polish & Styling
|
||||
|
||||
### Global styling ✅
|
||||
- [x] Manrope font loaded from Google Fonts as primary UI font (`wiregui/pages/style.py`)
|
||||
- [x] Font applied on all pages (layout, login, MFA challenge)
|
||||
- [x] Dark/light/auto theme toggle in header — cycles with icon button
|
||||
- [x] Theme preference stored in `users.theme_preference` column (migration `a3f1d8e92b01`)
|
||||
- [x] Theme persisted to DB and loaded into session on all login flows (password, MFA, magic link, OIDC, SAML)
|
||||
|
||||
### Account page (`/account`) ✅
|
||||
- [x] Card-based layout matching admin pages (diagnostics, settings)
|
||||
- [x] Account Details: `ui.grid(columns=2)` with bold labels, same as diagnostics
|
||||
- [x] Change Password: inline card section (no modal), outlined inputs, validation
|
||||
- [x] Connected SSO Providers: always visible card with empty state
|
||||
- [x] API Tokens: table with status badges, inline create, copy-to-clipboard with green accent card
|
||||
- [x] MFA: methods table, inline TOTP registration (QR + verify), WebAuthn, empty state
|
||||
- [x] Danger Zone: red left border accent, typed email confirmation, disabled if only admin
|
||||
|
||||
### Settings page (`/admin/settings`) ✅
|
||||
- [x] Converted from tabbed layout to stacked cards (Client Defaults, Security, Authentication)
|
||||
|
||||
### Consistency pass ✅
|
||||
- [x] All buttons solid (`unelevated`) — no outline buttons anywhere
|
||||
- [x] All pages use `w-full p-4` container with `text-h5 q-mb-md` page title
|
||||
- [x] All `text-grey-7` / `text-grey-8` replaced with dark-mode-safe `text-grey`
|
||||
- [x] Sidebar: removed hardcoded `bg-grey-1`, uses theme-aware background
|
||||
- [x] Card titles: `text-subtitle1 text-bold` + `ui.separator()` everywhere
|
||||
|
||||
### Remaining
|
||||
- [ ] SSO Providers: add Status column, "Disconnect" action
|
||||
- [ ] Admin pages (users, devices, rules): apply same card-based styling
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue