From cfa104858d7e84e7f42d5c3f5165e7976e1b63ff Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 6 Jun 2022 20:37:45 +0200 Subject: [PATCH] FEATURE: --creds can refer to a command line to execute in addition to a file or script (#1521) * Allow commands for creds file & refactoring * Minor fix * Add shell command ability to docs Signed-off-by: Jan-Philipp Benecke Co-authored-by: Tom Limoncelli --- docs/creds-json.md | 23 ++++++++++++++++--- go.mod | 2 ++ go.sum | 2 ++ pkg/credsfile/providerConfig.go | 40 +++++++++++++++++---------------- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/docs/creds-json.md b/docs/creds-json.md index 47f96a4fa..a7e759e48 100644 --- a/docs/creds-json.md +++ b/docs/creds-json.md @@ -150,7 +150,7 @@ The fix is to change one to match the other. Message: `ERROR: creds.json entry ... is missing ...: ...` -However no `TYPE` subkey was found in an entry in `creds.json`. +However no `TYPE` subkey was found in an entry in `creds.json`. In 3.16 forward, it is required if new-style `NewRegistrar()` or `NewDnsProvider()` was used. In 4.0 this is required. @@ -175,12 +175,29 @@ The `--creds` flag allows you to specify a different file name. * Normally the file is read as a JSON file. * Do not end the filename with `.yaml` or `.yml` as some day we hope to support YAML. -* Rather than specifying a file, you can specify a program to be run. The output of the program must be valid JSON and will be read the same way. - * If the name begins with `!`, the remainder of the name is taken to be the command to be run. +* Rather than specifying a file, you can specify a program or shell command to be run. The output of the program/command must be valid JSON and will be read the same way. + * If the name begins with `!`, the remainder of the name is taken to be a shell command or program to be run. * If the name is a file that is executable (chmod `+x` bit), it is taken as the command to be run. * Exceptions: The `x` bit is not checked if the filename ends with `.yaml`, `.yml` or `.json`. * Windows: Executing an external script isn't supported. There's no code that prevents it from trying, but it isn't supported. +### Example commands + +Following commands would execute a program/script: +``` bash +dnscontrol preview --creds !./creds.sh +dnscontrol preview --creds ./creds.sh +dnscontrol preview --creds creds.sh +dnscontrol preview --creds !creds.sh +dnscontrol preview --creds !/some/absolute/path/creds.sh +dnscontrol preview --creds /some/absolute/path/creds.sh +``` + +Following commands would execute a shell command: +``` bash +dnscontrol preview --creds "!op inject -i creds.json.tpl" +``` + ## Don't store secrets in a Git repo! Do NOT store secrets in a Git repository. That is not secure. For example, diff --git a/go.mod b/go.mod index 747025135..fabfb2746 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + require ( cloud.google.com/go/compute v1.6.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect diff --git a/go.sum b/go.sum index eba555c0b..0ede5f639 100644 --- a/go.sum +++ b/go.sum @@ -320,6 +320,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/pkg/credsfile/providerConfig.go b/pkg/credsfile/providerConfig.go index f873ec459..6ea7a6c8a 100644 --- a/pkg/credsfile/providerConfig.go +++ b/pkg/credsfile/providerConfig.go @@ -6,6 +6,7 @@ package credsfile import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -14,6 +15,7 @@ import ( "github.com/DisposaBoy/JsonConfigReader" "github.com/TomOnTime/utfutil" + "github.com/google/shlex" ) func quotedList(l []string) string { @@ -39,24 +41,14 @@ func LoadProviderConfigs(fname string) (map[string]map[string]string, error) { var dat []byte var err error + filesIsExecutable := strings.HasPrefix(fname, "!") || isExecutable(fname) - if strings.HasPrefix(fname, "!") { + if filesIsExecutable && !strings.HasSuffix(fname, ".json") { + // file is executable and is not a .json (needed because in Windows WSL all files are executable). dat, err = executeCredsFile(strings.TrimPrefix(fname, "!")) if err != nil { return nil, err } - } else if strings.HasSuffix(fname, ".json") { - // .json files are never executable (needed because in Windows WSL - // all files are executable). - dat, err = readCredsFile(fname) - if err != nil { - return nil, err - } - } else if isExecutable(fname) { - dat, err = executeCredsFile(fname) - if err != nil { - return nil, err - } } else { // no executable bit found nor marked as executable so read it in dat, err = readCredsFile(fname) @@ -111,13 +103,23 @@ func readCredsFile(filename string) ([]byte, error) { } func executeCredsFile(filename string) ([]byte, error) { - cmd := filename - if !strings.HasPrefix(filename, "/") { - // if the path doesn't start with `/` make sure we aren't relying on $PATH. - cmd = strings.Join([]string{".", filename}, string(filepath.Separator)) + cmd, err := shlex.Split(filename) + if err != nil { + return nil, err } - out, err := exec.Command(cmd).Output() - return out, err + command := cmd[0] + + // check if this is a file and not a command when there is no leading / + if fileExists(command) && !strings.HasPrefix(command, "/") { + command = strings.Join([]string{".", command}, string(filepath.Separator)) + } + + return exec.Command(command, cmd[1:]...).Output() +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return !errors.Is(err, os.ErrNotExist) } func replaceEnvVars(m map[string]map[string]string) error {