Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.
The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.
Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.
Future changes go through PRs against this repo — no more bin-only
deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
377
internal/config/config.go
Normal file
377
internal/config/config.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Databases map[string]DatabaseConfig `yaml:"databases"`
|
||||
TLS TLSConfig `yaml:"tls"`
|
||||
LDAP LDAPConfig `yaml:"ldap"`
|
||||
PowerDNS PowerDNSConfig `yaml:"powerdns"`
|
||||
EJBCA EJBCAConfig `yaml:"ejbca"`
|
||||
Hockeypuck HockeypuckConfig `yaml:"hockeypuck"`
|
||||
Infisical InfisicalConfig `yaml:"infisical"`
|
||||
CardDAV CardDAVConfig `yaml:"carddav"`
|
||||
Asterisk AsteriskConfig `yaml:"asterisk"`
|
||||
Kamailio KamailioConfig `yaml:"kamailio"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
ReadTimeout time.Duration `yaml:"readTimeout"`
|
||||
WriteTimeout time.Duration `yaml:"writeTimeout"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Database string `yaml:"database"`
|
||||
Schema string `yaml:"schema"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
SSLMode string `yaml:"sslMode"`
|
||||
MaxConns int `yaml:"maxConns"`
|
||||
MinConns int `yaml:"minConns"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
CertFile string `yaml:"certFile"`
|
||||
KeyFile string `yaml:"keyFile"`
|
||||
CAFile string `yaml:"caFile"`
|
||||
}
|
||||
|
||||
type LDAPConfig struct {
|
||||
Servers []string `yaml:"servers"`
|
||||
BaseDN string `yaml:"baseDn"`
|
||||
BindDN string `yaml:"bindDn"`
|
||||
BindPass string `yaml:"bindPassword"`
|
||||
PoolSize int `yaml:"poolSize"`
|
||||
UseTLS bool `yaml:"useTls"`
|
||||
CAFile string `yaml:"caFile"`
|
||||
}
|
||||
|
||||
type PowerDNSConfig struct {
|
||||
BaseURL string `yaml:"baseUrl"`
|
||||
APIKey string `yaml:"apiKey"`
|
||||
ServerID string `yaml:"serverId"`
|
||||
}
|
||||
|
||||
type EJBCAConfig struct {
|
||||
BaseURL string `yaml:"baseUrl"`
|
||||
CertFile string `yaml:"certFile"`
|
||||
KeyFile string `yaml:"keyFile"`
|
||||
CAFile string `yaml:"caFile"`
|
||||
}
|
||||
|
||||
type HockeypuckConfig struct {
|
||||
Servers []string `yaml:"servers"`
|
||||
}
|
||||
|
||||
type CardDAVConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Database string `yaml:"database"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
SSLMode string `yaml:"sslMode"`
|
||||
}
|
||||
|
||||
type AsteriskServerConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
AMIPort int `yaml:"amiPort"`
|
||||
}
|
||||
|
||||
type AsteriskConfig struct {
|
||||
Servers []AsteriskServerConfig `yaml:"servers"`
|
||||
AMIUser string `yaml:"amiUser"`
|
||||
AMISecret string `yaml:"amiSecret"`
|
||||
}
|
||||
|
||||
type KamailioConfig struct {
|
||||
Servers []string `yaml:"servers"`
|
||||
SSHUser string `yaml:"sshUser"`
|
||||
SSHKey string `yaml:"sshKeyFile"`
|
||||
}
|
||||
|
||||
type InfisicalConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
ProjectID string `yaml:"projectId"`
|
||||
Environment string `yaml:"environment"`
|
||||
TokenFile string `yaml:"tokenFile"`
|
||||
SecretPath string `yaml:"secretPath"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
APIKeys []string `yaml:"apiKeys"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
Format string `yaml:"format"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
cfg.setDefaults()
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) setDefaults() {
|
||||
if c.Server.Host == "" {
|
||||
c.Server.Host = "0.0.0.0"
|
||||
}
|
||||
if c.Server.Port == 0 {
|
||||
c.Server.Port = 8443
|
||||
}
|
||||
if c.Server.ReadTimeout == 0 {
|
||||
c.Server.ReadTimeout = 30 * time.Second
|
||||
}
|
||||
if c.Server.WriteTimeout == 0 {
|
||||
c.Server.WriteTimeout = 30 * time.Second
|
||||
}
|
||||
// Legacy single database config
|
||||
if c.Database.Port == 0 {
|
||||
c.Database.Port = 5432
|
||||
}
|
||||
if c.Database.SSLMode == "" {
|
||||
c.Database.SSLMode = "require"
|
||||
}
|
||||
if c.Database.MaxConns == 0 {
|
||||
c.Database.MaxConns = 25
|
||||
}
|
||||
if c.Database.MinConns == 0 {
|
||||
c.Database.MinConns = 5
|
||||
}
|
||||
// Multi-database defaults
|
||||
if c.Databases == nil {
|
||||
c.Databases = make(map[string]DatabaseConfig)
|
||||
}
|
||||
for name, db := range c.Databases {
|
||||
if db.Port == 0 {
|
||||
db.Port = 5432
|
||||
}
|
||||
if db.SSLMode == "" {
|
||||
db.SSLMode = c.Database.SSLMode
|
||||
}
|
||||
if db.Host == "" {
|
||||
db.Host = c.Database.Host
|
||||
}
|
||||
if db.MaxConns == 0 {
|
||||
db.MaxConns = 10
|
||||
}
|
||||
if db.MinConns == 0 {
|
||||
db.MinConns = 2
|
||||
}
|
||||
c.Databases[name] = db
|
||||
}
|
||||
if c.LDAP.PoolSize == 0 {
|
||||
c.LDAP.PoolSize = 10
|
||||
}
|
||||
if c.PowerDNS.ServerID == "" {
|
||||
c.PowerDNS.ServerID = "localhost"
|
||||
}
|
||||
if c.CardDAV.Port == 0 {
|
||||
c.CardDAV.Port = 5432
|
||||
}
|
||||
if c.CardDAV.SSLMode == "" {
|
||||
c.CardDAV.SSLMode = "disable"
|
||||
}
|
||||
if c.Asterisk.AMIUser == "" {
|
||||
c.Asterisk.AMIUser = "gsc-ops-api"
|
||||
}
|
||||
for i := range c.Asterisk.Servers {
|
||||
if c.Asterisk.Servers[i].AMIPort == 0 {
|
||||
c.Asterisk.Servers[i].AMIPort = 5038
|
||||
}
|
||||
}
|
||||
if c.Kamailio.SSHUser == "" {
|
||||
c.Kamailio.SSHUser = "root"
|
||||
}
|
||||
if c.Logging.Level == "" {
|
||||
c.Logging.Level = "info"
|
||||
}
|
||||
if c.Logging.Format == "" {
|
||||
c.Logging.Format = "json"
|
||||
}
|
||||
if c.Infisical.SecretPath == "" {
|
||||
c.Infisical.SecretPath = "/gsc-ops-api"
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSecretsFromInfisical fetches secrets from the Infisical API
|
||||
func (c *Config) LoadSecretsFromInfisical() error {
|
||||
if c.Infisical.Host == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tokenFile := c.Infisical.TokenFile
|
||||
if tokenFile == "" {
|
||||
tokenFile = "/etc/gsc-ops-api/.infisical"
|
||||
}
|
||||
|
||||
tokenData, err := os.ReadFile(tokenFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read Infisical token: %w", err)
|
||||
}
|
||||
token := strings.TrimSpace(string(tokenData))
|
||||
|
||||
// Legacy single database credentials
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_DB_USER"); err == nil && v != "" {
|
||||
c.Database.User = v
|
||||
}
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_DB_PASSWORD"); err == nil && v != "" {
|
||||
c.Database.Password = v
|
||||
}
|
||||
|
||||
// Per-database credentials: inherit from primary DB user/password
|
||||
for name, db := range c.Databases {
|
||||
if db.User == "" {
|
||||
db.User = c.Database.User
|
||||
}
|
||||
if db.Password == "" {
|
||||
db.Password = c.Database.Password
|
||||
}
|
||||
c.Databases[name] = db
|
||||
}
|
||||
|
||||
// LDAP credentials
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_DN"); err == nil && v != "" {
|
||||
c.LDAP.BindDN = v
|
||||
}
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_LDAP_BIND_PASSWORD"); err == nil && v != "" {
|
||||
c.LDAP.BindPass = v
|
||||
}
|
||||
|
||||
// PowerDNS API key
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_PDNS_API_KEY"); err == nil && v != "" {
|
||||
c.PowerDNS.APIKey = v
|
||||
}
|
||||
|
||||
// CardDAV DB password
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_CARDDAV_DB_PASSWORD"); err == nil && v != "" {
|
||||
c.CardDAV.Password = v
|
||||
}
|
||||
|
||||
// API keys for authorized clients
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SKILL_SERVER"); err == nil && v != "" {
|
||||
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
|
||||
}
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_VOICE_AGENT"); err == nil && v != "" {
|
||||
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
|
||||
}
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_GSC_MY"); err == nil && v != "" {
|
||||
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
|
||||
}
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_API_KEY_SYNAPSE_HUB"); err == nil && v != "" {
|
||||
c.Auth.APIKeys = append(c.Auth.APIKeys, v)
|
||||
}
|
||||
|
||||
// Asterisk AMI secret
|
||||
if v, err := c.fetchSecret(token, "GSC_OPS_ASTERISK_AMI_SECRET"); err == nil && v != "" {
|
||||
c.Asterisk.AMISecret = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) fetchSecret(token, secretName string) (string, error) {
|
||||
url := fmt.Sprintf("%s/api/v3/secrets/raw/%s?workspaceId=%s&environment=%s&secretPath=%s",
|
||||
c.Infisical.Host, secretName, c.Infisical.ProjectID, c.Infisical.Environment, c.Infisical.SecretPath)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("Infisical returned status %d for %s", resp.StatusCode, secretName)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Secret struct {
|
||||
SecretValue string `json:"secretValue"`
|
||||
} `json:"secret"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.Secret.SecretValue, nil
|
||||
}
|
||||
|
||||
// DatabaseDSN returns the legacy database connection string
|
||||
func (c *Config) DatabaseDSN() string {
|
||||
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
|
||||
c.Database.Host, c.Database.Port, c.Database.Database,
|
||||
c.Database.User, c.Database.Password, c.Database.SSLMode)
|
||||
}
|
||||
|
||||
// NamedDatabaseDSN returns the connection string for a named database
|
||||
func (c *Config) NamedDatabaseDSN(name string) string {
|
||||
db, ok := c.Databases[name]
|
||||
if !ok {
|
||||
return c.DatabaseDSN()
|
||||
}
|
||||
dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
|
||||
db.Host, db.Port, db.Database,
|
||||
db.User, db.Password, db.SSLMode)
|
||||
if db.Schema != "" {
|
||||
dsn += fmt.Sprintf(" search_path=%s,public", db.Schema)
|
||||
}
|
||||
return dsn
|
||||
}
|
||||
|
||||
// HasDatabase returns true if a named database is configured
|
||||
func (c *Config) HasDatabase(name string) bool {
|
||||
_, ok := c.Databases[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// CardDAVDSN returns the CardDAV database connection string
|
||||
func (c *Config) CardDAVDSN() string {
|
||||
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
|
||||
c.CardDAV.Host, c.CardDAV.Port, c.CardDAV.Database,
|
||||
c.CardDAV.User, c.CardDAV.Password, c.CardDAV.SSLMode)
|
||||
}
|
||||
Reference in New Issue
Block a user