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:
2025-12-13 17:44:09 +00:00
parent d69e01bbaf
commit 8100f87686
44 changed files with 3262 additions and 76 deletions

View File

@@ -0,0 +1,59 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
// Encrypt uses AES-GCM with a 32 byte key
func Encrypt(key []byte, plaintext string) (string, error) {
if len(key) != 32 {
return "", errors.New("invalid key length")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ct := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ct), nil
}
func Decrypt(key []byte, cipherText string) (string, error) {
if len(key) != 32 {
return "", errors.New("invalid key length")
}
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := aesgcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ct := data[:nonceSize], data[nonceSize:]
pt, err := aesgcm.Open(nil, nonce, ct, nil)
if err != nil {
return "", err
}
return string(pt), nil
}

View File

@@ -0,0 +1,121 @@
package iscsi
import (
"context"
"fmt"
"time"
"github.com/example/storage-appliance/internal/infra/osexec"
)
// Adapter wraps targetcli invocations for LIO (targetcli) management.
type Adapter struct {
Runner osexec.Runner
}
func NewAdapter(runner osexec.Runner) *Adapter { return &Adapter{Runner: runner} }
// CreateTarget creates an IQN target via targetcli
func (a *Adapter) CreateTarget(ctx context.Context, iqn string) error {
// Use a short timeout for cli interactions
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", "/iscsi", "create", iqn)
if err != nil {
return fmt.Errorf("targetcli create target failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli create returned: %s", stderr)
}
return nil
}
// CreateBackstore creates a block backstore for a zvol device.
func (a *Adapter) CreateBackstore(ctx context.Context, name, devpath string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// targetcli syntax: /backstores/block create <name> <devpath>
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", "/backstores/block", "create", name, devpath)
if err != nil {
return fmt.Errorf("targetcli create backstore failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli backstore returned: %s", stderr)
}
return nil
}
// CreateLUN maps backstore into target's TPG1 LUNs
func (a *Adapter) CreateLUN(ctx context.Context, iqn, backstoreName string, lunID int) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
bsPath := fmt.Sprintf("/backstores/block/%s", backstoreName)
tpgPath := fmt.Sprintf("/iscsi/%s/tpg1/luns", iqn)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", tpgPath, "create", bsPath)
if err != nil {
return fmt.Errorf("targetcli create lun failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli create lun returned: %s", stderr)
}
return nil
}
// DeleteLUN unmaps a LUN from a target; if the LUN is mapped fail unless forced.
func (a *Adapter) DeleteLUN(ctx context.Context, iqn string, lunID int) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
tpgPath := fmt.Sprintf("/iscsi/%s/tpg1/luns", iqn)
// delete by numeric id
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", tpgPath, "delete", fmt.Sprintf("%d", lunID))
if err != nil {
return fmt.Errorf("targetcli delete lun failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli delete lun returned: %s", stderr)
}
return nil
}
func (a *Adapter) AddPortal(ctx context.Context, iqn, address string, port int) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
tpgPath := fmt.Sprintf("/iscsi/%s/tpg1/portals", iqn)
addr := fmt.Sprintf("%s:%d", address, port)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", tpgPath, "create", addr)
if err != nil {
return fmt.Errorf("targetcli add portal failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli add portal returned: %s", stderr)
}
return nil
}
func (a *Adapter) AddACL(ctx context.Context, iqn, initiator string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
aclPath := fmt.Sprintf("/iscsi/%s/tpg1/acls", iqn)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", aclPath, "create", initiator)
if err != nil {
return fmt.Errorf("targetcli add acl failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli add acl returned: %s", stderr)
}
return nil
}
// Save writes the configuration to storage (saving targetcli config)
func (a *Adapter) Save(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "targetcli", "saveconfig")
if err != nil {
return fmt.Errorf("targetcli save failed: %v %s", err, stderr)
}
if code != 0 {
return fmt.Errorf("targetcli save returned: %s", stderr)
}
return nil
}

View File

