diff --git a/README.md b/README.md index 8dc642a..b284521 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,11 @@ You can use **environment variables** instead. For example: export BEACON_EMAIL_SMTP_PASSWORD="your-password" ``` +For password, you can instead provide a file containing the password. +```sh +export BEACON_EMAIL_SMTP_PASSWORD_FILE="/path/to/password-file" +``` + ### Configuration sources Beacon supports multiple configuration sources, with the following priority (highest to lowest): diff --git a/conf/config.go b/conf/config.go index d6ff443..1ad48eb 100644 --- a/conf/config.go +++ b/conf/config.go @@ -8,7 +8,7 @@ import ( "log" "os" "path/filepath" - "reflect" + "strings" "time" "github.com/caarlos0/env/v11" @@ -19,7 +19,8 @@ import ( const ENV_VAR_PREFIX = "BEACON_" type Secret struct { - value string + Value string `env:""` + FromFile string `env:"_FILE,file"` } func (s Secret) String() string { @@ -31,18 +32,23 @@ func (s Secret) GoString() string { } func (s *Secret) Get() string { - return s.value + if s.FromFile != "" { + // File contents tend to end with newline + return strings.TrimSpace(s.FromFile) + } else { + return s.Value + } } func (s *Secret) IsSet() bool { - return s.value != "" + return s.Value != "" || s.FromFile != "" } type EmailConfig struct { SmtpServer string `yaml:"smtp_server" env:"SMTP_SERVER"` SmtpPort int `yaml:"smtp_port" env:"SMTP_PORT"` SmtpUsername string `yaml:"smtp_username" env:"SMTP_USERNAME"` - SmtpPassword Secret `yaml:"smtp_password" env:"SMTP_PASSWORD"` + SmtpPassword Secret `yaml:"smtp_password" envPrefix:"SMTP_PASSWORD"` SendTo string `yaml:"send_to" env:"SEND_TO"` Sender string `yaml:"sender" env:"SENDER"` Prefix string `yaml:"prefix" env:"PREFIX"` @@ -66,10 +72,14 @@ func (s *Secret) UnmarshalYAML(node *yaml.Node) error { if node.Kind != yaml.ScalarNode { return fmt.Errorf("expected scalar node got %s", node.Value) } - s.value = node.Value + s.Value = node.Value return nil } +func (s Secret) MarshalYAML() (interface{}, error) { + return "*****", nil +} + // TzLocation wraps a *time.Location so we can provide custom YAML unmarshalling. type TzLocation struct { Location *time.Location @@ -86,7 +96,7 @@ type Config struct { Port int `yaml:"port" env:"PORT"` SchedulerPeriod time.Duration `yaml:"scheduler_period" env:"SCHEDULER_PERIOD"` - EmailConf EmailConfig `yaml:"email" envPrefix:"EMAIL_"` + EmailConf EmailConfig `yaml:"email" envPrefix:"EMAIL_"` Services ServicesList @@ -216,11 +226,6 @@ func ConfigFromBytes(data []byte) (*Config, error) { } err = env.ParseWithOptions(config, env.Options{ Prefix: ENV_VAR_PREFIX, - FuncMap: map[reflect.Type]env.ParserFunc{ - reflect.TypeOf(Secret{}): func(v string) (interface{}, error) { - return Secret{v}, nil - }, - }, }) if err != nil { return nil, err diff --git a/conf/config_test.go b/conf/config_test.go index e6d6725..2e83c29 100644 --- a/conf/config_test.go +++ b/conf/config_test.go @@ -102,7 +102,7 @@ services: } func TestSecretPrint(t *testing.T) { - secret := Secret{"Greg"} + secret := Secret{"Greg", ""} assert.Equal(t, secret.Get(), "Greg") assert.NotContains(t, fmt.Sprint(secret), "Greg") assert.NotContains(t, fmt.Sprint(&secret), "Greg") @@ -143,7 +143,7 @@ prefix: "[test]" "mail.smtp2go.com", 587, "beacon", - Secret{"h4xor"}, + Secret{"h4xor", ""}, "you@example.fake", "noreply@example.fake", "[test]", @@ -160,3 +160,27 @@ func TestSecretFromEnv(t *testing.T) { require.Equal(t, "secr4t", conf.EmailConf.SmtpPassword.Get()) } + +func TestSecretFromFile(t *testing.T) { + // Create the file + f, err := os.Create("secret-test.txt") + require.NoError(t, err) + _, err = f.WriteString("foo\n") + require.NoError(t, err) + + // Set both, file should have prio + err = os.Setenv("BEACON_EMAIL_SMTP_PASSWORD", "bar") + require.NoError(t, err) + err = os.Setenv("BEACON_EMAIL_SMTP_PASSWORD_FILE", "secret-test.txt") + require.NoError(t, err) + + conf, err := ConfigFromBytes([]byte("")) + require.NoError(t, err) + require.Equal(t, "foo", conf.EmailConf.SmtpPassword.Get()) + + // Cleanup + err = os.Unsetenv("BEACON_EMAIL_SMTP_PASSWORD_FILE") + require.NoError(t, err) + err = os.Remove("secret-test.txt") + require.NoError(t, err) +}