From c9be039c09d1879235d4a8e9b17a665746b0d6e1 Mon Sep 17 00:00:00 2001 From: Antoine <208711+primalmotion@users.noreply.github.com> Date: Thu, 8 Oct 2020 11:04:16 -0700 Subject: [PATCH] new/lombric: added a way to read secrets from files (#77) * new/lombric: added a way to read secret from files * lint + better trim * fixed: comments * style * added option to delete file after reading it * fixed: lint --- lombric/lombric.go | 34 +++++++++++++++++++++++- lombric/lombric_test.go | 58 ++++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/lombric/lombric.go b/lombric/lombric.go index 01432ac..e4e07e5 100644 --- a/lombric/lombric.go +++ b/lombric/lombric.go @@ -12,7 +12,10 @@ package lombric import ( + "bytes" "fmt" + "io/ioutil" + "net/url" "os" "reflect" "strconv" @@ -96,12 +99,41 @@ func Initialize(conf Configurable) { fail() } + // Replace secret from content of files if needed. + if _, ok := conf.(EnvPrexixer); ok { + + for _, key := range secretFlags { + + value := viper.GetString(key) + + if strings.HasPrefix(value, "file://") { + + u, err := url.Parse(value) + if err != nil { + panic(fmt.Sprintf("invalid url for secret: %s", err)) + } + + data, err := ioutil.ReadFile(u.Path) + if err != nil { + panic(fmt.Sprintf("unable to read secret file for key '%s': %s", key, err)) + } + viper.Set(key, string(bytes.TrimSpace(data))) + + if u.Query().Get("delete") != "" { + if err := os.Remove(u.Path); err != nil { + panic(fmt.Sprintf("unable to delete secret file: %s", err)) + } + } + } + } + } + if err := viper.Unmarshal(conf); err != nil { panic("Unable to unmarshal configuration: " + err.Error()) } + // Clean up all secrets if p, ok := conf.(EnvPrexixer); ok { - // Clean up all secrets for _, key := range secretFlags { env := strings.Replace(strings.ToUpper(p.Prefix()+"_"+key), "-", "_", -1) if err := os.Unsetenv(env); err != nil { diff --git a/lombric/lombric_test.go b/lombric/lombric_test.go index 472601e..cc28869 100644 --- a/lombric/lombric_test.go +++ b/lombric/lombric_test.go @@ -12,6 +12,8 @@ package lombric import ( + "fmt" + "io/ioutil" "net" "os" "testing" @@ -26,30 +28,33 @@ func init() { } type testConf struct { - ABool bool `mapstructure:"a-bool" desc:"This is a boolean" required:"true" default:"true"` + ABool bool `mapstructure:"a-bool" desc:"This is a boolean" default:"true"` + ARequiredBool bool `mapstructure:"a-required-bool" desc:"This is a boolean" required:"true"` ABoolNoDef bool `mapstructure:"a-bool-nodef" desc:"This is a no def boolean" ` - ABoolSlice []bool `mapstructure:"a-bool-slice" desc:"This is a bool slice" required:"true" default:"true,false,true"` - ADuration time.Duration `mapstructure:"a-duration" desc:"This is a duration" required:"true" default:"10s"` + ABoolSlice []bool `mapstructure:"a-bool-slice" desc:"This is a bool slice" default:"true,false,true"` + ADuration time.Duration `mapstructure:"a-duration" desc:"This is a duration" default:"10s"` ADurationNoDef time.Duration `mapstructure:"a-duration-nodef" desc:"This is a no def duration" ` - AInteger int `mapstructure:"a-integer" desc:"This is a number" required:"true" default:"42"` + AInteger int `mapstructure:"a-integer" desc:"This is a number" default:"42"` AIntegerNoDef int `mapstructure:"a-integer-nodef" desc:"This is a no def number" ` - AIntSlice []int `mapstructure:"a-int-slice" desc:"This is a int slice" required:"true" default:"1,2,3"` + AIntSlice []int `mapstructure:"a-int-slice" desc:"This is a int slice" default:"1,2,3"` AnEnum string `mapstructure:"a-enum" desc:"This is an enum" allowed:"a,b,c" default:"a"` AnIPSlice []net.IP `mapstructure:"a-ip-slice" desc:"This is an ip slice" default:"127.0.0.1,192.168.100.1"` AnotherStringSliceNoDef []string `mapstructure:"a-string-slice-from-var" desc:"This is a no def string" ` ASecret string `mapstructure:"a-secret-from-var" desc:"This is a secret" secret:"true"` - AString string `mapstructure:"a-string" desc:"This is a string" required:"true" default:"hello"` + ASecretFromFile string `mapstructure:"a-secret-from-file" desc:"This is a secret from file" secret:"true"` + ASecretFromFileDelete string `mapstructure:"a-secret-from-file-del" desc:"This is a secret from file" secret:"true"` + AString string `mapstructure:"a-string" desc:"This is a string" default:"hello"` AStringNoDef string `mapstructure:"a-string-nodef" desc:"This is a no def string" ` - AStringSlice []string `mapstructure:"a-string-slice" desc:"This is a string slice" required:"true" default:"a,b,c"` + AStringSlice []string `mapstructure:"a-string-slice" desc:"This is a string slice" default:"a,b,c"` AStringSliceNoDef []string `mapstructure:"a-string-slice-nodef" desc:"This is a no def string slice"` embedTestConf `mapstructure:",squash" override:"embedded-string-a=outter1,embedded-ignored-string=-"` } type embedTestConf struct { - EmbeddedStringA string `mapstructure:"embedded-string-a" desc:"This is a string" required:"true" default:"inner1"` - EmbeddedStringB string `mapstructure:"embedded-string-b" desc:"This is a string" required:"true" default:"inner2"` - EmbeddedIgnoredStringB string `mapstructure:"embedded-ignored-string" desc:"This is a string" required:"true" default:"inner3"` + EmbeddedStringA string `mapstructure:"embedded-string-a" desc:"This is a string" default:"inner1"` + EmbeddedStringB string `mapstructure:"embedded-string-b" desc:"This is a string" default:"inner2"` + EmbeddedIgnoredStringB string `mapstructure:"embedded-ignored-string" desc:"This is a string" default:"inner3"` } // Prefix return the configuration prefix. @@ -60,15 +65,39 @@ func TestLombric_Initialize(t *testing.T) { Convey("Given have a conf", t, func() { + sfile1, err := ioutil.TempFile(os.TempDir(), "secret") + if err != nil { + panic(err) + } + defer sfile1.Close() // nolint + if _, err := sfile1.WriteString("this-is-super=s3cr3t\n\n"); err != nil { + panic(err) + } + spath1 := fmt.Sprintf("file://%s", sfile1.Name()) + + sfile2, err := ioutil.TempFile(os.TempDir(), "secret2") + if err != nil { + panic(err) + } + defer sfile2.Close() // nolint + if _, err := sfile2.WriteString("wow\n\n"); err != nil { + panic(err) + } + spath2 := fmt.Sprintf("file://%s?delete=true", sfile2.Name()) + conf := &testConf{} os.Setenv("LOMBRIC_A_STRING_SLICE_FROM_VAR", "x y z") // nolint: errcheck + os.Setenv("LOMBRIC_A_REQUIRED_BOOL", "true") // nolint: errcheck os.Setenv("LOMBRIC_A_SECRET_FROM_VAR", "secret") // nolint: errcheck + os.Setenv("LOMBRIC_A_SECRET_FROM_FILE", spath1) // nolint: errcheck + os.Setenv("LOMBRIC_A_SECRET_FROM_FILE_DEL", spath2) // nolint: errcheck Initialize(conf) Convey("Then the flags should be correctly set", func() { So(conf.ABool, ShouldEqual, true) + So(conf.ARequiredBool, ShouldEqual, true) So(conf.ABoolNoDef, ShouldEqual, false) So(conf.ABoolSlice, ShouldResemble, []bool{true, false, true}) So(conf.ADuration, ShouldEqual, 10*time.Second) @@ -79,6 +108,9 @@ func TestLombric_Initialize(t *testing.T) { So(conf.AnIPSlice, ShouldResemble, []net.IP{net.IPv4(127, 0, 0, 1), net.IPv4(192, 168, 100, 1)}) So(conf.AnotherStringSliceNoDef, ShouldResemble, []string{"x", "y", "z"}) So(conf.ASecret, ShouldEqual, "secret") + So(conf.ASecretFromFile, ShouldEqual, "this-is-super=s3cr3t") + So(conf.ASecretFromFileDelete, ShouldEqual, "wow") + So(viper.GetString("a-secret-from-file"), ShouldEqual, "this-is-super=s3cr3t") So(conf.AString, ShouldEqual, "hello") So(conf.AStringNoDef, ShouldEqual, "") So(conf.AStringSlice, ShouldResemble, []string{"a", "b", "c"}) @@ -88,6 +120,12 @@ func TestLombric_Initialize(t *testing.T) { So(conf.EmbeddedStringB, ShouldEqual, "inner2") So(os.Getenv("LOMBRIC_A_SECRET_FROM_VAR"), ShouldEqual, "") So(viper.AllKeys(), ShouldNotContain, "embedded-ignored-string") + + _, err := os.Stat(sfile1.Name()) + So(os.IsNotExist(err), ShouldBeFalse) + + _, err = os.Stat(sfile2.Name()) + So(os.IsNotExist(err), ShouldBeTrue) }) }) }