91 lines
2.9 KiB
Go
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()
|
|
}
|