160 lines
6.1 KiB
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")
|
|
}
|