Files
MrBlake f36d6be511
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
feat: add NetBox plugin store
2026-08-24 20:51:25 +02:00

363 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NetBox Plugin Store
Der Store ist eine eigenständige PHP-Anwendung für einen kuratierten NetBox-Plugin-Katalog. Er liest Repositories von Forgejo/Gitea (standardmäßig `git.mrblake.cc`) ein, erkennt Plugin-Metadaten, rendert die README der exakt synchronisierten Commit-Revision und veröffentlicht erst nach einer getrennten Admin-Freigabe einen API-v1-Katalog.
Die native Installation mit PHP und Apache ist der primäre Betriebsweg. Docker ist optional.
## Funktionsumfang
- responsive deutschsprachige Store-, Such-, Filter- und Plugin-Detailseiten;
- vollständige Forgejo-/Gitea- und GitHub-Discovery für Benutzer und Organisationen;
- Erkennung über Topic, `netbox-plugin.json|yaml`, `pyproject.toml`, `setup.py` und `PluginConfig`;
- commitgenaue Reads für Manifest, Projektmetadaten, Tree, PluginConfig und README;
- CommonMark-Rendering ohne eingebettetes HTML; relative Links und Bilder werden auf die Raw-URL des Commit-SHA umgeschrieben;
- eigener Admin-Login mit Argon2id, CSRF-Schutz, Session-Härtung und persistentem Login-Rate-Limit;
- Freigabe/Ablehnung von Quellen, Plugins und jedem Release-Artefakt; Admin-Metadatenkorrekturen und Audit-Log;
- tatsächlicher Download jedes öffentlichen Wheel-Artefakts mit SHA-256 und Größe; sicherheitsrelevante Änderungen setzen die Freigabe zurück;
- öffentlicher, freigegebener JSON-Katalog unter `/api/v1/`;
- atomar geschriebener JSON-Datastore als Zero-Setup-Standard oder optional MariaDB über PDO;
- einmaliger Sync sowie signalverträglicher Watch-Modus ohne Cron/Celery;
- pro Source eine prozessübergreifende exklusive Sync-Lease sowie aggregierte Limits für Laufzeit, Requests, Bytes, Repositories und Releases.
Wichtig: Ein eingelesenes und freigegebenes Repository ist noch nicht automatisch installierbar. API v1 veröffentlicht ausschließlich separat freigegebene `.whl`-Artefakte. Fehlt ein Wheel, bleibt das Plugin als Katalogeintrag sichtbar und `latest_version` ist `null`; die UI zeigt „Kein installierbares Release“.
## 1. Native Installation (empfohlen)
### Voraussetzungen
- PHP 8.3 oder neuer (getestet mit PHP 8.4);
- Apache 2.4 mit `mod_rewrite` und `mod_headers`;
- Composer 2;
- PHP-Erweiterungen: `curl`, `dom`, `iconv`, `intl`, `json`, `mbstring`, `pdo` und für MariaDB zusätzlich `pdo_mysql`; für einen graceful beendbaren dauerhaften `--watch`-Prozess wird `pcntl` empfohlen;
- Schreibzugriff des Apache-/Sync-Benutzers auf `store/data`.
Beispiel für Debian/Ubuntu (Paketnamen können je Distribution/PHP-Repository abweichen):
```bash
sudo apt update
sudo apt install apache2 libapache2-mod-php php-cli php-curl php-xml php-intl php-mbstring php-mysql composer
sudo a2enmod rewrite headers
```
### Anwendung installieren
```bash
sudo mkdir -p /opt/netbox-plugin-store
sudo chown "$USER":"$USER" /opt/netbox-plugin-store
git clone <REPOSITORY-URL> /opt/netbox-plugin-store
cd /opt/netbox-plugin-store/store
composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative
cp .env.example .env
mkdir -p data
sudo chown -R www-data:www-data data
sudo chmod 750 data
```
Der JSON-Pfad in `.env.example` ist relativ zum `store/`-Verzeichnis und funktioniert nativ ohne Änderung:
```dotenv
STORE_DB_DRIVER=json
STORE_JSON_PATH=./data/store.json
```
Der Datastore verwendet einen prozessübergreifenden `flock`, schreibt in eine temporäre Datei, synchronisiert sie und ersetzt anschließend die alte Datei atomar. Eine separate nicht-blockierende Lease-Datei hält zusätzlich jeden Source-Sync für seine gesamte Laufzeit exklusiv; ein Prozessabbruch gibt die Kernel-Sperre frei. Verwende ein lokales Dateisystem; geteilte/NFS-Dateisysteme bieten nicht immer die benötigten Lock-/Rename-Garantien. Für mehrere Web-Hosts ist MariaDB die bessere Wahl; dort übernimmt `GET_LOCK` die Sync-Lease.
### Admin-Zugang sicher konfigurieren
Es gibt absichtlich keine Standard-Zugangsdaten. Erzeuge einen langen Zufallswert als Session-Secret und einen Argon2id-Hash:
```bash
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'
php bin/console hash-password 'ein-langes-zufaelliges-passwort'
```
Trage Benutzername, ausgegebenen Hash und Secret vollständig in `.env` ein:
```dotenv
STORE_ADMIN_USERNAME=admin
STORE_ADMIN_PASSWORD_HASH=$argon2id$...
STORE_SESSION_SECRET=<mindestens-32-zeichen>
```
Sind alle drei Werte leer, bleibt `/admin` deaktiviert. Ist nur ein Teil gesetzt, bricht die Anwendung fail-closed mit einer Konfigurationsmeldung ab. Vermeide Passwörter in der Shell-History; alternativ liest `hash-password` den Wert aus `STORE_ADMIN_PASSWORD_TO_HASH`.
Setze außerdem die öffentliche URL. Sichere Cookies richten sich standardmäßig nach ihrem Protokoll:
```dotenv
APP_ENV=production
STORE_PUBLIC_URL=https://plugins.example.com
# Optionaler expliziter Override:
STORE_COOKIE_SECURE=true
```
Ein Production-Quickstart über `http://localhost` funktioniert mit `STORE_PUBLIC_URL=http://localhost:3000` und `STORE_COOKIE_SECURE=false`; für einen öffentlichen Betrieb ist HTTPS erforderlich.
### Apache konfigurieren
Nur `store/public` darf DocumentRoot sein. Passe Pfad und Servernamen in [deploy/apache-vhost.conf.example](deploy/apache-vhost.conf.example) an:
```bash
sudo cp deploy/apache-vhost.conf.example /etc/apache2/sites-available/netbox-plugin-store.conf
sudo editor /etc/apache2/sites-available/netbox-plugin-store.conf
sudo a2ensite netbox-plugin-store
sudo apache2ctl configtest
sudo systemctl reload apache2
```
Die Beispielkonfiguration erlaubt `.htaccess` ausschließlich im Store-DocumentRoot. Alternativ können Rewrite- und Header-Regeln aus `public/.htaccess` direkt in den vHost übernommen und `AllowOverride None` gesetzt werden. Für Produktion HTTPS direkt mit Apache `mod_ssl`/ACME konfigurieren und `STORE_PUBLIC_URL` auf `https://…` setzen; ein Reverse Proxy ist nicht erforderlich.
Übernimm für den produktiven PHP-SAPI außerdem die sicherheitsrelevanten Werte aus `deploy/php-production.ini` (insbesondere `display_errors=Off`, `log_errors=On` und `expose_php=Off`) in deine PHP-Konfiguration und lade Apache neu. Der Front Controller deaktiviert die Fehlerausgabe zusätzlich selbst.
### Bootstrap und erster Sync
```bash
cd /opt/netbox-plugin-store/store
sudo -u www-data php bin/console bootstrap
sudo -u www-data php bin/console sync
```
Wenn noch keine Quelle existiert, legt `bootstrap` (und standardmäßig auch `sync`) aus den `STORE_DEFAULT_*`-Werten eine aktive, freigegebene Forgejo-Quelle an. Standardwerte:
- Provider `forgejo`;
- Basis `https://git.mrblake.cc`;
- API `https://git.mrblake.cc/api/v1`;
- Benutzer `MrBlake` entsprechend `/api/v1/users/MrBlake/repos`.
Mit `--no-bootstrap` wird die automatische Anlage unterdrückt. Einzelne Quellen lassen sich mit `--source=slug` synchronisieren; `--fail-fast` beendet den Lauf beim ersten Repositoryfehler.
Jeder Source-Lauf ist mit konservativen, konfigurierbaren Gesamtbudgets begrenzt. Eine Überschreitung beendet ihn fail-closed als `failed`; neue Freigaben entstehen dabei nicht. Die Defaults stehen auch in `.env.example`:
```dotenv
STORE_SYNC_MAX_SECONDS=900
STORE_SYNC_MAX_REQUESTS=2500
STORE_SYNC_MAX_BYTES=1073741824
STORE_SYNC_MAX_REPOSITORIES=2000
STORE_SYNC_MAX_RELEASES=1000
```
Die Limits zählen Redirects und Retries als weitere Requests sowie Metadaten und Artefakte gemeinsam gegen das Byte-Budget. Repository- und Release-Zähler werden nach jeder validierten Provider-API-Seite vor deren Übernahme in den Akkumulator geprüft; dadurch kann höchstens die gerade empfangene Seite zusätzlich im Speicher liegen. `STORE_SYNC_MAX_RELEASES` ist zugleich auf die API-v1-Grenze von 1.000 begrenzt. Ein zweiter Lauf derselben Source wird unabhängig vom Alter eines sichtbaren Run-Eintrags abgewiesen; nach einem Prozessabbruch wird der verwaiste Eintrag erst nach erfolgreichem Erwerb der exklusiven Lease geschlossen.
### Regelmäßiger Sync mit systemd
Die mitgelieferte Unit ist ein `oneshot`-Dienst mit einem 15-Minuten-Timer. Passe gegebenenfalls `/opt/netbox-plugin-store` und `www-data` an:
```bash
sudo cp deploy/netbox-plugin-store-sync.service /etc/systemd/system/
sudo cp deploy/netbox-plugin-store-sync.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now netbox-plugin-store-sync.timer
systemctl list-timers netbox-plugin-store-sync.timer
journalctl -u netbox-plugin-store-sync.service
```
Alternativ kann ein dauerhafter Prozess verwendet werden:
```bash
sudo -u www-data php bin/console sync --watch --interval=900
```
Oder per Cron (keine überlappenden Läufe; der Store blockiert parallele Syncs zusätzlich):
```cron
*/15 * * * * cd /opt/netbox-plugin-store/store && /usr/bin/php bin/console sync >>/var/log/netbox-plugin-store-sync.log 2>&1
```
## 2. MariaDB statt JSON (optional)
Der MariaDB-Adapter nutzt dieselbe Repository-/State-Schnittstelle. Er legt beim ersten Start automatisch eine einzelne, per InnoDB-Transaktion und `SELECT … FOR UPDATE` geschützte State-Zeile an. Das äquivalente Schema liegt unter `config/schema.sql`.
```dotenv
STORE_DB_DRIVER=mariadb
STORE_MARIADB_DSN=mysql:host=127.0.0.1;port=3306;dbname=netbox_store;charset=utf8mb4
STORE_MARIADB_USER=netbox_store
STORE_MARIADB_PASSWORD=<starkes-passwort>
```
Der Datenbankbenutzer benötigt `CREATE`, `SELECT`, `INSERT` und `UPDATE` auf der Store-Datenbank. Sichere die Datenbank wie andere Produktionsdaten regelmäßig.
## 3. Quellen und automatische Erkennung
Neue Quellen werden in `/admin` zunächst als `pending` angelegt und müssen per POST/CSRF freigegeben werden. Verfügbare Provider:
- `forgejo` für Forgejo und Gitea;
- `github` für GitHub-User oder -Organisationen.
Tokens werden nur aus dem in der Source hinterlegten Namen einer Umgebungsvariable gelesen, beispielsweise `GITEA_TOKEN` oder `GITHUB_TOKEN`; Tokenwerte werden nie im Datastore gespeichert. Ein Authorization-Header wird nur gesendet, wenn die Ziel-Origin exakt der `apiUrl`-Origin entspricht, und nach jedem Redirect neu bewertet.
Private GitHub-Repositories dürfen mit einem entsprechend berechtigten Token für Metadaten und README eingelesen werden. Ihre Release-Assets werden in v1 bewusst fail-closed übersprungen: Der Store betreibt keinen Artefakt-Cache, und der Host-Agent besitzt keine GitHub-Credentials für einen späteren Download. Der Sync zeigt dafür einen Moderationshinweis und zieht eventuell ältere installierbare Releases dieses Repositorys zurück. Installierbar sind von GitHub ausschließlich öffentliche `.whl`-Assets über deren `browser_download_url`; Provider-Tokens werden niemals an Asset-Hosts weitergereicht.
Die zuverlässigste Discovery gelingt mit einem Manifest im Repository-Root:
```json
{
"name": "Mein NetBox Plugin",
"summary": "Kurze Beschreibung",
"package_name": "netbox-mein-plugin",
"import_name": "netbox_mein_plugin",
"version": "1.2.3",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8"
}
```
Alternativ werden PEP-621/Poetry/Setuptools-Metadaten, Entry Points, `setup.py` und `PluginConfig` ausgewertet. Dynamische Setuptools-Versionen über `tool.setuptools.dynamic.version.attr` werden aus dem am gleichen Commit gepinnten Modul gelesen. Bleiben Paket, Top-Level-Import oder Kompatibilitätsgrenzen unbekannt, blockiert die Admin-Seite die Freigabe mit einer klaren Meldung. Dort können die Felder korrigiert werden; diese Overrides bleiben bei späteren Syncs erhalten.
## 4. Ein Wheel veröffentlichen und freigeben
Die aktuell eingelesenen MrBlake-Repositories besitzen überwiegend keine Forgejo-Release-Wheels. Sie können nach Plugin-Freigabe im Store erscheinen, sind aber nicht automatisch installierbar.
Empfohlener Release-Ablauf im Plugin-Repository:
```bash
python -m pip install --upgrade build
python -m build
```
1. Einen unveränderlichen Git-Tag setzen.
2. In Forgejo einen Release für diesen Tag erstellen.
3. Das erzeugte `dist/*.whl` als Release-Asset hochladen.
4. Den Store synchronisieren.
5. Im Admin zunächst fehlende Plugin-Metadaten korrigieren/freigeben und anschließend genau das neue Release-Artefakt freigeben.
Beim Sync lädt der Store das Wheel tatsächlich, berechnet SHA-256 und `artifact_size` und speichert den Commit-SHA. Neue Releases erben niemals die Plugin-Freigabe. Ändern sich URL, Hash, Größe, Commit, Paket-/Importname oder Kompatibilitätsgrenzen, werden Plugin bzw. Release wieder `pending`. Entfernte Upstream-Releases werden als zurückgezogen markiert und verschwinden sofort aus dem öffentlichen Katalog.
Ein Git-Tag oder Quellcode-Archiv allein erzeugt kein installierbares Release. Der Store erzeugt keine Ersatzartefakte; NetBox-Client und Host-Agent installieren ausschließlich separat veröffentlichte und freigegebene Wheels.
## 5. Moderationsablauf
Unter `/admin` stehen ausschließlich POST-Aktionen mit CSRF-Schutz bereit:
1. Source prüfen und freigeben;
2. Source synchronisieren;
3. Plugin-Metadaten prüfen/korrigieren und Plugin freigeben;
4. Wheel-Hash, Größe, Version und Kompatibilität prüfen und Release separat freigeben.
Der Store erzeugt beim Release-Approval einen kanonischen `approved_payload_sha256`. Die öffentliche API liefert ein Artefakt nur, wenn der aktuelle Payload weiterhin exakt zu diesem Approval-Hash passt. Das ist unabhängig von den sichtbaren Statusfeldern eine zusätzliche Defense-in-Depth-Prüfung.
## 6. API v1
Alle Routen tolerieren einen abschließenden Slash:
```text
GET /api/v1/plugins/
GET /api/v1/plugins/:slug/
GET /api/v1/plugins/:slug/releases/:version/
```
Die Liste ist paginiert (`page`, `page_size`) und unterstützt `q`, `source` und `netbox_version`. Sie enthält ausschließlich aktive, vollständig validierte und freigegebene Source-/Plugin-Ketten.
Plugin-Detail (gekürzt auf ein Release):
```json
{
"api_version": "v1",
"slug": "demo-plugin",
"name": "Demo Plugin",
"summary": "Kurzbeschreibung",
"description": "Beschreibung",
"repository_url": "https://git.example.com/team/demo",
"latest_version": "1.2.3",
"package_name": "netbox-demo",
"import_name": "netbox_demo",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"approved": true,
"status": "approved",
"releases": [
{
"version": "1.2.3",
"download_url": "https://git.example.com/assets/netbox_demo-1.2.3-py3-none-any.whl",
"sha256": "<64-hex>",
"artifact_size": 12345,
"commit_sha": "<git-sha>",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"published_at": "2026-08-20T10:00:00+00:00",
"approved": true,
"status": "approved",
"immutable": true,
"approved_payload_sha256": "<64-hex>"
}
]
}
```
Die Release-Detailroute liefert exakt das flache Release-Objekt aus `releases[]`, ohne interne Admin-ID. Ohne freigegebenes Wheel ist `releases` leer und `latest_version` `null`. Ein Plugin-Detail enthält höchstens 1.000 Releases. `commit_sha` ist entweder leer oder exakt 40 kleingeschriebene Hex-Zeichen. Vor der Freigabe validiert der Store außerdem den Wheel-Dateinamen und verlangt, dass dessen normalisierte Distribution und Version exakt zu `package_name` und `version` passen; damit scheitert ein Artefakt nicht erst später im Host-Agent.
## 7. Netzwerk- und SSRF-Schutz
Ausgehende Ziele müssen credential-freies HTTPS verwenden und in `STORE_ALLOWED_SOURCE_HOSTS` stehen. Jeder DNS-A/AAAA-Wert wird geprüft; Loopback, private, Link-Local- und reservierte Adressen sind standardmäßig gesperrt. cURL wird an die geprüfte Adresse gepinnt, Redirects werden einzeln erneut aufgelöst/geprüft, und Provider-Tokens bleiben auf der exakten API-Origin.
```dotenv
STORE_ALLOWED_SOURCE_HOSTS=git.mrblake.cc,github.com,api.github.com,*.github.com,*.githubusercontent.com
STORE_ALLOW_PRIVATE_NETWORKS=false
```
Für ein bewusst internes Forgejo muss dessen Host explizit allowgelistet und `STORE_ALLOW_PRIVATE_NETWORKS=true` gesetzt werden. Diese Ausnahme erweitert den SSRF-Radius und sollte nur in einem kontrollierten Netz verwendet werden.
`STORE_TRUST_PROXY=true` darf nur genutzt werden, wenn direkter Zugriff auf Apache per Firewall ausgeschlossen ist. Zusätzlich werden Forwarded-IPs nur von exakt gelisteten Peers akzeptiert:
```dotenv
STORE_TRUST_PROXY=true
STORE_TRUSTED_PROXY_IPS=10.20.0.10,2001:db8::10
```
## 8. Optionale Docker-Installation
Das Image enthält Apache und PHP 8.4; ein Proxy ist nicht erforderlich. Der Scheduler läuft im Container als `www-data`, damit Webprozess und Watcher dieselben geschützten JSON-Dateien lesen können.
```bash
cd store
docker build -t netbox-plugin-store .
docker volume create netbox-plugin-store-data
docker run -d --name netbox-plugin-store \
-p 3000:80 \
--env-file .env \
-e STORE_PUBLIC_URL=http://localhost:3000 \
-e STORE_COOKIE_SECURE=false \
-e STORE_JSON_PATH=/var/www/html/data/store.json \
-e STORE_SCHEDULER_ENABLED=true \
-v netbox-plugin-store-data:/var/www/html/data \
netbox-plugin-store
```
Der Healthcheck ist `GET /healthz`. Für Produktion HTTPS konfigurieren und `STORE_PUBLIC_URL`/Cookie-Einstellung entsprechend setzen.
## 9. Update, Backup und Diagnose
Native Aktualisierung:
```bash
cd /opt/netbox-plugin-store
git pull --ff-only
cd store
composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative
sudo systemctl reload apache2
sudo -u www-data php bin/console sync
```
Vor Updates den JSON-Datastore oder MariaDB sichern. Der JSON-Store darf nur kopiert werden, während kein Schreibvorgang läuft; am einfachsten Apache/Timer kurz stoppen oder ein konsistentes dateisystemseitiges Backup-Verfahren verwenden.
Nützliche Prüfungen:
```bash
php -l public/index.php
composer validate --strict
composer test
curl -fsS http://127.0.0.1/healthz
curl -fsS http://127.0.0.1/api/v1/plugins/
```
Sync-Ergebnisse stehen im Admin-Dashboard und werden als JSON vom CLI ausgegeben. Einzelne Repository-Fehler führen zu einem `partial`-Lauf und archivieren den letzten bekannten Eintrag nicht; eine erfolgreich festgestellte Nicht-Kandidatur oder ein erfolgreich festgestellter Release-Rückzug wird dagegen fail-closed aus dem öffentlichen Katalog entfernt.
## Tests
Der zero-dependency Runner nutzt PHPs Laufzeit direkt; die Anwendungsabhängigkeiten müssen per Composer installiert sein:
```bash
composer install
composer test
```
Abgedeckt sind unter anderem API-Feldvertrag und 1.000er-Grenze, Approval-Payload, Wheel-Distribution/-Version/-Dateiname, 40-stellige Commit-SHAs, strikte Versions-/Metadatenvalidierung, README-Sanitizing und Commit-Pinning, Origin-Token-Isolation, private GitHub-Assets, private-IP-SSRF, atomare JSON-Grenzen und exklusive Leases, aggregierte Sync-Budgets, Trusted-Proxy-Auswertung, sticky Admin-Overrides, Rehash bei ersetzten Assets, Withdrawal und Nicht-Kandidaten-Archivierung.