package api import ( "context" "database/sql" "fmt" "git.ryuvia.com/niklas/terdut-server/internal/models" ) const ( // operatorAccountName is the instance-scoped service account the operator // key belongs to. operatorAccountName = "terdut-operator" // operatorKeyName names the one key SeedOperatorKey manages on it, so a // rotation replaces that key and leaves any others alone. operatorKeyName = "seed" ) // SeedOperatorKey makes key the operator account's credential: it creates the // instance-scoped service account if needed and replaces its "seed" key with // this one. Idempotent, so every replica can run it at every start, and a // rotated key simply wins on the next restart. // // The key is hashed like any other, so only the caller that generated it ever // holds the raw value. An empty key does nothing. func SeedOperatorKey(ctx context.Context, db *sql.DB, key string) error { if key == "" { return nil } tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("seed operator key: %w", err) } defer tx.Rollback() //nolint:errcheck // Serialise replicas starting together; transaction-scoped, so it needs no // explicit release. if _, err := tx.ExecContext(ctx, "SELECT pg_advisory_xact_lock($1)", operatorKeyLockKey); err != nil { return fmt.Errorf("seed operator key: lock: %w", err) } var accountID int64 err = tx.QueryRowContext(ctx, "SELECT id FROM service_accounts WHERE name = $1 AND scope = $2", operatorAccountName, models.ServiceAccountScopeInstance).Scan(&accountID) if err == sql.ErrNoRows { err = tx.QueryRowContext(ctx, "INSERT INTO service_accounts (name, scope) VALUES ($1, $2) RETURNING id", operatorAccountName, models.ServiceAccountScopeInstance).Scan(&accountID) } if err != nil { return fmt.Errorf("seed operator key: account: %w", err) } if _, err := tx.ExecContext(ctx, "DELETE FROM service_account_keys WHERE service_account_id = $1 AND name = $2", accountID, operatorKeyName); err != nil { return fmt.Errorf("seed operator key: drop old key: %w", err) } if _, err := tx.ExecContext(ctx, "INSERT INTO service_account_keys (service_account_id, key_hash, name) VALUES ($1, $2, $3)", accountID, hashToken(key), operatorKeyName); err != nil { return fmt.Errorf("seed operator key: store key: %w", err) } return tx.Commit() } // operatorKeyLockKey is the transaction-scoped advisory lock SeedOperatorKey // holds; distinct from the other lock keys in this package. const operatorKeyLockKey int64 = 7265_0010