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,225 @@
package shares
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"strings"
"github.com/example/storage-appliance/internal/audit"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/nfs"
"github.com/example/storage-appliance/internal/infra/samba"
)
var (
ErrForbidden = errors.New("forbidden")
)
type SharesService struct {
DB *sql.DB
NFS *nfs.Adapter
Samba *samba.Adapter
Audit audit.AuditLogger
}
func NewSharesService(db *sql.DB, n *nfs.Adapter, s *samba.Adapter, a audit.AuditLogger) *SharesService {
return &SharesService{DB: db, NFS: n, Samba: s, Audit: a}
}
func (s *SharesService) ListNFS(ctx context.Context) ([]domain.Share, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT id, name, path, type, options FROM shares WHERE type = 'nfs'`)
if err != nil {
return nil, err
}
defer rows.Close()
var res []domain.Share
for rows.Next() {
var id, name, path, typ, options string
if err := rows.Scan(&id, &name, &path, &typ, &options); err != nil {
return nil, err
}
var optMap map[string]string
if options != "" {
_ = json.Unmarshal([]byte(options), &optMap)
}
res = append(res, domain.Share{ID: domain.UUID(id), Name: name, Path: path, Type: typ})
}
return res, nil
}
// CreateNFS stores a new NFS export, re-renders /etc/exports and applies it
func (s *SharesService) CreateNFS(ctx context.Context, user, role, name, path string, opts map[string]string) (string, error) {
if role != "admin" && role != "operator" {
return "", ErrForbidden
}
// Verify path exists and is a dataset: check dataset table for matching name
var count int
if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(1) FROM datasets WHERE name = ?`, path).Scan(&count); err != nil {
return "", err
}
if count == 0 {
return "", fmt.Errorf("path not a known dataset: %s", path)
}
// Prevent exporting system paths: disallow leading '/' entries; require dataset name like pool/ds
if path == "/" || path == "/etc" || path == "/bin" || path == "/usr" {
return "", fmt.Errorf("can't export system path: %s", path)
}
// store options as JSON
optJSON, _ := json.Marshal(opts)
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO shares (id, name, path, type, options) VALUES (?, ?, ?, 'nfs', ?)`, id, name, path, string(optJSON)); err != nil {
return "", err
}
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "nfs.create", ResourceType: "share", ResourceID: name, Success: true, Details: map[string]any{"path": path}})
}
// re-render exports
shares, err := s.ListNFS(ctx)
if err != nil {
return id, err
}
if s.NFS != nil {
if err := s.NFS.RenderExports(ctx, shares); err != nil {
return id, err
}
if err := s.NFS.Apply(ctx); err != nil {
return id, err
}
}
return id, nil
}
// SMB functions
func (s *SharesService) ListSMB(ctx context.Context) ([]domain.Share, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT id, name, path, type, options FROM shares WHERE type = 'smb'`)
if err != nil {
return nil, err
}
defer rows.Close()
var res []domain.Share
for rows.Next() {
var id, name, path, typ, options string
if err := rows.Scan(&id, &name, &path, &typ, &options); err != nil {
return nil, err
}
var config map[string]string
if options != "" {
_ = json.Unmarshal([]byte(options), &config)
}
res = append(res, domain.Share{ID: domain.UUID(id), Name: name, Path: path, Type: typ, Config: config})
}
return res, nil
}
func (s *SharesService) CreateSMB(ctx context.Context, user, role, name, path string, readOnly bool, allowedUsers []string) (string, error) {
if role != "admin" && role != "operator" {
return "", ErrForbidden
}
// Verify dataset
var count int
if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(1) FROM datasets WHERE name = ?`, path).Scan(&count); err != nil {
return "", err
}
if count == 0 {
return "", fmt.Errorf("path not a known dataset: %s", path)
}
// disallow system paths by basic checks
if path == "/" || path == "/etc" || path == "/bin" || path == "/usr" {
return "", fmt.Errorf("can't export system path: %s", path)
}
// store options as JSON (read_only, allowed_users)
cfg := map[string]string{}
if readOnly {
cfg["read_only"] = "true"
} else {
cfg["read_only"] = "false"
}
if len(allowedUsers) > 0 {
cfg["allowed_users"] = strings.Join(allowedUsers, " ")
}
optJSON, _ := json.Marshal(cfg)
id := uuid.New().String()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO shares (id, name, path, type, options) VALUES (?, ?, ?, 'smb', ?)`, id, name, path, string(optJSON)); err != nil {
return "", err
}
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "smb.create", ResourceType: "share", ResourceID: name, Success: true, Details: map[string]any{"path": path, "read_only": readOnly}})
}
// re-render smb conf and reload
shares, err := s.ListSMB(ctx)
if err != nil {
return id, err
}
if s.Samba != nil {
if err := s.Samba.RenderConf(ctx, shares); err != nil {
return id, err
}
if err := s.Samba.Reload(ctx); err != nil {
return id, err
}
}
return id, nil
}
func (s *SharesService) DeleteSMB(ctx context.Context, user, role, id string) error {
if role != "admin" && role != "operator" {
return ErrForbidden
}
if _, err := s.DB.ExecContext(ctx, `DELETE FROM shares WHERE id = ?`, id); err != nil {
return err
}
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "smb.delete", ResourceType: "share", ResourceID: id, Success: true})
}
shares, err := s.ListSMB(ctx)
if err != nil {
return err
}
if s.Samba != nil {
if err := s.Samba.RenderConf(ctx, shares); err != nil {
return err
}
if err := s.Samba.Reload(ctx); err != nil {
return err
}
}
return nil
}
func (s *SharesService) DeleteNFS(ctx context.Context, user, role, id string) error {
if role != "admin" && role != "operator" {
return ErrForbidden
}
// verify exists
if _, err := s.DB.ExecContext(ctx, `DELETE FROM shares WHERE id = ?`, id); err != nil {
return err
}
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "nfs.delete", ResourceType: "share", ResourceID: id, Success: true})
}
// re-render exports
shares, err := s.ListNFS(ctx)
if err != nil {
return err
}
if s.NFS != nil {
if err := s.NFS.RenderExports(ctx, shares); err != nil {
return err
}
if err := s.NFS.Apply(ctx); err != nil {
return err
}
}
return nil
}
func (s *SharesService) NFSStatus(ctx context.Context) (string, error) {
if s.NFS == nil {
return "unavailable", nil
}
return s.NFS.Status(ctx)
}