@@ -0,0 +1,122 @@
package minio
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/example/storage-appliance/internal/infra/osexec"
)
type Adapter struct {
Runner osexec.Runner
EnvPath string
}
func NewAdapter(runner osexec.Runner, envPath string) *Adapter {
if envPath == "" {
envPath = "/etc/minio/minio.env"
}
return &Adapter{Runner: runner, EnvPath: envPath}
}
type Settings struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
DataPath string `json:"data_path"`
Port int `json:"port"`
TLS bool `json:"tls"`
}
// WriteEnv writes environment file used by MinIO service
func (a *Adapter) WriteEnv(ctx context.Context, s Settings) error {
dir := filepath.Dir(a.EnvPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// env lines
lines := []string{
fmt.Sprintf("MINIO_ROOT_USER=%s", s.AccessKey),
fmt.Sprintf("MINIO_ROOT_PASSWORD=%s", s.SecretKey),
fmt.Sprintf("MINIO_VOLUMES=%s", s.DataPath),
}
if s.Port != 0 {
lines = append(lines, fmt.Sprintf("MINIO_OPTS=--address :%d", s.Port))
}
content := strings.Join(lines, "\n") + "\n"
tmp := filepath.Join(dir, ".minio.env.tmp")
if err := os.WriteFile(tmp, []byte(content), 0600); err != nil {
return err
}
if err := os.Rename(tmp, a.EnvPath); err != nil {
return err
}
return nil
}
// Reload reloads minio service to pick up new env; prefer systemctl reload
func (a *Adapter) Reload(ctx context.Context) error {
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "reload", "minio")
if err == nil {
return nil
}
// fallback to restart
_, stderr, _, err = osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "restart", "minio")
if err != nil {
return fmt.Errorf("minio reload/restart failed: %s", stderr)
}
return nil
}
// ConfigureMC configures mc alias to point to the MinIO service using given settings
func (a *Adapter) ConfigureMC(ctx context.Context, alias string, settings Settings) error {
secure := "--insecure"
if settings.TLS {
secure = ""
}
// mc alias set <alias> <endpoint> <access> <secret> [--api S3v4]
endpoint := fmt.Sprintf("http://127.0.0.1:%d", settings.Port)
if settings.TLS {
endpoint = fmt.Sprintf("https://127.0.0.1:%d", settings.Port)
}
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "mc", "alias", "set", alias, endpoint, settings.AccessKey, settings.SecretKey, secure)
if err != nil {
return fmt.Errorf("mc alias set failed: %s", stderr)
}
return nil
}
// ListBuckets uses mc to list buckets via alias
func (a *Adapter) ListBuckets(ctx context.Context, alias string) ([]string, error) {
out, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "mc", "ls", "--json", alias)
if err != nil {
return nil, fmt.Errorf("mc ls failed: %s", stderr)
}
// parse JSON lines, each contains a 'key' or 'name' - in mc, `ls --json` returns 'key'
var buckets []string
lines := strings.Split(strings.TrimSpace(out), "\n")
for _, l := range lines {
var obj map[string]any
if err := json.Unmarshal([]byte(l), &obj); err != nil {
continue
}
if otype, ok := obj["type"].(string); ok && otype == "bucket" {
if name, ok := obj["key"].(string); ok {
buckets = append(buckets, name)
}
}
}
return buckets, nil
}
// CreateBucket uses mc to create a new bucket alias/<name>
func (a *Adapter) CreateBucket(ctx context.Context, alias, name string) error {
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "mc", "mb", alias+"/"+name)
if err != nil {
return fmt.Errorf("mc mb failed: %s", stderr)
}
return nil
}

74
internal/infra/nfs/nfs.go Normal file
View File

@@ -0,0 +1,74 @@
package nfs
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/osexec"
)
type Adapter struct {
Runner osexec.Runner
ExportsPath string
}
func NewAdapter(runner osexec.Runner, exportsPath string) *Adapter {
if exportsPath == "" {
exportsPath = "/etc/exports"
}
return &Adapter{Runner: runner, ExportsPath: exportsPath}
}
// RenderExports renders the given shares into /etc/exports atomically
func (a *Adapter) RenderExports(ctx context.Context, shares []domain.Share) error {
var lines []string
for _, s := range shares {
// default options for NFS export
opts := "rw,sync,no_root_squash"
if s.Type == "nfs" {
// if options stored as JSON use it
if sPath := s.Path; sPath != "" {
// options may be in s.Name? No, for now use default
}
}
lines = append(lines, fmt.Sprintf("%s %s", s.Path, opts))
}
content := strings.Join(lines, "\n") + "\n"
dir := filepath.Dir(a.ExportsPath)
tmp := filepath.Join(dir, ".exports.tmp")
if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
return err
}
// atomic rename
if err := os.Rename(tmp, a.ExportsPath); err != nil {
return err
}
return nil
}
// Apply runs exportfs -ra to apply exports
func (a *Adapter) Apply(ctx context.Context) error {
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "exportfs", "-ra")
if err != nil {
return fmt.Errorf("exportfs failed: %s", stderr)
}
return nil
}
// Status checks systemd for nfs server status
func (a *Adapter) Status(ctx context.Context) (string, error) {
// try common unit names
names := []string{"nfs-server", "nfs-kernel-server"}
for _, n := range names {
out, _, _, err := osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "is-active", n)
if err == nil && strings.TrimSpace(out) != "" {
return strings.TrimSpace(out), nil
}
}
return "unknown", nil
}

View File

