Add RBAC support with roles, permissions, and session management. Implement middleware for authentication and CSRF protection. Enhance audit logging with additional fields. Update HTTP handlers and routes for new features.
This commit is contained in:
@@ -3,8 +3,9 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
|
||||
"github.com/example/storage-appliance/internal/auth"
|
||||
)
|
||||
|
||||
// MigrateAndSeed performs a very small migration set and seeds an admin user
|
||||
@@ -19,12 +20,37 @@ func MigrateAndSeed(ctx context.Context, db *sql.DB) error {
|
||||
`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 {
|
||||
@@ -32,13 +58,132 @@ func MigrateAndSeed(ctx context.Context, db *sql.DB) error {
|
||||
}
|
||||
if count == 0 {
|
||||
// note: simple seeded password: admin (do not use in prod)
|
||||
pwHash, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO users (id, username, password_hash, role) VALUES (?, 'admin', ?, 'admin')`, "admin", string(pwHash)); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user