From b12944d8a7b043e939f6d6e5bb25715f748130cb Mon Sep 17 00:00:00 2001 From: Jefferson Girao Date: Tue, 25 Jul 2017 09:08:15 +0200 Subject: [PATCH] Loading vault url from kubernetes vault auth file --- auth_approle.go | 16 +-- auth_token.go | 14 +-- auth_userpass.go | 17 ++- config.go | 47 +++++-- config_test.go | 77 ++++++++++++ tests/approle_auth_file.json | 1 + tests/approle_auth_file.yml | 3 + tests/auth_file.json | 1 + tests/invalid_kubernetes_vault_auth_file.json | 1 + tests/kubernetes_vault_auth_file.json | 1 + tests/token_auth_file.json | 1 + tests/token_auth_file.yml | 2 + tests/userpass_auth_file.json | 1 + tests/userpass_auth_file.yml | 3 + utils.go | 36 +++--- utils_test.go | 115 ++++++++++++++++++ vault.go | 13 +- 17 files changed, 286 insertions(+), 63 deletions(-) create mode 100644 config_test.go create mode 100644 tests/approle_auth_file.json create mode 100644 tests/approle_auth_file.yml create mode 100644 tests/auth_file.json create mode 100644 tests/invalid_kubernetes_vault_auth_file.json create mode 100644 tests/kubernetes_vault_auth_file.json create mode 100644 tests/token_auth_file.json create mode 100644 tests/token_auth_file.yml create mode 100644 tests/userpass_auth_file.json create mode 100644 tests/userpass_auth_file.yml create mode 100644 utils_test.go diff --git a/auth_approle.go b/auth_approle.go index 96698be..5628663 100644 --- a/auth_approle.go +++ b/auth_approle.go @@ -40,21 +40,17 @@ func NewAppRolePlugin(client *api.Client) AuthInterface { } // Create a approle plugin with the secret id and role id provided in the file -func (r authAppRolePlugin) Create(cfg map[string]string) (string, error) { - // step: extract the options - roleID, _ := cfg["role_id"] - secretID, _ := cfg["secret_id"] - - if roleID == "" { - roleID = os.Getenv("VAULT_SIDEKICK_ROLE_ID") +func (r authAppRolePlugin) Create(cfg *vaultAuthOptions) (string, error) { + if cfg.RoleID == "" { + cfg.RoleID = os.Getenv("VAULT_SIDEKICK_ROLE_ID") } - if secretID == "" { - secretID = os.Getenv("VAULT_SIDEKICK_SECRET_ID") + if cfg.SecretID == "" { + cfg.SecretID = os.Getenv("VAULT_SIDEKICK_SECRET_ID") } // step: create the token request request := r.client.NewRequest("POST", "/v1/auth/approle/login") - login := appRoleLogin{SecretID: secretID, RoleID: roleID} + login := appRoleLogin{SecretID: cfg.SecretID, RoleID: cfg.RoleID} if err := request.SetJSONBody(login); err != nil { return "", err } diff --git a/auth_token.go b/auth_token.go index ae3e0eb..d5dcfb5 100644 --- a/auth_token.go +++ b/auth_token.go @@ -37,18 +37,16 @@ func NewUserTokenPlugin(client *api.Client) AuthInterface { } // Create retrieves the token from an environment variable or file -func (r authTokenPlugin) Create(cfg map[string]string) (string, error) { - filename, _ := cfg["filename"] - fileFormat, _ := cfg["fileFormat"] - if filename != "" { - content, err := readConfigFile(filename, fileFormat) +func (r authTokenPlugin) Create(cfg *vaultAuthOptions) (string, error) { + if cfg.FileName != "" { + content, err := readConfigFile(cfg.FileName, cfg.FileFormat) if err != nil { return "", err } // check: ensure we have a token in the file - token, found := content["token"] - if !found { - return "", fmt.Errorf("the auth file: %s does not contain a token", filename) + token := content.Token + if token == "" { + return "", fmt.Errorf("the auth file: %s does not contain a token", cfg.FileName) } return token, nil diff --git a/auth_userpass.go b/auth_userpass.go index 4aa64fa..0609a5f 100644 --- a/auth_userpass.go +++ b/auth_userpass.go @@ -41,21 +41,18 @@ func NewUserPassPlugin(client *api.Client) AuthInterface { } // Create a userpass plugin with the username and password provide in the file -func (r authUserPassPlugin) Create(cfg map[string]string) (string, error) { +func (r authUserPassPlugin) Create(cfg *vaultAuthOptions) (string, error) { // step: extract the options - username, _ := cfg["username"] - password, _ := cfg["password"] - - if username == "" { - username = os.Getenv("VAULT_SIDEKICK_USERNAME") + if cfg.Username == "" { + cfg.Username = os.Getenv("VAULT_SIDEKICK_USERNAME") } - if password == "" { - password = os.Getenv("VAULT_SIDEKICK_PASSWORD") + if cfg.Password == "" { + cfg.Password = os.Getenv("VAULT_SIDEKICK_PASSWORD") } // step: create the token request - request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", username)) - if err := request.SetJSONBody(userPassLogin{Password: password}); err != nil { + request := r.client.NewRequest("POST", fmt.Sprintf("/v1/auth/userpass/login/%s", cfg.Username)) + if err := request.SetJSONBody(userPassLogin{Password: cfg.Password}); err != nil { return "", err } // step: make the request diff --git a/config.go b/config.go index e801102..c4d4ff0 100644 --- a/config.go +++ b/config.go @@ -20,9 +20,25 @@ import ( "flag" "fmt" "net/url" + "os" "time" ) +type vaultAuthOptions struct { + ClientToken string + Token string + LeaseDuration int + Renewable bool + Method string + VaultURL string `json:"vaultAddr"` + RoleID string `json:"role_id" yaml:"role_id"` + SecretID string `json:"secret_id" yaml:"secret_id"` + FileName string + FileFormat string + Username string + Password string +} + type config struct { // the url for th vault server vaultURL string @@ -31,7 +47,7 @@ type config struct { // whether or not the auth file format is default vaultAuthFileFormat string // the authentication options - vaultAuthOptions map[string]string + vaultAuthOptions *vaultAuthOptions // the vault ca file vaultCaFile string // the place to write the resources @@ -59,7 +75,9 @@ var ( func init() { // step: setup some defaults options.resources = new(VaultResources) - options.vaultAuthOptions = map[string]string{VaultAuth: "token"} + options.vaultAuthOptions = &vaultAuthOptions{ + Method: "token", + } flag.StringVar(&options.vaultURL, "vault", getEnv("VAULT_ADDR", "https://127.0.0.1:8200"), "url the vault service or VAULT_ADDR") flag.StringVar(&options.vaultAuthFile, "auth", getEnv("AUTH_FILE", ""), "a configuration file in json or yaml containing authentication arguments") @@ -83,20 +101,33 @@ func parseOptions() error { // validateOptions parses and validates the command line options func validateOptions(cfg *config) (err error) { - // step: validate the vault url - if _, err = url.Parse(cfg.vaultURL); err != nil { - return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL) - } - // step: read in the token if required + if cfg.vaultAuthFile != "" { if exists, _ := fileExists(cfg.vaultAuthFile); !exists { return fmt.Errorf("the token file: %s does not exists, please check", cfg.vaultAuthFile) } - options.vaultAuthOptions, err = readConfigFile(options.vaultAuthFile, options.vaultAuthFileFormat) + + cfg.vaultAuthOptions, err = readConfigFile(cfg.vaultAuthFile, cfg.vaultAuthFileFormat) if err != nil { return fmt.Errorf("unable to read in authentication options from: %s, error: %s", cfg.vaultAuthFile, err) } + if cfg.vaultAuthOptions.VaultURL != "" { + cfg.vaultURL = cfg.vaultAuthOptions.VaultURL + } + } + + if cfg.vaultURL == "" { + cfg.vaultURL = os.Getenv("VAULT_ADDR") + } + + if cfg.vaultURL == "" { + return fmt.Errorf("VAULT_ADDR is unset") + } + + // step: validate the vault url + if _, err = url.Parse(cfg.vaultURL); err != nil { + return fmt.Errorf("invalid vault url: '%s' specified", cfg.vaultURL) } if cfg.vaultCaFile != "" { diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..5c3d69c --- /dev/null +++ b/config_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "os" + "testing" +) + +func TestValidateOptionsWithoutVaultURL(t *testing.T) { + os.Setenv("VAULT_ADDR", "") + + cfg := &config{} + err := validateOptions(cfg) + + if err == nil { + t.Errorf("should have raised error: %v", err) + } + +} + +func TestValidateOptionsWithEnvFallback(t *testing.T) { + os.Setenv("VAULT_ADDR", "http://testurl:8080") + + cfg := &config{} + err := validateOptions(cfg) + + if err != nil { + t.Errorf("raised an error: %v", err) + } + + actual := cfg.vaultURL + expected := "http://testurl:8080" + + if actual != expected { + t.Errorf("Expected Vault URL to be %s got %s", expected, actual) + } + +} + +func TestValidateOptionsWithInvalidVaultURL(t *testing.T) { + cfg := &config{ + vaultURL: "%invalid_url", + } + err := validateOptions(cfg) + + if err == nil { + t.Errorf("should have raised error") + } +} + +func TestValidateOptionsWithInvalidVaultURLFromAuthFile(t *testing.T) { + cfg := &config{ + vaultAuthFile: "tests/invalid_kubernetes_vault_auth_file.json", + } + err := validateOptions(cfg) + + if err == nil { + t.Errorf("should have raised error") + } +} + +func TestValidateOptionsWithVaultURLFromAuthFile(t *testing.T) { + cfg := &config{ + vaultAuthFile: "tests/kubernetes_vault_auth_file.json", + } + err := validateOptions(cfg) + + if err != nil { + t.Errorf("raising an error %v", err) + } + + actual := cfg.vaultURL + expected := "http://testurl:8080" + + if actual != expected { + t.Errorf("Expected Vault URL to be %s got %s", expected, actual) + } +} diff --git a/tests/approle_auth_file.json b/tests/approle_auth_file.json new file mode 100644 index 0000000..5a8adf5 --- /dev/null +++ b/tests/approle_auth_file.json @@ -0,0 +1 @@ +{"method": "approle", "role_id": "admin", "secret_id": "foobar"} diff --git a/tests/approle_auth_file.yml b/tests/approle_auth_file.yml new file mode 100644 index 0000000..3151970 --- /dev/null +++ b/tests/approle_auth_file.yml @@ -0,0 +1,3 @@ +method: approle +role_id: admin +secret_id: foobar diff --git a/tests/auth_file.json b/tests/auth_file.json new file mode 100644 index 0000000..3849e40 --- /dev/null +++ b/tests/auth_file.json @@ -0,0 +1 @@ +{"method": "userpass", "username": "admin", "password": "foobar"} diff --git a/tests/invalid_kubernetes_vault_auth_file.json b/tests/invalid_kubernetes_vault_auth_file.json new file mode 100644 index 0000000..d9b30c6 --- /dev/null +++ b/tests/invalid_kubernetes_vault_auth_file.json @@ -0,0 +1 @@ +{"renewable": true, "leaseDuration": 60, "vaultAddr": "%invalid_url", "token": "foobar"} diff --git a/tests/kubernetes_vault_auth_file.json b/tests/kubernetes_vault_auth_file.json new file mode 100644 index 0000000..8d59ec4 --- /dev/null +++ b/tests/kubernetes_vault_auth_file.json @@ -0,0 +1 @@ +{"renewable": true, "leaseDuration": 60, "vaultAddr": "http://testurl:8080", "token": "foobar"} diff --git a/tests/token_auth_file.json b/tests/token_auth_file.json new file mode 100644 index 0000000..23a5fd3 --- /dev/null +++ b/tests/token_auth_file.json @@ -0,0 +1 @@ +{"method": "token", "token": "foobar"} diff --git a/tests/token_auth_file.yml b/tests/token_auth_file.yml new file mode 100644 index 0000000..b1c876e --- /dev/null +++ b/tests/token_auth_file.yml @@ -0,0 +1,2 @@ +method: token +token: foobar diff --git a/tests/userpass_auth_file.json b/tests/userpass_auth_file.json new file mode 100644 index 0000000..3849e40 --- /dev/null +++ b/tests/userpass_auth_file.json @@ -0,0 +1 @@ +{"method": "userpass", "username": "admin", "password": "foobar"} diff --git a/tests/userpass_auth_file.yml b/tests/userpass_auth_file.yml new file mode 100644 index 0000000..3b7b983 --- /dev/null +++ b/tests/userpass_auth_file.yml @@ -0,0 +1,3 @@ +method: userpass +username: admin +password: foobar diff --git a/utils.go b/utils.go index cf8f553..3b57c1e 100644 --- a/utils.go +++ b/utils.go @@ -70,7 +70,7 @@ func getKeys(data map[string]interface{}) []string { // readConfigFile read in a configuration file // filename : the path to the file -func readConfigFile(filename, fileFormat string) (map[string]string, error) { +func readConfigFile(filename, fileFormat string) (*vaultAuthOptions, error) { // step: check the file exists if exists, err := fileExists(filename); !exists { return nil, fmt.Errorf("the file: %s does not exist", filename) @@ -91,44 +91,44 @@ func readConfigFile(filename, fileFormat string) (map[string]string, error) { // readJsonFile read in and unmarshall the data into a map // filename : the path to the file container the json data -func readJSONFile(filename, format string) (map[string]string, error) { - data := make(map[string]string, 0) +func readJSONFile(filename, format string) (*vaultAuthOptions, error) { + opts := &vaultAuthOptions{} content, err := ioutil.ReadFile(filename) if err != nil { - return data, err + return nil, err } // unmarshall the data - err = json.Unmarshal(content, &data) + err = json.Unmarshal(content, &opts) if err != nil && format == "default" { - return data, err + return nil, err } if err != nil && format == "kubernetes-vault" { - if data["clientToken"] != "" { - data[VaultAuth] = "token" - data["token"] = data["clientToken"] - return data, nil + if opts.ClientToken != "" { + opts.Method = "token" + opts.Token = opts.ClientToken + return opts, nil } - return data, err + return nil, err } - return data, nil + return opts, nil } // readYAMLFile read in and unmarshall the data into a map // filename : the path to the file container the yaml data -func readYAMLFile(filename string) (map[string]string, error) { - data := make(map[string]string, 0) +func readYAMLFile(filename string) (*vaultAuthOptions, error) { + o := &vaultAuthOptions{} content, err := ioutil.ReadFile(filename) if err != nil { - return data, err + return nil, err } - err = yaml.Unmarshal(content, data) + err = yaml.Unmarshal(content, o) if err != nil { - return data, err + return nil, err } - return data, nil + return o, nil } // getDurationWithin generate a random integer between min and max diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..1127eed --- /dev/null +++ b/utils_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "testing" +) + +func TestReadConfigFileKubernetesVault(t *testing.T) { + o, err := readConfigFile("tests/kubernetes_vault_auth_file.json", "kubernetes-vault") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + tokenExpected := "foobar" + + if o.Token != tokenExpected { + t.Errorf("Expected user %s got %s", tokenExpected, o.Token) + } +} + +func TestReadConfigUserPassJSON(t *testing.T) { + o, err := readConfigFile("tests/userpass_auth_file.json", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + userExpected := "admin" + passwordExpected := "foobar" + + if o.Username != userExpected { + t.Errorf("Expected user %s got %s", userExpected, o.Username) + } + + if o.Password != passwordExpected { + t.Errorf("Expected user %s got %s", passwordExpected, o.Password) + } +} + +func TestReadConfigUserPassYAML(t *testing.T) { + o, err := readConfigFile("tests/userpass_auth_file.yml", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + userExpected := "admin" + passwordExpected := "foobar" + + if o.Username != userExpected { + t.Errorf("Expected user %s got %s", userExpected, o.Username) + } + + if o.Password != passwordExpected { + t.Errorf("Expected user %s got %s", passwordExpected, o.Password) + } +} + +func TestReadConfigAppRoleJSON(t *testing.T) { + o, err := readConfigFile("tests/approle_auth_file.json", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + roleIDExpected := "admin" + secretIDExpected := "foobar" + + if o.RoleID != roleIDExpected { + t.Errorf("Expected roleID %s got %s", roleIDExpected, o.RoleID) + } + + if o.SecretID != secretIDExpected { + t.Errorf("Expected secretID %s got %s", secretIDExpected, o.SecretID) + } +} + +func TestReadConfigAppRoleYAML(t *testing.T) { + o, err := readConfigFile("tests/approle_auth_file.yml", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + roleIDExpected := "admin" + secretIDExpected := "foobar" + + if o.RoleID != roleIDExpected { + t.Errorf("Expected roleID %s got %s", roleIDExpected, o.RoleID) + } + + if o.SecretID != secretIDExpected { + t.Errorf("Expected secretID %s got %s", secretIDExpected, o.SecretID) + } +} +func TestReadConfigTokenJSON(t *testing.T) { + o, err := readConfigFile("tests/token_auth_file.json", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + expected := "foobar" + + if o.Token != expected { + t.Errorf("Expected token %s got %s", expected, o.Token) + } +} + +func TestReadConfigTokenYAML(t *testing.T) { + o, err := readConfigFile("tests/token_auth_file.yml", "default") + if err != nil { + t.Errorf("raising an error: %v", err) + } + + expected := "foobar" + + if o.Token != expected { + t.Errorf("Expected token %s got %s", expected, o.Token) + } +} diff --git a/vault.go b/vault.go index a72e3aa..e6e3ee0 100644 --- a/vault.go +++ b/vault.go @@ -31,15 +31,10 @@ import ( "github.com/hashicorp/vault/api" ) -const ( - // VaultAuth the method to use when authenticating to vault - VaultAuth = "method" -) - // AuthInterface is the authentication interface type AuthInterface interface { // Create and handle renewals of the token - Create(map[string]string) (string, error) + Create(*vaultAuthOptions) (string, error) } // VaultService is the main interface into the vault API - placing into a structure @@ -425,15 +420,15 @@ func newVaultClient(opts *config) (*api.Client, error) { return nil, err } - plugin, _ := opts.vaultAuthOptions[VaultAuth] + plugin := opts.vaultAuthOptions.Method switch plugin { case "userpass": token, err = NewUserPassPlugin(client).Create(opts.vaultAuthOptions) case "approle": token, err = NewAppRolePlugin(client).Create(opts.vaultAuthOptions) case "token": - opts.vaultAuthOptions["filename"] = options.vaultAuthFile - opts.vaultAuthOptions["fileFormat"] = options.vaultAuthFileFormat + opts.vaultAuthOptions.FileName = options.vaultAuthFile + opts.vaultAuthOptions.FileFormat = options.vaultAuthFileFormat token, err = NewUserTokenPlugin(client).Create(opts.vaultAuthOptions) default: return nil, fmt.Errorf("unsupported authentication plugin: %s", plugin)