API-02: modul-registry-aktivierungspruefung
internal/moduleregistry: Registry.Register traegt Fachmodule mit Name, Version und benoetigten Feature-Flags ein (Akzeptanzkriterium 1), fehlende Pflichtangaben werden abgewiesen. IsActive kombiniert Registrierung + LIC-02 Feature-Flag-Auswertung (ALLE benoetigten Flags muessen fuer den Tenant aktiv sein) — ein nicht registriertes Modul ist nie aktiv. List liefert alle Module fuer Statusseite/Lizenzoberflaeche (Akzeptanzkriterium 3). RequireActiveModule ist die zentrale Durchsetzungs-Middleware (Casbin- Prinzip): weist Anfragen an ein deaktiviertes Modul ab, BEVOR der Modul-Handler ueberhaupt aufgerufen wird (Akzeptanzkriterium 2) — Pruefung per Test belegt, dass der Handler bei Deaktivierung nachweislich nicht erreicht wird. Service-Credentials (Akzeptanzkriterium 4): Registry.Provision stellt pro Modul-Instanz Client-ID + Secret aus, gespeichert wird nur der SHA-256-Hash des Secrets. Registry.Authenticate vergleicht timing-safe (dieselbe subtle.ConstantTimeCompare-Referenzimplementierung wie AUD-02). RequireServiceCredential-Middleware liest X-Client-Id/X-Client-Secret und weist Aufrufe ohne gueltiges Credential mit 401 ab, bevor der Core-seitige Endpunkt (z.B. Audit-Nachlieferung, Nutzungsmeldung) erreicht wird. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Anfrage an deaktiviertes Modul nachweislich vor Modul-Logik abgewiesen — TestRequireActiveModule_BlocksBeforeHandler: handlerReached bleibt false bei 403, wird true erst nach Aktivierung bei 200. PASS. 2. Registrierung mit fehlenden Pflichtangaben abgewiesen — TestRegister_RejectsMissingFields (leerer Name, leere Version). PASS. 3. Registry-Abfrage liefert konsistente Daten nach Aktivierung/Deaktivierung — TestIsActive_ReflectsFlagStateConsistently: aus/an/aus-Zyklus, IsActive folgt dem Flag-Zustand korrekt. PASS. 4. Aufruf mit ungueltigem/fehlendem Service-Credential abgewiesen, mit gueltigem angenommen — TestRequireServiceCredential_RejectsInvalidAcceptsValid und TestProvisionAndAuthenticate (falsches Secret, unbekannte Client-ID, korrektes Credential). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4a30345e07
commit
b23cd1961f
@@ -0,0 +1,120 @@
|
||||
// Package moduleregistry implementiert Core API-02: die Registry, in der
|
||||
// sich Fachmodule (DMS, Mail, weitere) mit Metadaten eintragen, gekoppelt an
|
||||
// die Aktivierungspruefung aus LIC-02 (Feature-Flags). Zusaetzlich
|
||||
// authentifiziert die Registry Modul-Instanzen selbst ueber ein bei
|
||||
// Provisionierung ausgestelltes Service-Credential.
|
||||
package moduleregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingName = errors.New("moduleregistry: name darf nicht leer sein")
|
||||
ErrMissingVersion = errors.New("moduleregistry: version darf nicht leer sein")
|
||||
ErrModuleNotFound = errors.New("moduleregistry: modul nicht registriert")
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
Name string
|
||||
Version string
|
||||
RequiredFlags []string
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
pool *pgxpool.Pool
|
||||
flags *flag.Service
|
||||
}
|
||||
|
||||
func NewRegistry(pool *pgxpool.Pool, flags *flag.Service) *Registry {
|
||||
return &Registry{pool: pool, flags: flags}
|
||||
}
|
||||
|
||||
// Register traegt ein Modul mit Name, Version und benoetigten Feature-Flags
|
||||
// ein (Akzeptanzkriterium 1). Fehlende Pflichtangaben werden abgewiesen
|
||||
// (Akzeptanzkriterium 1 / Pruefung 2). Erneutes Register desselben Namens
|
||||
// aktualisiert Version/Flags (Redeploy-Fall).
|
||||
func (r *Registry) Register(ctx context.Context, name, version string, requiredFlags []string) (Module, error) {
|
||||
if name == "" {
|
||||
return Module{}, ErrMissingName
|
||||
}
|
||||
if version == "" {
|
||||
return Module{}, ErrMissingVersion
|
||||
}
|
||||
if requiredFlags == nil {
|
||||
requiredFlags = []string{}
|
||||
}
|
||||
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO modules (name, version, required_flags, registered_at)
|
||||
VALUES ($1, $2, $3, now())
|
||||
ON CONFLICT (name) DO UPDATE SET version = $2, required_flags = $3, registered_at = now()
|
||||
`, name, version, requiredFlags)
|
||||
if err != nil {
|
||||
return Module{}, fmt.Errorf("modul registrieren: %w", err)
|
||||
}
|
||||
return Module{Name: name, Version: version, RequiredFlags: requiredFlags}, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Get(ctx context.Context, name string) (Module, error) {
|
||||
var m Module
|
||||
m.Name = name
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT version, required_flags FROM modules WHERE name = $1
|
||||
`, name).Scan(&m.Version, &m.RequiredFlags)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Module{}, ErrModuleNotFound
|
||||
}
|
||||
return Module{}, fmt.Errorf("modul lesen: %w", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// List liefert alle registrierten Module (Akzeptanzkriterium 3: ueber API
|
||||
// abfragbar, z.B. fuer Statusseite/Lizenzoberflaeche).
|
||||
func (r *Registry) List(ctx context.Context) ([]Module, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT name, version, required_flags FROM modules ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("module auflisten: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []Module
|
||||
for rows.Next() {
|
||||
var m Module
|
||||
if err := rows.Scan(&m.Name, &m.Version, &m.RequiredFlags); err != nil {
|
||||
return nil, fmt.Errorf("modul lesen: %w", err)
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// IsActive prueft, ob ein registriertes Modul fuer einen Tenant aktiviert
|
||||
// ist: registriert UND alle benoetigten Feature-Flags sind fuer diesen
|
||||
// Tenant aktiv (Akzeptanzkriterium 2). Ein nicht registriertes Modul gilt
|
||||
// immer als nicht aktiv.
|
||||
func (r *Registry) IsActive(ctx context.Context, tenantSlug, moduleName string) (bool, error) {
|
||||
m, err := r.Get(ctx, moduleName)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrModuleNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, flagKey := range m.RequiredFlags {
|
||||
if !r.flags.IsEnabled(ctx, tenantSlug, flagKey) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user