No description
  • Java 73.1%
  • CSS 9.9%
  • JavaScript 6.6%
  • HTML 4.3%
  • PLpgSQL 4.3%
  • Other 1.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Anders 9361fee5ed Verify against a real build and fix three pre-existing bugs
Actually checked "does this work with mvn quarkus:dev" by installing
JDK 21 + Maven standalone (no root, no system changes) and, further, a
throwaway local PostgreSQL 18 instance to get a full boot rather than
stopping at "no database available". Full details and what was and
wasn't covered are in the README's "Kvar innan produktion" section.

Three real bugs found and fixed, all pre-existing (not introduced by
last round's additions, though three new files had copied the same
wrong import):

- @Authenticated was imported from jakarta.annotation.security, which
  doesn't have that annotation at all. The real one is
  io.quarkus.security.Authenticated. The project would never have
  compiled before this fix.
- CSRF protection was configured in application.properties but
  quarkus-csrf-reactive (now quarkus-rest-csrf) was never added to
  pom.xml, so none of that configuration was doing anything.
- CurrentUser directly injected the OIDC JsonWebToken, which doesn't
  exist as a bean when OIDC is disabled (%dev.quarkus.oidc.enabled=false)
  -- a CDI startup failure in dev mode specifically, unrelated to the
  database. Changed to Instance<JsonWebToken>, resolved lazily.

Also fixed two Panache generics bugs in last round's own new code
(MembershipService.invite(), PortalResource.server()): chaining
.stream() straight off Membership.list(...) / AuditEvent.find(...)
without an explicitly-typed intermediate variable left the generic
type defaulting to PanacheEntityBase instead of the real entity type.

