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,228 @@
package iscsi
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/example/storage-appliance/internal/audit"
"github.com/example/storage-appliance/internal/infra/iscsi"
"github.com/example/storage-appliance/internal/infra/zfs"
)
var (
ErrForbidden = errors.New("forbidden")
)
type ISCSIService struct {
DB *sql.DB
ZFS *zfs.Adapter
ISCSI *iscsi.Adapter
Audit audit.AuditLogger
}
func NewISCSIService(db *sql.DB, z *zfs.Adapter, i *iscsi.Adapter, a audit.AuditLogger) *ISCSIService {
return &ISCSIService{DB: db, ZFS: z, ISCSI: i, Audit: a}
}
func (s *ISCSIService) ListTargets(ctx context.Context) ([]map[string]any, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT id, iqn, name, created_at FROM iscsi_targets`)
if err != nil { return nil, err }
defer rows.Close()
res := []map[string]any{}
for rows.Next() {
var id, iqn, name string
var created time.Time
if err := rows.Scan(&id, &iqn, &name, &created); err != nil { return nil, err }
res = append(res, map[string]any{"id": id, "iqn": iqn, "name": name, "created_at": created})
}
return res, nil
}
func (s *ISCSIService) CreateTarget(ctx context.Context, user, role, name, iqn string) (string, error) {
if role != "admin" { return "", ErrForbidden }
if iqn == "" || !strings.HasPrefix(iqn, "iqn.") { return "", errors.New("invalid IQN") }
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_targets (id, iqn, name) VALUES (?, ?, ?)`, id, iqn, name); err != nil {
return "", err
}
if s.ISCSI != nil {
if err := s.ISCSI.CreateTarget(ctx, iqn); err != nil {
return "", err
}
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
}
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.target.create", ResourceType: "iscsi_target", ResourceID: id, Success: true}) }
return id, nil
}
// CreateLUN creates a zvol and maps it as LUN for the IQN. lunName is the zvol path e.g. pool/dataset/vol
func (s *ISCSIService) CreateLUN(ctx context.Context, user, role, targetID, lunName string, size string, blocksize int) (string, error) {
if role != "admin" && role != "operator" { return "", ErrForbidden }
// fetch target IQN
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil {
return "", err
}
// build zvol name
zvol := lunName // expect fully qualified dataset, e.g., pool/iscsi/target/lun0
// create zvol via zfs adapter
props := map[string]string{}
if blocksize > 0 {
// convert bytes to K unit if divisible
// For simplicity, just set volblocksize as "8K" or "512"; attempt simple conversion
props["volblocksize"] = fmt.Sprintf("%d", blocksize)
}
if s.ZFS != nil {
if _, err := s.ZFS.ListDatasets(ctx, ""); err == nil { // no-op to check connectivity
}
if err := s.ZFS.CreateZVol(ctx, zvol, size, props); err != nil {
return "", err
}
}
// backstore name and device path
bsName := "bs-" + uuid.New().String()
devpath := "/dev/zvol/" + zvol
if s.ISCSI != nil {
if err := s.ISCSI.CreateBackstore(ctx, bsName, devpath); err != nil {
return "", err
}
}
// determine LUN ID as next available for target
var maxLun sql.NullInt64
if err := s.DB.QueryRowContext(ctx, `SELECT MAX(lun_id) FROM iscsi_luns WHERE target_id = ?`, targetID).Scan(&maxLun); err != nil && err != sql.ErrNoRows { return "", err }
nextLun := 0
if maxLun.Valid { nextLun = int(maxLun.Int64) + 1 }
if s.ISCSI != nil {
if err := s.ISCSI.CreateLUN(ctx, iqn, bsName, nextLun); err != nil {
return "", err
}
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
}
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_luns (id, target_id, lun_id, zvol, size, blocksize, mapped) VALUES (?, ?, ?, ?, ?, ?, 1)`, id, targetID, nextLun, zvol, sizeToInt(size), blocksize); err != nil {
return "", err
}
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.create", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
return id, nil
}
func sizeToInt(s string) int {
// naive conversion: strip trailing G/M/K
// This function can be improved; for now return 0
return 0
}
func (s *ISCSIService) ListLUNs(ctx context.Context, targetID string) ([]map[string]any, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT id, lun_id, zvol, size, blocksize, mapped, created_at FROM iscsi_luns WHERE target_id = ?`, targetID)
if err != nil { return nil, err }
defer rows.Close()
res := []map[string]any{}
for rows.Next() {
var id, zvol string
var lunID int
var size int
var blocksize int
var mapped int
var created time.Time
if err := rows.Scan(&id, &lunID, &zvol, &size, &blocksize, &mapped, &created); err != nil { return nil, err }
res = append(res, map[string]any{"id": id, "lun_id": lunID, "zvol": zvol, "size": size, "blocksize": blocksize, "mapped": mapped == 1, "created_at": created})
}
return res, nil
}
func (s *ISCSIService) GetTargetInfo(ctx context.Context, targetID string) (map[string]any, error) {
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return nil, err }
portals := []map[string]any{}
rows, err := s.DB.QueryContext(ctx, `SELECT id, address, port FROM iscsi_portals WHERE target_id = ?`, targetID)
if err != nil { return nil, err }
defer rows.Close()
for rows.Next() {
var id, address string
var port int
if err := rows.Scan(&id, &address, &port); err != nil { return nil, err }
portals = append(portals, map[string]any{"id": id, "address": address, "port": port})
}
inits := []map[string]any{}
rows2, err := s.DB.QueryContext(ctx, `SELECT id, initiator_iqn FROM iscsi_initiators WHERE target_id = ?`, targetID)
if err != nil { return nil, err }
defer rows2.Close()
for rows2.Next() {
var id, iqnStr string
if err := rows2.Scan(&id, &iqnStr); err != nil { return nil, err }
inits = append(inits, map[string]any{"id": id, "iqn": iqnStr})
}
res := map[string]any{"iqn": iqn, "portals": portals, "initiators": inits}
return res, nil
}
func (s *ISCSIService) DeleteLUN(ctx context.Context, user, role, id string, force bool) error {
if role != "admin" { return ErrForbidden }
// check LUN
var mappedInt int
var targetID string
var lunID int
if err := s.DB.QueryRowContext(ctx, `SELECT target_id, lun_id, mapped FROM iscsi_luns WHERE id = ?`, id).Scan(&targetID, &lunID, &mappedInt); err != nil { return err }
if mappedInt == 1 && !force { return errors.New("LUN is mapped; unmap (drain) before deletion or specify force") }
// delete via adapter
if s.ISCSI != nil {
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return err }
if err := s.ISCSI.DeleteLUN(ctx, iqn, lunID); err != nil { return err }
if err := s.ISCSI.Save(ctx); err != nil { return err }
}
if _, err := s.DB.ExecContext(ctx, `DELETE FROM iscsi_luns WHERE id = ?`, id); err != nil { return err }
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.delete", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
return nil
}
// UnmapLUN removes LUN mapping from target, sets mapped to false in DB
func (s *ISCSIService) UnmapLUN(ctx context.Context, user, role, id string) error {
if role != "admin" && role != "operator" { return ErrForbidden }
var targetID string
var lunID int
if err := s.DB.QueryRowContext(ctx, `SELECT target_id, lun_id FROM iscsi_luns WHERE id = ?`, id).Scan(&targetID, &lunID); err != nil { return err }
if s.ISCSI != nil {
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return err }
if err := s.ISCSI.DeleteLUN(ctx, iqn, lunID); err != nil { return err }
if err := s.ISCSI.Save(ctx); err != nil { return err }
}
if _, err := s.DB.ExecContext(ctx, `UPDATE iscsi_luns SET mapped = 0 WHERE id = ?`, id); err != nil { return err }
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.lun.unmap", ResourceType: "iscsi_lun", ResourceID: id, Success: true}) }
return nil
}
func (s *ISCSIService) AddPortal(ctx context.Context, user, role, targetID, address string, port int) (string, error) {
if role != "admin" && role != "operator" { return "", ErrForbidden }
// verify target exists and fetch IQN
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return "", err }
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_portals (id, target_id, address, port) VALUES (?, ?, ?, ?)`, id, targetID, address, port); err != nil { return "", err }
if s.ISCSI != nil {
if err := s.ISCSI.AddPortal(ctx, iqn, address, port); err != nil { return "", err }
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
}
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.portal.add", ResourceType: "iscsi_portal", ResourceID: id, Success: true}) }
return id, nil
}
func (s *ISCSIService) AddInitiator(ctx context.Context, user, role, targetID, initiatorIQN string) (string, error) {
if role != "admin" && role != "operator" { return "", ErrForbidden }
var iqn string
if err := s.DB.QueryRowContext(ctx, `SELECT iqn FROM iscsi_targets WHERE id = ?`, targetID).Scan(&iqn); err != nil { return "", err }
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO iscsi_initiators (id, target_id, initiator_iqn) VALUES (?, ?, ?)`, id, targetID, initiatorIQN); err != nil { return "", err }
if s.ISCSI != nil {
if err := s.ISCSI.AddACL(ctx, iqn, initiatorIQN); err != nil { return "", err }
if err := s.ISCSI.Save(ctx); err != nil { return "", err }
}
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "iscsi.initiator.add", ResourceType: "iscsi_initiator", ResourceID: id, Success: true}) }
return id, nil
}