package db import ( "context" "database/sql" "log" "github.com/example/storage-appliance/internal/auth" ) // MigrateAndSeed performs a very small migration set and seeds an admin user func MigrateAndSeed(ctx context.Context, db *sql.DB) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() stmts := []string{ `CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT, role TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS pools (name TEXT PRIMARY KEY, guid TEXT, health TEXT, capacity TEXT);`, `CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, type TEXT, status TEXT, progress INTEGER DEFAULT 0, owner TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME);`, `CREATE TABLE IF NOT EXISTS shares (id TEXT PRIMARY KEY, name TEXT, path TEXT, type TEXT, options TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS object_storage (id TEXT PRIMARY KEY, name TEXT, access_key TEXT, secret_key TEXT, data_path TEXT, port INTEGER, tls INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS buckets (id TEXT PRIMARY KEY, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS iscsi_targets (id TEXT PRIMARY KEY, iqn TEXT NOT NULL UNIQUE, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS iscsi_portals (id TEXT PRIMARY KEY, target_id TEXT NOT NULL, address TEXT NOT NULL, port INTEGER DEFAULT 3260, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS iscsi_initiators (id TEXT PRIMARY KEY, target_id TEXT NOT NULL, initiator_iqn TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS iscsi_luns (id TEXT PRIMARY KEY, target_id TEXT NOT NULL, lun_id INTEGER NOT NULL, zvol TEXT NOT NULL, size INTEGER, blocksize INTEGER, mapped INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, // RBAC tables `CREATE TABLE IF NOT EXISTS roles (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS permissions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`, `CREATE TABLE IF NOT EXISTS role_permissions (role_id TEXT NOT NULL, permission_id TEXT NOT NULL, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE);`, `CREATE TABLE IF NOT EXISTS user_roles (user_id TEXT NOT NULL, role_id TEXT NOT NULL, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE);`, `CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);`, `CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);`, `CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);`, `CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);`, } for _, s := range stmts { if _, err := tx.ExecContext(ctx, s); err != nil { return err } } // Enhance audit_events table if needed (add missing columns) enhanceAuditTable(ctx, tx) // Seed default roles and permissions if err := seedRolesAndPermissions(ctx, tx); err != nil { return err } // Seed a default admin user if not exists var count int if err := tx.QueryRowContext(ctx, `SELECT COUNT(1) FROM users WHERE username = 'admin'`).Scan(&count); err != nil { log.Printf("seed check failed: %v", err) } if count == 0 { // note: simple seeded password: admin (do not use in prod) pwHash, err := auth.HashPassword("admin") if err != nil { return err } if _, err := tx.ExecContext(ctx, `INSERT INTO users (id, username, password_hash, role) VALUES (?, 'admin', ?, 'admin')`, "admin", pwHash); err != nil { return err } // Assign admin role to admin user var adminRoleID string if err := tx.QueryRowContext(ctx, `SELECT id FROM roles WHERE name = 'admin'`).Scan(&adminRoleID); err == nil { tx.ExecContext(ctx, `INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES (?, ?)`, "admin", adminRoleID) } } if err := tx.Commit(); err != nil { return err } return nil } func enhanceAuditTable(ctx context.Context, tx *sql.Tx) { // Check if columns exist and add them if missing // SQLite doesn't support IF NOT EXISTS for ALTER TABLE, so we'll try-catch columns := []struct { name string stmt string }{ {"actor", `ALTER TABLE audit_events ADD COLUMN actor TEXT;`}, {"resource", `ALTER TABLE audit_events ADD COLUMN resource TEXT;`}, {"payload_hash", `ALTER TABLE audit_events ADD COLUMN payload_hash TEXT;`}, {"result", `ALTER TABLE audit_events ADD COLUMN result TEXT;`}, {"client_ip", `ALTER TABLE audit_events ADD COLUMN client_ip TEXT;`}, } for _, col := range columns { _, err := tx.ExecContext(ctx, col.stmt) if err != nil { // Column might already exist, ignore error log.Printf("Note: %s column may already exist: %v", col.name, err) } } } func seedRolesAndPermissions(ctx context.Context, tx *sql.Tx) error { // Seed default roles roles := []struct { id string name string description string }{ {"admin", "admin", "Administrator with full access"}, {"operator", "operator", "Operator with limited administrative access"}, {"viewer", "viewer", "Read-only access"}, } for _, r := range roles { _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO roles (id, name, description) VALUES (?, ?, ?)`, r.id, r.name, r.description) if err != nil { return err } } // Seed default permissions permissions := []struct { id string name string description string }{ {"storage.pool.create", "storage.pool.create", "Create storage pools"}, {"storage.pool.scrub", "storage.pool.scrub", "Scrub storage pools"}, {"storage.dataset.create", "storage.dataset.create", "Create datasets"}, {"storage.dataset.snapshot", "storage.dataset.snapshot", "Create snapshots"}, {"shares.nfs.create", "shares.nfs.create", "Create NFS shares"}, {"shares.nfs.delete", "shares.nfs.delete", "Delete NFS shares"}, {"shares.smb.create", "shares.smb.create", "Create SMB shares"}, {"shares.smb.delete", "shares.smb.delete", "Delete SMB shares"}, {"iscsi.target.create", "iscsi.target.create", "Create iSCSI targets"}, {"iscsi.lun.create", "iscsi.lun.create", "Create iSCSI LUNs"}, {"iscsi.lun.delete", "iscsi.lun.delete", "Delete iSCSI LUNs"}, {"iscsi.lun.unmap", "iscsi.lun.unmap", "Unmap iSCSI LUNs"}, {"iscsi.portal.create", "iscsi.portal.create", "Add iSCSI portals"}, {"iscsi.initiator.create", "iscsi.initiator.create", "Add iSCSI initiators"}, {"users.manage", "users.manage", "Manage users"}, {"roles.manage", "roles.manage", "Manage roles and permissions"}, } for _, p := range permissions { _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO permissions (id, name, description) VALUES (?, ?, ?)`, p.id, p.name, p.description) if err != nil { return err } } // Assign all permissions to admin role var adminRoleID string if err := tx.QueryRowContext(ctx, `SELECT id FROM roles WHERE name = 'admin'`).Scan(&adminRoleID); err != nil { return err } for _, p := range permissions { _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)`, adminRoleID, p.id) if err != nil { return err } } // Assign some permissions to operator role var operatorRoleID string if err := tx.QueryRowContext(ctx, `SELECT id FROM roles WHERE name = 'operator'`).Scan(&operatorRoleID); err == nil { operatorPerms := []string{ "storage.pool.create", "storage.dataset.create", "storage.dataset.snapshot", "shares.nfs.create", "shares.nfs.delete", "shares.smb.create", "shares.smb.delete", "iscsi.target.create", "iscsi.lun.create", "iscsi.lun.delete", "iscsi.portal.create", "iscsi.initiator.create", } for _, permID := range operatorPerms { tx.ExecContext(ctx, `INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)`, operatorRoleID, permID) } } return nil }