With a live database, the app now boots fully, Flyway migrates
cleanly, the new scheduled jobs run without error, and the Qute
templates were confirmed to render correctly in both languages against
real view-model data (checked via a temporary local-only test route,
never committed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWeXEhdkA4nj8BRdjKjMwh
2026-09-14 19:56:51 +02:00
docs Complete the remaining "Kvar innan produktion" checklist items 2026-09-14 19:21:26 +02:00
src/main Verify against a real build and fix three pre-existing bugs 2026-09-14 19:56:51 +02:00
.gitignore Lägg in abon-panel-källkoden i repot istället för zip 2026-09-14 18:35:20 +02:00
compose.yml Lägg in abon-panel-källkoden i repot istället för zip 2026-09-14 18:35:20 +02:00
Dockerfile Lägg in abon-panel-källkoden i repot istället för zip 2026-09-14 18:35:20 +02:00
pom.xml Verify against a real build and fix three pre-existing bugs 2026-09-14 19:56:51 +02:00
proxmox-setup.sh Lägg in abon-panel-källkoden i repot istället för zip 2026-09-14 18:35:20 +02:00
README.md Verify against a real build and fix three pre-existing bugs 2026-09-14 19:56:51 +02:00
setup.sh Lägg in abon-panel-källkoden i repot istället för zip 2026-09-14 18:35:20 +02:00

ABON-panel — backend v1.0

Kundportal för ABON:s VPS-kunder. Proxmox bakom ett broker-lager, Pocket ID för inloggning.

Stack: Java · Quarkus · PostgreSQL · Flyway (migrationer) · JWT (via quarkus-oidc mot Pocket ID).

Version 1.0 gör: logga in, se sina VPS:er, start/stop/reboot, brandväggsregler. Inget mer.

Kom igång

# På panel-VPS:en
git clone <repo> /opt/docker/abon-panel && cd /opt/docker/abon-panel
./setup.sh                 # genererar secrets, skapar .env
vim .env                   # fyll i OIDC_CLIENT_ID, PROXMOX_*, NEWT_*
docker compose up -d

# På en Proxmox-nod
./proxmox-setup.sh         # skapar panel@pve + två tokens

Lokalt: mvn quarkus:dev (OIDC avstängt i dev-profilen).

Var säkerheten sitter

Fil Vad den skyddar mot
auth/AuthorizationService.java IDOR — enda vägen till en Service
proxmox/ProxmoxEndpointGuard.java Att VM.Config.Network används till NIC-flytt
proxmox/ProxmoxBroker.java Att vmid kommer från klienten; brandväggslåsningar
job/JobWorker.java Att ett gammalt jobb körs efter indragen behörighet
audit/AuditService.java Ändrad historik — hash-kedja per kund
V1__init.sql UPDATE/DELETE på audit_event, via triggers
application.properties Spoofad X-Forwarded-For, CSRF, tokenlivslängd
job/JobService.java Rate limit; en client som spammar start/stop
membership/MembershipService.java En kund utan ägare; en obekräftad inbjudan som ger åtkomst
firewall/FirewallReconciliationJob.java Att brandväggsdrift går obemärkt förbi
audit/AuditVerificationJob.java Att en bruten audit-kedja går obemärkt förbi

Fyra beslut värda att förstå innan du ändrar

Proxmox äger tillstånd, panelen äger relationer. VM-status hämtas live vid varje visning. Går Proxmox inte att nå visas reachable: false — aldrig gammal data som om den vore aktuell.

Klienten ser aldrig vmid. Den skickar service.id (UUID). vmid slås upp i databasen efter att ägarskapet är verifierat. En manipulerad request kan därför inte peka på någon annans VM.

Panelen äger hela brandväggen på VM-nivå. Varje ändring skriver om <vmid>.fw helt. enable, ipfilter, macfilter och policy_in: DROP skrivs varje gång och verifieras efteråt. "Av" i kundens gränssnitt betyder tillåt-allt-regler, inte att anti-spoofing försvinner.

JWT kompletteras med DB-kontroll. Access token lever 5 minuter. Varje destruktiv operation kontrollerar dessutom medlemskap och tjänstestatus mot databasen, både när jobbet köas och i workern innan Proxmox anropas. Läsning kan släpa 5 minuter; att göra något kan den inte.

Vad som medvetet inte finns än

Uppgradering, tillval, backup, Invoice Ninja, webbhotell, mail, Foto Cloud. Strukturen bär dem — service heter inte server, customer har invoice_ninja_client_id, statusfältet har SUSPENDED och PENDING_UPGRADE — men inget av det är byggt.

Suspend sätts för hand i 1.0:

UPDATE service SET status='SUSPENDED', suspended_at=now() WHERE id='...';

Broker vägrar då starta tjänsten, så det räcker som spärr.

Kvar innan produktion

  • Rate limiting på strömoperationer (max N per minut per tjänst) — job/JobService.java, config job.rate-limit.max-per-minute (default 6/min).
  • Qute-vyer — web/PortalResource.java + templates/PortalResource/*.html, static assets under META-INF/resources/static/. Read-only rendering; all mutations still go through the existing JSON API from browser-side JS (static/app.js).
  • Inbjudningsflöde — membership/MembershipService.java + api/MembershipResource.java (POST /api/customers/{id}/invitations, POST /api/invitations/{token}/accept). No outbound email yet: the invite endpoint returns the token/link for an owner to relay manually.
  • Medlemshantering (lägg till/ta bort/ändra roll, med minst-en-ägare-regeln) — same MembershipResource/MembershipService, enforces MembershipService.LastOwnerException (409) on demoting/removing the last owner.
  • Periodisk avstämning brandvägg: panelens DB mot Proxmox — firewall/FirewallReconciliationJob.java, config firewall.reconciliation.interval (default 5m). Read-only: logs drift at ERROR level, never auto-corrects.
  • Schemalagd verifiering av audit-kedjan — audit/AuditVerificationJob.java, config audit.verification.interval (default 1h).
  • PUB/DPA och villkorstext för supportåtkomst — draft in docs/pub-dpa-support-access.md. Not legal advice, not reviewed by a lawyer — must not be published as-is.

Update: verified end-to-end, including a live boot against a real database. JDK 21 + Maven 3.9.9 and a throwaway PostgreSQL 18 instance were installed standalone for this check (all as the regular user, no root, no system changes, all removed afterwards). Results:

  • mvn package builds clean.
  • mvn quarkus:dev, pointed at the local Postgres via DB_HOST, starts fully: Flyway applies V1__init.sql to a fresh database with no errors, the app reaches Listening on: http://localhost:8080, and GET /q/health reports the database connection as UP.
  • The new scheduled jobs ran on their own on startup and completed without error: AuditVerificationJob ("0 customer chain(s) OK") and FirewallReconciliationJob ("0 active services checked, no drift").
  • Static assets serve correctly: /static/app.js, /static/style.css, /static/fonts/fonts.css, /static/img/logotyp.svg all returned 200 with the expected byte sizes.
  • Protected routes correctly reject unauthenticated requests (/, /servers, /api/services all returned 403, as expected with OIDC disabled in dev and no way to complete a real login there).
  • The Qute templates render correctly with real data, in both languages. Using a temporary, local-only, unauthenticated resource (never committed) that called PortalResource.Templates.servers(...) and .server(...) directly with fake view-model data: all three renders (server list in English, server list in Swedish, server detail) returned 200 with no exceptions in the log, and the output HTML was inspected directly — headings, the "N servers/server in drift" count line, the language toggle button (correctly offering the other language), the customer switcher dropdown, all four power buttons with correct per-status disabled attributes, and the firewall table headers all rendered exactly as intended in the correct language.

Three real, pre-existing bugs were found and fixed along the way — none of them related to this round's additions specifically, they were latent in the codebase and only surfaced once it was actually built:

  1. @Authenticated was imported from the wrong package (jakarta.annotation.security.Authenticated, which doesn't exist — the real annotation is io.quarkus.security.Authenticated) across AuditResource, MeResource, ServiceResource, SupportResource (pre-existing, from before this round) and MembershipResource, PortalResource, RootResource (new this round, copied the same wrong import). The project would never have compiled before this fix.
  2. CSRF protection was configured but not installed: application.properties sets quarkus.csrf-reactive.*, but quarkus-csrf-reactive (now named quarkus-rest-csrf) was never added to pom.xml, so every one of those settings was silently ignored — confirmed by mvn quarkus:dev logging them as unrecognized. Added the dependency; rest-csrf now shows up in the "Installed features" list at boot.
  3. CurrentUser could never start in dev mode: it directly @Injects the OIDC JsonWebToken, but %dev.quarkus.oidc.enabled=false means no such bean exists in dev — a CDI validation failure at startup, independent of the database. Changed the injection to Instance<JsonWebToken>, resolved lazily instead of required at boot.

Two Panache generics bugs were also found and fixed in this round's own new code specifically (MembershipService.invite(), PortalResource.server()): chaining .stream() straight off Membership.list(...) / AuditEvent.find(...).list() without first assigning to an explicitly-typed List<Membership> / List<AuditEvent> variable left Java unable to infer the generic type, defaulting it to PanacheEntityBase instead. Fixed by introducing the typed intermediate variable, matching the pattern already used elsewhere in this codebase (e.g. AuditResource.list()).

What this check did NOT cover: the actual Proxmox integration (no real Proxmox cluster reachable from this environment — ProxmoxBroker calls were never exercised), the actual OIDC login flow against Pocket ID (dev mode had OIDC disabled; a real login was never completed against a real IdP), and the CSRF protection's actual behavior against a real form submission or fetch call. Those still need to be checked against the real staging/production environment.

Follow-up items these additions surfaced, not yet done

  • No outbound email for invitations — the accept link/token is only returned in the API response right now.
  • Audit log timestamps in the new UI are rendered via Instant's default toString() (ISO-8601), not a human-friendly local format.
  • FirewallReconciliationJob and AuditVerificationJob only log on drift — no alerting integration (email/Slack/etc.) exists yet.