190 lines
8.4 KiB
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
|
|
}
|