Merge pull request #46 from jeffersongirao/vault-url-from-kube-auth-file

Loading vault url from kubernetes vault auth file, exit if vault url is not set.
This commit is contained in:
Rohith Jayawardene 2017-08-15 20:31:42 +01:00 committed by GitHub
commit 12208bcad4
17 changed files with 286 additions and 63 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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 != "" {

77
config_test.go Normal file
View file

@ -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)
}
}

View file

@ -0,0 +1 @@
{"method": "approle", "role_id": "admin", "secret_id": "foobar"}

View file

@ -0,0 +1,3 @@
method: approle
role_id: admin
secret_id: foobar

1
tests/auth_file.json Normal file
View file

@ -0,0 +1 @@
{"method": "userpass", "username": "admin", "password": "foobar"}

View file

@ -0,0 +1 @@
{"renewable": true, "leaseDuration": 60, "vaultAddr": "%invalid_url", "token": "foobar"}

View file

@ -0,0 +1 @@
{"renewable": true, "leaseDuration": 60, "vaultAddr": "http://testurl:8080", "token": "foobar"}

View file

@ -0,0 +1 @@
{"method": "token", "token": "foobar"}

View file

@ -0,0 +1,2 @@
method: token
token: foobar

View file

@ -0,0 +1 @@
{"method": "userpass", "username": "admin", "password": "foobar"}

View file

@ -0,0 +1,3 @@
method: userpass
username: admin
password: foobar

View file

@ -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

115
utils_test.go Normal file
View file

@ -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)
}
}

View file

@ -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
@ -464,15 +459,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)