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:
225
internal/service/shares/shares.go
Normal file
225
internal/service/shares/shares.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user