internal/license: Ed25519-signierte Lizenzschluessel (stdlib crypto/ed25519, keine neue Abhaengigkeit). Issuer haelt den privaten Schluessel (lebt beim Lizenzgeber), Validator nur den oeffentlichen (lebt im Core-Prozess) — klare Trennung Ausstellung/Pruefung nach Unleash-Vorbild (Flag-Verwaltung vs. Flag-Auswertung). Store.Install prueft NUR die Signatur und persistiert den Lizenzumfang (Plan, Modul-Liste, Laufzeit) in tenant_licenses (Registry-DB, 1:1 zu tenants). Eine bereits abgelaufene, aber korrekt signierte Lizenz laesst sich trotzdem einspielen — der Ablauf wird erst bei Store.RequireActive bewertet (liefert ErrLicenseExpired statt Panic/Absturz), waehrend Store.Status den Umfang unabhaengig vom Ablauf weiterhin liefert. Neu: scripts/run-checks.sh buendelt reset-test-env.sh + go build/vet/test (-p 1) zu einem Ein-Kommando-Check fuer den Testhost. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Manipulierter Lizenzschluessel zuverlaessig erkannt — TestParse_RejectsTamperedKey, TestParse_RejectsWrongKeyPair, TestStore_InstallRejectsInvalidSignature. PASS. 2. Ablauf loest definierten eingeschraenkten Zustand aus, kein harter Systemausfall — TestStore_RequireActive_DetectsExpiry (inkl. recover()- Absicherung im Test, dass kein Panic auftritt), ErrLicenseExpired statt Absturz; Status bleibt trotzdem abfragbar. PASS. 3. Signaturpruefung von zweiter Person gegen Dokumentation nachvollzogen — NICHT durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
2.9 KiB
Go
89 lines
2.9 KiB
Go
package license
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var (
|
|
ErrNoLicense = errors.New("license: kein lizenzdatensatz fuer diesen tenant")
|
|
ErrLicenseExpired = errors.New("license: lizenz abgelaufen")
|
|
)
|
|
|
|
// Store persistiert den Lizenzumfang je Tenant in der Control-Plane-Registry
|
|
// (siehe internal/tenant.Registry — dieselbe Datenbank, aber ein eigener,
|
|
// unabhaengiger Store, um internal/tenant nicht um lizenzfremde Belange zu
|
|
// erweitern).
|
|
type Store struct {
|
|
pool *pgxpool.Pool
|
|
validator *Validator
|
|
}
|
|
|
|
func NewStore(pool *pgxpool.Pool, validator *Validator) *Store {
|
|
return &Store{pool: pool, validator: validator}
|
|
}
|
|
|
|
// Install prueft die Signatur des Lizenzschluessels (Akzeptanzkriterium 1)
|
|
// und ersetzt den bisherigen Lizenzdatensatz des Tenants vollstaendig. Ein
|
|
// bereits abgelaufener, aber korrekt signierter Schluessel wird trotzdem
|
|
// gespeichert — der Ablauf wird erst bei der Nutzung (RequireActive)
|
|
// bewertet, nicht beim Einspielen.
|
|
func (s *Store) Install(ctx context.Context, tenantID, licenseKey string) (Payload, error) {
|
|
payload, err := s.validator.Parse(licenseKey)
|
|
if err != nil {
|
|
return Payload{}, err
|
|
}
|
|
|
|
_, err = s.pool.Exec(ctx, `
|
|
INSERT INTO tenant_licenses (tenant_id, plan, modules, issued_at, valid_until, raw_key, installed_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, now())
|
|
ON CONFLICT (tenant_id) DO UPDATE SET
|
|
plan = $2, modules = $3, issued_at = $4, valid_until = $5, raw_key = $6, installed_at = now()
|
|
`, tenantID, payload.Plan, payload.Modules, payload.IssuedAt, payload.ValidUntil, licenseKey)
|
|
if err != nil {
|
|
return Payload{}, fmt.Errorf("lizenz speichern: %w", err)
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func (s *Store) get(ctx context.Context, tenantID string) (Payload, error) {
|
|
var p Payload
|
|
row := s.pool.QueryRow(ctx, `
|
|
SELECT plan, modules, issued_at, valid_until
|
|
FROM tenant_licenses WHERE tenant_id = $1
|
|
`, tenantID)
|
|
if err := row.Scan(&p.Plan, &p.Modules, &p.IssuedAt, &p.ValidUntil); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return Payload{}, ErrNoLicense
|
|
}
|
|
return Payload{}, fmt.Errorf("lizenz lesen: %w", err)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// Status liefert den persistierten Lizenzumfang unabhaengig vom Ablauf
|
|
// (Akzeptanzkriterium 3: Plan, Modul-Liste, Laufzeit abfragbar).
|
|
func (s *Store) Status(ctx context.Context, tenantID string) (Payload, error) {
|
|
return s.get(ctx, tenantID)
|
|
}
|
|
|
|
// RequireActive liefert den Lizenzumfang NUR, wenn die Lizenz noch nicht
|
|
// abgelaufen ist — sonst ErrLicenseExpired statt eines harten Fehlers/Panics
|
|
// (Akzeptanzkriterium 2: definierter eingeschraenkter Zustand). Aufrufende
|
|
// Module (LIC-02/03) entscheiden, was "eingeschraenkt" konkret bedeutet.
|
|
func (s *Store) RequireActive(ctx context.Context, tenantID string) (Payload, error) {
|
|
p, err := s.get(ctx, tenantID)
|
|
if err != nil {
|
|
return Payload{}, err
|
|
}
|
|
if time.Now().After(p.ValidUntil) {
|
|
return Payload{}, ErrLicenseExpired
|
|
}
|
|
return p, nil
|
|
}
|