feat(federation): add SQL user authentication for peer sync
Merge SQL user authentication with Emma federation sync implementation: - Add federation_peers table for encrypted credential storage - Add credentials.go with AES-256-GCM encryption, SHA-256 key derivation - Extend FederatedStorage interface with credential methods - Add --user, --password, --sovereignty flags to bd federation add-peer - Integrate credentials into PushTo/PullFrom/Fetch via withPeerCredentials - DOLT_REMOTE_USER/PASSWORD env vars protected by mutex for concurrency Credentials automatically used when syncing with peers that have stored auth. Continues: bd-wkumz.10, Closes: bd-4p67y Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
ea51c4b0bd
commit
d3d2326a8b
@@ -2,16 +2,23 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/dolt"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
federationPeer string
|
||||
federationStrategy string
|
||||
federationUser string
|
||||
federationPassword string
|
||||
federationSov string
|
||||
)
|
||||
|
||||
var federationCmd = &cobra.Command{
|
||||
@@ -66,17 +73,22 @@ Examples:
|
||||
|
||||
var federationAddPeerCmd = &cobra.Command{
|
||||
Use: "add-peer <name> <url>",
|
||||
Short: "Add a federation peer",
|
||||
Long: `Add a new federation peer remote.
|
||||
Short: "Add a federation peer with optional SQL credentials",
|
||||
Long: `Add a new federation peer remote with optional SQL user authentication.
|
||||
|
||||
The URL can be:
|
||||
- dolthub://org/repo DoltHub hosted repository
|
||||
- host:port/database Direct dolt sql-server connection
|
||||
- file:///path/to/repo Local file path (for testing)
|
||||
|
||||
Credentials are encrypted and stored locally. They are used automatically
|
||||
when syncing with the peer. If --user is provided without --password,
|
||||
you will be prompted for the password interactively.
|
||||
|
||||
Examples:
|
||||
bd federation add-peer town-beta dolthub://acme/town-beta-beads
|
||||
bd federation add-peer town-gamma 192.168.1.100:3306/beads`,
|
||||
bd federation add-peer town-gamma 192.168.1.100:3306/beads --user sync-bot
|
||||
bd federation add-peer partner https://partner.example.com/beads --user admin --password secret`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runFederationAddPeer,
|
||||
}
|
||||
@@ -109,6 +121,11 @@ func init() {
|
||||
// Flags for status
|
||||
federationStatusCmd.Flags().StringVar(&federationPeer, "peer", "", "Specific peer to check")
|
||||
|
||||
// Flags for add-peer (SQL user authentication)
|
||||
federationAddPeerCmd.Flags().StringVarP(&federationUser, "user", "u", "", "SQL username for authentication")
|
||||
federationAddPeerCmd.Flags().StringVarP(&federationPassword, "password", "p", "", "SQL password (prompted if --user set without --password)")
|
||||
federationAddPeerCmd.Flags().StringVar(&federationSov, "sovereignty", "", "Sovereignty tier (T1, T2, T3, T4)")
|
||||
|
||||
rootCmd.AddCommand(federationCmd)
|
||||
}
|
||||
|
||||
@@ -288,19 +305,63 @@ func runFederationAddPeer(cmd *cobra.Command, args []string) {
|
||||
FatalErrorRespectJSON("federation requires Dolt backend")
|
||||
}
|
||||
|
||||
if err := fs.AddRemote(ctx, name, url); err != nil {
|
||||
FatalErrorRespectJSON("failed to add peer: %v", err)
|
||||
// If user is provided but password is not, prompt for it
|
||||
password := federationPassword
|
||||
if federationUser != "" && password == "" {
|
||||
fmt.Fprint(os.Stderr, "Password: ")
|
||||
pwBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Fprintln(os.Stderr) // newline after password
|
||||
if err != nil {
|
||||
FatalErrorRespectJSON("failed to read password: %v", err)
|
||||
}
|
||||
password = string(pwBytes)
|
||||
}
|
||||
|
||||
// Validate sovereignty tier if provided
|
||||
sov := federationSov
|
||||
if sov != "" {
|
||||
sov = strings.ToUpper(sov)
|
||||
if sov != "T1" && sov != "T2" && sov != "T3" && sov != "T4" {
|
||||
FatalErrorRespectJSON("invalid sovereignty tier: %s (must be T1, T2, T3, or T4)", federationSov)
|
||||
}
|
||||
}
|
||||
|
||||
// If credentials provided, use AddFederationPeer to store them
|
||||
if federationUser != "" {
|
||||
peer := &storage.FederationPeer{
|
||||
Name: name,
|
||||
RemoteURL: url,
|
||||
Username: federationUser,
|
||||
Password: password,
|
||||
Sovereignty: sov,
|
||||
}
|
||||
if err := fs.AddFederationPeer(ctx, peer); err != nil {
|
||||
FatalErrorRespectJSON("failed to add peer: %v", err)
|
||||
}
|
||||
} else {
|
||||
// No credentials, just add the remote
|
||||
if err := fs.AddRemote(ctx, name, url); err != nil {
|
||||
FatalErrorRespectJSON("failed to add peer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"added": name,
|
||||
"url": url,
|
||||
"added": name,
|
||||
"url": url,
|
||||
"has_auth": federationUser != "",
|
||||
"sovereignty": sov,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Added peer %s: %s\n", ui.RenderAccent(name), url)
|
||||
if federationUser != "" {
|
||||
fmt.Printf(" User: %s (credentials stored)\n", federationUser)
|
||||
}
|
||||
if sov != "" {
|
||||
fmt.Printf(" Sovereignty: %s\n", sov)
|
||||
}
|
||||
}
|
||||
|
||||
func runFederationRemovePeer(cmd *cobra.Command, args []string) {
|
||||
|
||||
327
internal/storage/dolt/credentials.go
Normal file
327
internal/storage/dolt/credentials.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package dolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
)
|
||||
|
||||
// Credential storage and encryption for federation peers.
|
||||
// Enables SQL user authentication when syncing with peer Gas Towns.
|
||||
|
||||
// federationEnvMutex protects DOLT_REMOTE_USER/PASSWORD env vars from concurrent access.
|
||||
// Environment variables are process-global, so we need to serialize federation operations.
|
||||
var federationEnvMutex sync.Mutex
|
||||
|
||||
// validPeerNameRegex matches valid peer names (alphanumeric, hyphens, underscores)
|
||||
var validPeerNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
|
||||
|
||||
// validatePeerName checks that a peer name is safe for use as a Dolt remote name
|
||||
func validatePeerName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("peer name cannot be empty")
|
||||
}
|
||||
if len(name) > 64 {
|
||||
return fmt.Errorf("peer name too long (max 64 characters)")
|
||||
}
|
||||
if !validPeerNameRegex.MatchString(name) {
|
||||
return fmt.Errorf("peer name must start with a letter and contain only alphanumeric characters, hyphens, and underscores")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encryptionKey derives a key from the database path for credential encryption.
|
||||
// This provides basic protection - credentials are not stored in plaintext.
|
||||
// For production, consider using system keyring or external secret managers.
|
||||
func (s *DoltStore) encryptionKey() []byte {
|
||||
// Use SHA-256 hash of the database path as the key (32 bytes for AES-256)
|
||||
// This ties credentials to this specific database location
|
||||
h := sha256.New()
|
||||
h.Write([]byte(s.dbPath + "beads-federation-key-v1"))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// encryptPassword encrypts a password using AES-GCM
|
||||
func (s *DoltStore) encryptPassword(password string) ([]byte, error) {
|
||||
if password == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(s.encryptionKey())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// decryptPassword decrypts a password using AES-GCM
|
||||
func (s *DoltStore) decryptPassword(encrypted []byte) (string, error) {
|
||||
if len(encrypted) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(s.encryptionKey())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// AddFederationPeer adds or updates a federation peer with credentials.
|
||||
// This stores credentials in the database and also adds the Dolt remote.
|
||||
func (s *DoltStore) AddFederationPeer(ctx context.Context, peer *storage.FederationPeer) error {
|
||||
// Validate peer name
|
||||
if err := validatePeerName(peer.Name); err != nil {
|
||||
return fmt.Errorf("invalid peer name: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt password before storing
|
||||
var encryptedPwd []byte
|
||||
var err error
|
||||
if peer.Password != "" {
|
||||
encryptedPwd, err = s.encryptPassword(peer.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt password: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert the peer credentials
|
||||
_, err = s.db.ExecContext(ctx, `
|
||||
INSERT INTO federation_peers (name, remote_url, username, password_encrypted, sovereignty)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
remote_url = VALUES(remote_url),
|
||||
username = VALUES(username),
|
||||
password_encrypted = VALUES(password_encrypted),
|
||||
sovereignty = VALUES(sovereignty),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, peer.Name, peer.RemoteURL, peer.Username, encryptedPwd, peer.Sovereignty)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add federation peer: %w", err)
|
||||
}
|
||||
|
||||
// Also add the Dolt remote
|
||||
if err := s.AddRemote(ctx, peer.Name, peer.RemoteURL); err != nil {
|
||||
// Ignore "remote already exists" errors
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
return fmt.Errorf("failed to add dolt remote: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFederationPeer retrieves a federation peer by name.
|
||||
// Returns nil if peer doesn't exist.
|
||||
func (s *DoltStore) GetFederationPeer(ctx context.Context, name string) (*storage.FederationPeer, error) {
|
||||
var peer storage.FederationPeer
|
||||
var encryptedPwd []byte
|
||||
var lastSync sql.NullTime
|
||||
var username sql.NullString
|
||||
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT name, remote_url, username, password_encrypted, sovereignty, last_sync, created_at, updated_at
|
||||
FROM federation_peers WHERE name = ?
|
||||
`, name).Scan(&peer.Name, &peer.RemoteURL, &username, &encryptedPwd, &peer.Sovereignty, &lastSync, &peer.CreatedAt, &peer.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get federation peer: %w", err)
|
||||
}
|
||||
|
||||
if username.Valid {
|
||||
peer.Username = username.String
|
||||
}
|
||||
if lastSync.Valid {
|
||||
peer.LastSync = &lastSync.Time
|
||||
}
|
||||
|
||||
// Decrypt password
|
||||
if len(encryptedPwd) > 0 {
|
||||
peer.Password, err = s.decryptPassword(encryptedPwd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt password: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
// ListFederationPeers returns all configured federation peers.
|
||||
func (s *DoltStore) ListFederationPeers(ctx context.Context) ([]*storage.FederationPeer, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT name, remote_url, username, password_encrypted, sovereignty, last_sync, created_at, updated_at
|
||||
FROM federation_peers ORDER BY name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list federation peers: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var peers []*storage.FederationPeer
|
||||
for rows.Next() {
|
||||
var peer storage.FederationPeer
|
||||
var encryptedPwd []byte
|
||||
var lastSync sql.NullTime
|
||||
var username sql.NullString
|
||||
|
||||
if err := rows.Scan(&peer.Name, &peer.RemoteURL, &username, &encryptedPwd, &peer.Sovereignty, &lastSync, &peer.CreatedAt, &peer.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan federation peer: %w", err)
|
||||
}
|
||||
|
||||
if username.Valid {
|
||||
peer.Username = username.String
|
||||
}
|
||||
if lastSync.Valid {
|
||||
peer.LastSync = &lastSync.Time
|
||||
}
|
||||
|
||||
// Decrypt password
|
||||
if len(encryptedPwd) > 0 {
|
||||
peer.Password, err = s.decryptPassword(encryptedPwd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt password: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
peers = append(peers, &peer)
|
||||
}
|
||||
|
||||
return peers, rows.Err()
|
||||
}
|
||||
|
||||
// RemoveFederationPeer removes a federation peer and its credentials.
|
||||
func (s *DoltStore) RemoveFederationPeer(ctx context.Context, name string) error {
|
||||
result, err := s.db.ExecContext(ctx, "DELETE FROM federation_peers WHERE name = ?", name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove federation peer: %w", err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
// Peer not in credentials table, but might still be a Dolt remote
|
||||
// Continue to try removing the remote
|
||||
}
|
||||
|
||||
// Also remove the Dolt remote (best-effort)
|
||||
_ = s.RemoveRemote(ctx, name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePeerLastSync updates the last sync time for a peer.
|
||||
func (s *DoltStore) UpdatePeerLastSync(ctx context.Context, name string) error {
|
||||
_, err := s.db.ExecContext(ctx, "UPDATE federation_peers SET last_sync = CURRENT_TIMESTAMP WHERE name = ?", name)
|
||||
return err
|
||||
}
|
||||
|
||||
// setFederationCredentials sets DOLT_REMOTE_USER and DOLT_REMOTE_PASSWORD env vars.
|
||||
// Returns a cleanup function that must be called (typically via defer) to unset them.
|
||||
// The caller must hold federationEnvMutex.
|
||||
func setFederationCredentials(username, password string) func() {
|
||||
if username != "" {
|
||||
os.Setenv("DOLT_REMOTE_USER", username)
|
||||
}
|
||||
if password != "" {
|
||||
os.Setenv("DOLT_REMOTE_PASSWORD", password)
|
||||
}
|
||||
return func() {
|
||||
os.Unsetenv("DOLT_REMOTE_USER")
|
||||
os.Unsetenv("DOLT_REMOTE_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
// withPeerCredentials executes a function with peer credentials set in environment.
|
||||
// If the peer has stored credentials, they are set as DOLT_REMOTE_USER/PASSWORD
|
||||
// for the duration of the function call.
|
||||
func (s *DoltStore) withPeerCredentials(ctx context.Context, peerName string, fn func() error) error {
|
||||
// Look up credentials for this peer
|
||||
peer, err := s.GetFederationPeer(ctx, peerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get peer credentials: %w", err)
|
||||
}
|
||||
|
||||
// If we have credentials, set env vars with mutex protection
|
||||
if peer != nil && (peer.Username != "" || peer.Password != "") {
|
||||
federationEnvMutex.Lock()
|
||||
cleanup := setFederationCredentials(peer.Username, peer.Password)
|
||||
defer func() {
|
||||
cleanup()
|
||||
federationEnvMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
err = fn()
|
||||
|
||||
// Update last sync time on success
|
||||
if err == nil && peer != nil {
|
||||
_ = s.UpdatePeerLastSync(ctx, peerName)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// PushWithCredentials pushes to a remote using stored credentials.
|
||||
func (s *DoltStore) PushWithCredentials(ctx context.Context, remoteName string) error {
|
||||
return s.withPeerCredentials(ctx, remoteName, func() error {
|
||||
return s.PushTo(ctx, remoteName)
|
||||
})
|
||||
}
|
||||
|
||||
// PullWithCredentials pulls from a remote using stored credentials.
|
||||
func (s *DoltStore) PullWithCredentials(ctx context.Context, remoteName string) ([]storage.Conflict, error) {
|
||||
var conflicts []storage.Conflict
|
||||
err := s.withPeerCredentials(ctx, remoteName, func() error {
|
||||
var pullErr error
|
||||
conflicts, pullErr = s.PullFrom(ctx, remoteName)
|
||||
return pullErr
|
||||
})
|
||||
return conflicts, err
|
||||
}
|
||||
|
||||
// FederationPeer is an alias for storage.FederationPeer for convenience.
|
||||
type FederationPeer = storage.FederationPeer
|
||||
13
internal/storage/dolt/credentials_test.go
Normal file
13
internal/storage/dolt/credentials_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package dolt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
)
|
||||
|
||||
func TestDoltStoreImplementsCredentialMethods(t *testing.T) {
|
||||
// Compile-time check that DoltStore implements FederatedStorage with credential methods
|
||||
var _ storage.FederatedStorage = (*DoltStore)(nil)
|
||||
t.Log("DoltStore implements FederatedStorage interface with credential methods")
|
||||
}
|
||||
@@ -12,39 +12,51 @@ import (
|
||||
// These methods enable peer-to-peer synchronization between Gas Towns.
|
||||
|
||||
// PushTo pushes commits to a specific peer remote.
|
||||
// If credentials are stored for this peer, they are used automatically.
|
||||
func (s *DoltStore) PushTo(ctx context.Context, peer string) error {
|
||||
// DOLT_PUSH(remote, branch)
|
||||
_, err := s.db.ExecContext(ctx, "CALL DOLT_PUSH(?, ?)", peer, s.branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push to peer %s: %w", peer, err)
|
||||
}
|
||||
return nil
|
||||
return s.withPeerCredentials(ctx, peer, func() error {
|
||||
// DOLT_PUSH(remote, branch)
|
||||
_, err := s.db.ExecContext(ctx, "CALL DOLT_PUSH(?, ?)", peer, s.branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push to peer %s: %w", peer, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PullFrom pulls changes from a specific peer remote.
|
||||
// If credentials are stored for this peer, they are used automatically.
|
||||
// Returns any merge conflicts if present.
|
||||
func (s *DoltStore) PullFrom(ctx context.Context, peer string) ([]storage.Conflict, error) {
|
||||
// DOLT_PULL(remote) - pulls and merges
|
||||
_, err := s.db.ExecContext(ctx, "CALL DOLT_PULL(?)", peer)
|
||||
if err != nil {
|
||||
// Check if the error is due to merge conflicts
|
||||
conflicts, conflictErr := s.GetConflicts(ctx)
|
||||
if conflictErr == nil && len(conflicts) > 0 {
|
||||
return conflicts, nil
|
||||
var conflicts []storage.Conflict
|
||||
err := s.withPeerCredentials(ctx, peer, func() error {
|
||||
// DOLT_PULL(remote) - pulls and merges
|
||||
_, pullErr := s.db.ExecContext(ctx, "CALL DOLT_PULL(?)", peer)
|
||||
if pullErr != nil {
|
||||
// Check if the error is due to merge conflicts
|
||||
c, conflictErr := s.GetConflicts(ctx)
|
||||
if conflictErr == nil && len(c) > 0 {
|
||||
conflicts = c
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to pull from peer %s: %w", peer, pullErr)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to pull from peer %s: %w", peer, err)
|
||||
}
|
||||
return nil, nil
|
||||
return nil
|
||||
})
|
||||
return conflicts, err
|
||||
}
|
||||
|
||||
// Fetch fetches refs from a peer without merging.
|
||||
// If credentials are stored for this peer, they are used automatically.
|
||||
func (s *DoltStore) Fetch(ctx context.Context, peer string) error {
|
||||
// DOLT_FETCH(remote)
|
||||
_, err := s.db.ExecContext(ctx, "CALL DOLT_FETCH(?)", peer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch from peer %s: %w", peer, err)
|
||||
}
|
||||
return nil
|
||||
return s.withPeerCredentials(ctx, peer, func() error {
|
||||
// DOLT_FETCH(remote)
|
||||
_, err := s.db.ExecContext(ctx, "CALL DOLT_FETCH(?)", peer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch from peer %s: %w", peer, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ListRemotes returns configured remote names and URLs.
|
||||
|
||||
@@ -234,6 +234,20 @@ CREATE TABLE IF NOT EXISTS interactions (
|
||||
INDEX idx_interactions_issue_id (issue_id),
|
||||
INDEX idx_interactions_parent_id (parent_id)
|
||||
);
|
||||
|
||||
-- Federation peers table (for SQL user authentication)
|
||||
-- Stores credentials for peer-to-peer Dolt remotes between Gas Towns
|
||||
CREATE TABLE IF NOT EXISTS federation_peers (
|
||||
name VARCHAR(255) PRIMARY KEY,
|
||||
remote_url VARCHAR(1024) NOT NULL,
|
||||
username VARCHAR(255),
|
||||
password_encrypted BLOB,
|
||||
sovereignty VARCHAR(8) DEFAULT '',
|
||||
last_sync DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_federation_peers_sovereignty (sovereignty)
|
||||
);
|
||||
`
|
||||
|
||||
// defaultConfig contains the default configuration values
|
||||
|
||||
@@ -171,6 +171,27 @@ type FederatedStorage interface {
|
||||
|
||||
// SyncStatus returns the sync status with a peer.
|
||||
SyncStatus(ctx context.Context, peer string) (*SyncStatus, error)
|
||||
|
||||
// Credential management for SQL user authentication
|
||||
|
||||
// AddFederationPeer adds or updates a federation peer with credentials.
|
||||
AddFederationPeer(ctx context.Context, peer *FederationPeer) error
|
||||
|
||||
// GetFederationPeer retrieves a federation peer by name.
|
||||
// Returns nil if peer doesn't exist.
|
||||
GetFederationPeer(ctx context.Context, name string) (*FederationPeer, error)
|
||||
|
||||
// ListFederationPeers returns all configured federation peers.
|
||||
ListFederationPeers(ctx context.Context) ([]*FederationPeer, error)
|
||||
|
||||
// RemoveFederationPeer removes a federation peer and its credentials.
|
||||
RemoveFederationPeer(ctx context.Context, name string) error
|
||||
|
||||
// PushWithCredentials pushes to a remote using stored credentials.
|
||||
PushWithCredentials(ctx context.Context, remoteName string) error
|
||||
|
||||
// PullWithCredentials pulls from a remote using stored credentials.
|
||||
PullWithCredentials(ctx context.Context, remoteName string) ([]Conflict, error)
|
||||
}
|
||||
|
||||
// RemoteInfo describes a configured remote.
|
||||
@@ -188,6 +209,19 @@ type SyncStatus struct {
|
||||
HasConflicts bool // Whether there are unresolved conflicts
|
||||
}
|
||||
|
||||
// FederationPeer represents a remote peer with authentication credentials.
|
||||
// Used for peer-to-peer Dolt remotes between Gas Towns with SQL user auth.
|
||||
type FederationPeer struct {
|
||||
Name string // Unique name for this peer (used as remote name)
|
||||
RemoteURL string // Dolt remote URL (e.g., http://host:port/org/db)
|
||||
Username string // SQL username for authentication
|
||||
Password string // Password (decrypted, not stored directly)
|
||||
Sovereignty string // Sovereignty tier: T1, T2, T3, T4
|
||||
LastSync *time.Time // Last successful sync time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// IsFederated checks if a storage instance supports federation.
|
||||
func IsFederated(s Storage) bool {
|
||||
_, ok := s.(FederatedStorage)
|
||||
|
||||
Reference in New Issue
Block a user