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") }