package config import ( "encoding/json" "errors" "fmt" "io/fs" "os" "github.com/dexidp/dex/connector/oidc" "github.com/kelseyhightower/envconfig" "go.uber.org/zap" ) const ( envConfigPrefix = "POLYCULECONNECT" DefaultConfigPath = "/etc/polyculeconnect.json" ) type ListeningMode string const ( ModeUnix ListeningMode = "unix" ModeNet ListeningMode = "net" ) type BackendConfigType string const ( Memory BackendConfigType = "memory" SQLite BackendConfigType = "sqlite" ) const ( defaultLogLevel = zap.InfoLevel defaultServerMode = ModeNet defaultServerHost = "0.0.0.0" defaultServerPort = 5000 defaultServerSocket = "" defaultServerStaticDir = "./" defaultIssuer = "http://localhost:5000" defaultStorageType = Memory defaultStorageFile = "./polyculeconnect.db" defaultStorageHost = "127.0.0.1" defaultStoragePort = 5432 defaultStorageDB = "polyculeconnect" defaultStorageUser = "polyculeconnect" defaultStoragePassword = "polyculeconnect" defaultStorageSSLMode = "disable" defaultStorageSSLCaFile = "" ) // Deprecated: remove when we finally drop the JSON config type BackendConfig struct { Config *oidc.Config `json:"config"` Name string `json:"name"` ID string `json:"ID"` Type BackendConfigType `json:"type"` Local bool `json:"local"` } type SSLStorageConfig struct { Mode string `json:"mode" envconfig:"STORAGE_SSL_MODE"` CaFile string `json:"ca_file" envconfig:"STORAGE_SSL_CA_FILE"` } type StorageConfig struct { File string `envconfig:"STORAGE_PATH"` Host string `envconfig:"STORAGE_HOST"` Port int `envconfig:"STORAGE_PORT"` Database string `envconfig:"STORAGE_DATABASE"` User string `envconfig:"STORAGE_USER"` Password string `envconfig:"STORAGE_PASSWORD"` SSL SSLStorageConfig } type logConfig struct { Level string `json:"level"` } type serverConfig struct { Mode string `json:"mode"` Host string `json:"host"` Port int `json:"port"` Sock string `json:"sock"` } type jsonStorageConfig struct { Type string `json:"type"` File string `json:"path"` Host string `json:"host" ` Port int `json:"port"` Database string `json:"database" ` User string `json:"user" ` Password string `json:"password" ` SSL SSLStorageConfig `json:"ssl"` } type jsonConf struct { LogConfig logConfig `json:"log"` ServerConfig serverConfig `json:"server"` StorageConfig jsonStorageConfig `json:"storage"` } func (c *jsonConf) initValues(ac *AppConfig) { c.LogConfig = logConfig{Level: ac.LogLevel.String()} c.ServerConfig = serverConfig{ Mode: string(ac.ServerMode), Host: ac.Host, Port: ac.Port, Sock: ac.SockPath, } c.StorageConfig = jsonStorageConfig{ Type: ac.StorageType, File: ac.StorageConfig.File, Host: ac.StorageConfig.Host, Port: ac.StorageConfig.Port, Database: ac.StorageConfig.Database, User: ac.StorageConfig.User, Password: ac.StorageConfig.Password, SSL: ac.StorageConfig.SSL, } } type AppConfig struct { LogLevel zap.AtomicLevel `envconfig:"LOG_LEVEL"` ServerMode ListeningMode `envconfig:"SERVER_MODE"` Host string `envconfig:"SERVER_HOST"` Port int `envconfig:"SERVER_PORT"` SockPath string `envconfig:"SERVER_SOCK"` StorageType string `envconfig:"STORAGE_TYPE"` StorageConfig *StorageConfig Issuer string StaticDir string } func defaultConfig() AppConfig { return AppConfig{ LogLevel: zap.NewAtomicLevelAt(defaultLogLevel), ServerMode: defaultServerMode, Host: defaultServerHost, Port: defaultServerPort, SockPath: defaultServerSocket, StorageType: string(defaultStorageType), StorageConfig: &StorageConfig{ File: defaultStorageFile, Host: defaultStorageHost, Port: defaultServerPort, Database: defaultStorageDB, User: defaultStorageUser, Password: defaultStoragePassword, SSL: SSLStorageConfig{ Mode: defaultStorageSSLMode, CaFile: defaultStorageSSLCaFile, }, }, Issuer: defaultIssuer, StaticDir: defaultServerStaticDir, } } func parseLevel(lvlStr string) zap.AtomicLevel { var res zap.AtomicLevel if err := res.UnmarshalText([]byte(lvlStr)); err != nil { return zap.NewAtomicLevelAt(zap.InfoLevel) } return res } func (ac *AppConfig) UnmarshalJSON(data []byte) error { var jsonConf jsonConf jsonConf.initValues(ac) if err := json.Unmarshal(data, &jsonConf); err != nil { return fmt.Errorf("failed to read JSON: %w", err) } ac.LogLevel = parseLevel(jsonConf.LogConfig.Level) ac.ServerMode = ListeningMode(jsonConf.ServerConfig.Mode) ac.Host = jsonConf.ServerConfig.Host ac.Port = jsonConf.ServerConfig.Port ac.SockPath = jsonConf.ServerConfig.Sock ac.StorageType = jsonConf.StorageConfig.Type ac.StorageConfig.File = jsonConf.StorageConfig.File ac.StorageConfig.Host = jsonConf.StorageConfig.Host ac.StorageConfig.Port = jsonConf.StorageConfig.Port ac.StorageConfig.Database = jsonConf.StorageConfig.Database ac.StorageConfig.User = jsonConf.StorageConfig.User ac.StorageConfig.Password = jsonConf.StorageConfig.Password ac.StorageConfig.SSL = jsonConf.StorageConfig.SSL return nil } func (ac *AppConfig) RedirectURI() string { return ac.Issuer + "/callback" } func New(filepath string) (*AppConfig, error) { conf := defaultConfig() content, err := os.ReadFile(filepath) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("failed to read config file %q: %w", filepath, err) } } else { if err := json.Unmarshal(content, &conf); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } } if err := envconfig.Process(envConfigPrefix, &conf); err != nil { return nil, err } if err := envconfig.Process(envConfigPrefix, conf.StorageConfig); err != nil { return nil, err } if err := envconfig.Process(envConfigPrefix, &conf.StorageConfig.SSL); err != nil { return nil, err } return &conf, nil }