@@ -0,0 +1,87 @@
package samba
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/osexec"
)
type Adapter struct {
Runner osexec.Runner
IncludePath string
}
func NewAdapter(runner osexec.Runner, includePath string) *Adapter {
if includePath == "" {
includePath = "/etc/samba/smb.conf.d/appliance.conf"
}
return &Adapter{Runner: runner, IncludePath: includePath}
}
// RenderConf writes the Samba include file for appliance-managed shares
func (a *Adapter) RenderConf(ctx context.Context, shares []domain.Share) error {
var lines []string
lines = append(lines, "# Appliance-managed SMB share configuration")
for _, s := range shares {
if s.Type != "smb" {
continue
}
opts := []string{"path = " + s.Path}
// parse options if stored in s.Name or s.Config; fallback to broad default
// s.Config may have read-only or allowed users
if ro, ok := s.Config["read_only"]; ok && ro == "true" {
opts = append(opts, "read only = yes")
} else {
opts = append(opts, "read only = no")
}
if users, ok := s.Config["allowed_users"]; ok {
opts = append(opts, "valid users = "+users)
}
// write section
lines = append(lines, fmt.Sprintf("[%s]", s.Name))
for _, l := range opts {
lines = append(lines, l)
}
lines = append(lines, "")
}
content := strings.Join(lines, "\n") + "\n"
dir := filepath.Dir(a.IncludePath)
tmp := filepath.Join(dir, ".appliance.smb.tmp")
if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
return err
}
if err := os.Rename(tmp, a.IncludePath); err != nil {
return err
}
return nil
}
// Reload reloads or restarts samba to apply config
func (a *Adapter) Reload(ctx context.Context) error {
// try to reload first
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "reload", "smbd")
if err == nil {
return nil
}
// fallback to restart
_, stderr, _, err = osexec.ExecWithRunner(a.Runner, ctx, "systemctl", "restart", "smbd")
if err != nil {
return fmt.Errorf("samba reload/restart failed: %s", stderr)
}
return nil
}
// CreateSambaUser optional: stub for creating a local samba user mapped to appliance user
func (a *Adapter) CreateSambaUser(ctx context.Context, user, password string) error {
// This is optional - we use smbpasswd command in production; stub for now
_, stderr, _, err := osexec.ExecWithRunner(a.Runner, ctx, "smbpasswd", "-a", user)
if err != nil {
return fmt.Errorf("smbpasswd failed: %s", stderr)
}
return nil
}

View File

@@ -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
}

View File

@@ -16,3 +16,28 @@ func (i *ISCSIAdapter) CreateLUN(ctx context.Context, target string, backstore s
log.Printf("iscsi: CreateLUN target=%s backstore=%s lun=%d (stub)", target, backstore, lunID)
return nil
}
func (i *ISCSIAdapter) CreateBackstore(ctx context.Context, name string, devpath string) error {
log.Printf("iscsi: CreateBackstore name=%s dev=%s (stub)", name, devpath)
return nil
}
func (i *ISCSIAdapter) DeleteLUN(ctx context.Context, target string, lunID int) error {
log.Printf("iscsi: DeleteLUN target=%s lun=%d (stub)", target, lunID)
return nil
}
func (i *ISCSIAdapter) AddPortal(ctx context.Context, iqn string, addr string, port int) error {
log.Printf("iscsi: AddPortal iqn=%s addr=%s port=%d (stub)", iqn, addr, port)
return nil
}
func (i *ISCSIAdapter) AddACL(ctx context.Context, iqn, initiator string) error {
log.Printf("iscsi: AddACL iqn=%s initiator=%s (stub)", iqn, initiator)
return nil
}
func (i *ISCSIAdapter) Save(ctx context.Context) error {
log.Printf("iscsi: Save (stub)")
return nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/osexec"
@@ -100,6 +99,30 @@ func (a *Adapter) CreateDataset(ctx context.Context, name string, props map[stri
return nil
}
// CreateZVol creates a block device zvol with the given size and optional props
func (a *Adapter) CreateZVol(ctx context.Context, name, size string, props map[string]string) error {
args := []string{"create", "-V", size, name}
for k, v := range props {
args = append([]string{"create", "-o", fmt.Sprintf("%s=%s", k, v)}, args...)
}
// Note: above building may produce repeated 'create' parts - keep simple: build args now
// We'll just construct a direct zfs create -V size -o prop=val name
args = []string{"create", "-V", size}
for k, v := range props {
args = append(args, "-o", fmt.Sprintf("%s=%s", k, v))
}
args = append(args, name)
out, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zfs", args...)
_ = out
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("zfs create vol failed: %s", stderr)
}
return nil
}
func (a *Adapter) Snapshot(ctx context.Context, dataset, snapName string) error {
name := fmt.Sprintf("%s@%s", dataset, snapName)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zfs", "snapshot", name)

View File

@@ -3,7 +3,6 @@ package zfs
import (
"context"
"testing"
"time"
"github.com/example/storage-appliance/internal/infra/osexec"
)