Files
br ca6cdcca63 software steht so weit.
jetzt build + docker build und testen
2026-10-02 13:25:34 +02:00

91 lines
2.9 KiB
Go

package store
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"time"
)
// hashToken: Tokens werden nur als SHA-256 gespeichert. Ein einfacher Hash
// reicht, weil Tokens zufällig mit hoher Entropie erzeugt werden.
func hashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
// AddEnrollmentToken hinterlegt einen einmaligen Einladungs-Token für
// tenantID. Erzeugt wird der Token vom Aufrufer; gespeichert wird nur sein
// Hash.
func (s *Store) AddEnrollmentToken(ctx context.Context, token, tenantID string, expiresAt time.Time) error {
if token == "" {
return invalid("Token fehlt")
}
if !expiresAt.After(s.now()) {
return invalid("Ablaufzeitpunkt %v liegt nicht in der Zukunft", expiresAt)
}
_, err := s.db.ExecContext(ctx,
`INSERT INTO enrollment_tokens (token_hash, tenant_id, expires_at, used) VALUES (?, ?, ?, 0)`,
hashToken(token), tenantID, formatTime(expiresAt))
return mapConstraint(err, "Enrollment-Token")
}
// Enroll löst token ein und legt in derselben Transaktion den Knoten n an.
// n.TenantID wird durch den Mandanten des Tokens ersetzt. Schlägt das Anlegen
// fehl, bleibt der Token unverbraucht. Fehler: ErrNotFound (unbekannter
// Token), ErrTokenUsed, ErrTokenExpired sowie die Fehler von CreateNode.
func (s *Store) Enroll(ctx context.Context, token string, n Node) (Node, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return Node{}, err
}
defer tx.Rollback()
hash := hashToken(token)
var tenantID string
var expiresAt time.Time
var used bool
err = tx.QueryRowContext(ctx,
`SELECT tenant_id, expires_at, used FROM enrollment_tokens WHERE token_hash = ?`, hash).
Scan(&tenantID, dbTime{&expiresAt}, &used)
switch {
case errors.Is(err, sql.ErrNoRows):
return Node{}, fmt.Errorf("Enrollment-Token: %w", ErrNotFound)
case err != nil:
return Node{}, err
case used:
return Node{}, ErrTokenUsed
case !s.now().Before(expiresAt):
return Node{}, ErrTokenExpired
}
if _, err := tx.ExecContext(ctx, `UPDATE enrollment_tokens SET used = 1, used_at = ? WHERE token_hash = ?`,
formatTime(s.now()), hash); err != nil {
return Node{}, err
}
n.TenantID = tenantID
node, err := s.insertNode(ctx, tx, n)
if err != nil {
return Node{}, err
}
if err := tx.Commit(); err != nil {
return Node{}, err
}
return node, nil
}
// PurgeEnrollmentTokens löscht Einladungen, die vor mehr als olderThan
// eingelöst wurden oder abgelaufen sind (0 = sofort), und liefert ihre Anzahl.
func (s *Store) PurgeEnrollmentTokens(ctx context.Context, olderThan time.Duration) (int64, error) {
cutoff := formatTime(s.now().Add(-olderThan))
res, err := s.db.ExecContext(ctx, `DELETE FROM enrollment_tokens
WHERE (used = 1 AND (used_at IS NULL OR used_at <= ?)) OR expires_at <= ?`, cutoff, cutoff)
if err != nil {
return 0, err
}
return res.RowsAffected()
}