Files
storage-appliance/internal/service/objectstore/objectstore.go

160 lines
6.1 KiB
Go

package objectstore
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/example/storage-appliance/internal/audit"
"github.com/example/storage-appliance/internal/infra/minio"
"github.com/example/storage-appliance/internal/infra/crypto"
)
var (
ErrForbidden = errors.New("forbidden")
)
type Settings struct {
ID string
Name string
AccessKey string
SecretKey string
DataPath string
Port int
TLS bool
CreatedAt time.Time
}
type ObjectService struct {
DB *sql.DB
Minio *minio.Adapter
Audit audit.AuditLogger
// encryption key for secret storage
Key []byte
}
func NewObjectService(db *sql.DB, m *minio.Adapter, a audit.AuditLogger, key []byte) *ObjectService {
return &ObjectService{DB: db, Minio: m, Audit: a, Key: key}
}
func (s *ObjectService) SetSettings(ctx context.Context, user, role string, stMap map[string]any) error {
if role != "admin" {
return ErrForbidden
}
// convert map to Settings struct for local use
st := Settings{}
if v, ok := stMap["access_key"].(string); ok { st.AccessKey = v }
if v, ok := stMap["secret_key"].(string); ok { st.SecretKey = v }
if v, ok := stMap["data_path"].(string); ok { st.DataPath = v }
if v, ok := stMap["name"].(string); ok { st.Name = v }
if v, ok := stMap["port"].(int); ok { st.Port = v }
if v, ok := stMap["tls"].(bool); ok { st.TLS = v }
// encrypt access key and secret key
if len(s.Key) != 32 {
return errors.New("encryption key must be 32 bytes")
}
encAccess, err := crypto.Encrypt(s.Key, st.AccessKey)
if err != nil {
return err
}
encSecret, err := crypto.Encrypt(s.Key, st.SecretKey)
if err != nil {
return err
}
// upsert into DB (single row)
if _, err := s.DB.ExecContext(ctx, `INSERT OR REPLACE INTO object_storage (id, name, access_key, secret_key, data_path, port, tls) VALUES ('minio', ?, ?, ?, ?, ?, ?)` , st.Name, encAccess, encSecret, st.DataPath, st.Port, boolToInt(st.TLS)); err != nil {
return err
}
if s.Audit != nil {
s.Audit.Record(ctx, audit.Event{UserID: user, Action: "object.settings.update", ResourceType: "object_storage", ResourceID: "minio", Success: true})
}
if s.Minio != nil {
// write env file
settings := minio.Settings{AccessKey: st.AccessKey, SecretKey: st.SecretKey, DataPath: st.DataPath, Port: st.Port, TLS: st.TLS}
if err := s.Minio.WriteEnv(ctx, settings); err != nil {
return err
}
if err := s.Minio.Reload(ctx); err != nil {
return err
}
}
return nil
}
func (s *ObjectService) GetSettings(ctx context.Context) (map[string]any, error) {
var st Settings
row := s.DB.QueryRowContext(ctx, `SELECT name, access_key, secret_key, data_path, port, tls, created_at FROM object_storage WHERE id = 'minio'`)
var encAccess, encSecret string
var tlsInt int
if err := row.Scan(&st.Name, &encAccess, &encSecret, &st.DataPath, &st.Port, &tlsInt, &st.CreatedAt); err != nil {
return nil, err
}
st.TLS = tlsInt == 1
if len(s.Key) == 32 {
if A, err := crypto.Decrypt(s.Key, encAccess); err == nil { st.AccessKey = A }
if S, err := crypto.Decrypt(s.Key, encSecret); err == nil { st.SecretKey = S }
}
res := map[string]any{"name": st.Name, "access_key": st.AccessKey, "secret_key": st.SecretKey, "data_path": st.DataPath, "port": st.Port, "tls": st.TLS, "created_at": st.CreatedAt}
return res, nil
}
func boolToInt(b bool) int { if b { return 1 }; return 0 }
// ListBuckets via minio adapter or fallback to DB
func (s *ObjectService) ListBuckets(ctx context.Context) ([]string, error) {
if s.Minio != nil {
// ensure mc alias is configured
stMap, err := s.GetSettings(ctx)
if err != nil { return nil, err }
alias := "appliance"
mSet := minio.Settings{}
if v, ok := stMap["access_key"].(string); ok { mSet.AccessKey = v }
if v, ok := stMap["secret_key"].(string); ok { mSet.SecretKey = v }
if v, ok := stMap["data_path"].(string); ok { mSet.DataPath = v }
if v, ok := stMap["port"].(int); ok { mSet.Port = v }
if v, ok := stMap["tls"].(bool); ok { mSet.TLS = v }
s.Minio.ConfigureMC(ctx, alias, mSet)
return s.Minio.ListBuckets(ctx, alias)
}
// fallback to DB persisted buckets
rows, err := s.DB.QueryContext(ctx, `SELECT name FROM buckets`)
if err != nil { return nil, err }
defer rows.Close()
var res []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil { return nil, err }
res = append(res, name)
}
return res, nil
}
func (s *ObjectService) CreateBucket(ctx context.Context, user, role, name string) (string, error) {
if role != "admin" && role != "operator" { return "", ErrForbidden }
// attempt via minio adapter
if s.Minio != nil {
stMap, err := s.GetSettings(ctx)
if err != nil { return "", err }
alias := "appliance"
mSet := minio.Settings{}
if v, ok := stMap["access_key"].(string); ok { mSet.AccessKey = v }
if v, ok := stMap["secret_key"].(string); ok { mSet.SecretKey = v }
if v, ok := stMap["data_path"].(string); ok { mSet.DataPath = v }
if v, ok := stMap["port"].(int); ok { mSet.Port = v }
if v, ok := stMap["tls"].(bool); ok { mSet.TLS = v }
if err := s.Minio.ConfigureMC(ctx, alias, mSet); err != nil { return "", err }
if err := s.Minio.CreateBucket(ctx, alias, name); err != nil { return "", err }
// persist
id := fmt.Sprintf("bucket-%d", time.Now().UnixNano())
if _, err := s.DB.ExecContext(ctx, `INSERT INTO buckets (id, name) VALUES (?, ?)`, id, name); err != nil {
return "", err
}
if s.Audit != nil { s.Audit.Record(ctx, audit.Event{UserID: user, Action: "object.bucket.create", ResourceType: "bucket", ResourceID: name, Success: true}) }
return id, nil
}
return "", errors.New("no minio adapter configured")
}