Files
storage-appliance/internal/infra/sqlite/db/migrations.go

190 lines
8.4 KiB
Go

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
}