Skip to content

Commit

Permalink
config: Read token from other sources
Browse files Browse the repository at this point in the history
Currently the gitlab personal access token is stored in cleartext in
the config file.  Allowing for users to specify a command to fetch the
token from a password manager or other encryption utility provides
additional security.

Add a "load_token" config entry to allow users to specify another utility
to obtain the token.

Suggested-by: Thomas Furfaro <tom@furfmon.com>
Signed-off-by: Prarit Bhargava <prarit@redhat.com>
  • Loading branch information
prarit committed Sep 9, 2020
1 parent f8306df commit 6298774
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 22 deletions.
73 changes: 57 additions & 16 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strings"
"syscall"
Expand All @@ -27,9 +28,9 @@ const defaultGitLabHost = "https://gitlab.com"
// them to the provided confpath (default: ~/.config/lab.hcl)
func New(confpath string, r io.Reader) error {
var (
reader = bufio.NewReader(r)
host, token string
err error
reader = bufio.NewReader(r)
host, token, loadToken string
err error
)
// If core host is set in the environment (LAB_CORE_HOST) we only want
// to prompt for the token. We'll use the environments host and place
Expand All @@ -50,33 +51,51 @@ func New(confpath string, r io.Reader) error {
host = viper.GetString("core.host")
}

tokenURL, err := url.Parse(host)
if err != nil {
return err
}
tokenURL.Path = "profile/personal_access_tokens"
viper.Set("core.host", host)

fmt.Printf("Create a token here: %s\nEnter default GitLab token (scope: api): ", tokenURL.String())
token, err = readPassword()
token, loadToken, err = readPassword(*reader)
if err != nil {
return err
}
if token != "" {
viper.Set("core.token", token)
} else if loadToken != "" {
viper.Set("core.load_token", loadToken)
}

viper.Set("core.host", host)
viper.Set("core.token", token)
if err := viper.WriteConfigAs(confpath); err != nil {
return err
}
fmt.Printf("\nConfig saved to %s\n", confpath)
return nil
}

var readPassword = func() (string, error) {
var readPassword = func(reader bufio.Reader) (string, string, error) {
var loadToken string

tokenURL, err := url.Parse(viper.GetString("core.host"))
if err != nil {
return "", "", err
}
tokenURL.Path = "profile/personal_access_tokens"

fmt.Printf("Create a token here: %s\nEnter default GitLab token (scope: api), or leave blank to provide a command to load the token: ", tokenURL.String())
byteToken, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", err
return "", "", err
}
return strings.TrimSpace(string(byteToken)), nil
if strings.TrimSpace(string(byteToken)) == "" {
fmt.Printf("\nEnter command to load the token:")
loadToken, err = reader.ReadString('\n')
if err != nil {
return "", "", err
}
}

if strings.TrimSpace(string(byteToken)) == "" && strings.TrimSpace(loadToken) == "" {
log.Fatal("Error: No token provided. A token can be created at ", tokenURL.String())
}
return strings.TrimSpace(string(byteToken)), strings.TrimSpace(loadToken), nil
}

// CI returns credentials suitable for use within GitLab CI or empty strings if
Expand Down Expand Up @@ -161,6 +180,28 @@ func getUser(host, token string, skipVerify bool) string {
return u.Username
}

// GetToken returns a token string from the config file.
// The token string can be cleartext or returned from a password manager or
// encryption utility.
func GetToken() string {
token := viper.GetString("core.token")
if token == "" && viper.GetString("core.load_token") != "" {
// args[0] isn't really an arg ;)
args := strings.Split(viper.GetString("core.load_token"), " ")
_token, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
log.Fatal(err)
}
token = string(_token)
// tools like pass and a simple bash script add a '\n' to
// their output which confuses the gitlab WebAPI
if token[len(token)-1:] == "\n" {
token = strings.TrimSuffix(token, "\n")
}
}
return token
}

// LoadConfig() loads the main config file and returns a tuple of
// host, user, token, ca_file, skipVerify
func LoadConfig() (string, string, string, string, bool) {
Expand Down Expand Up @@ -226,7 +267,7 @@ func LoadConfig() (string, string, string, string, bool) {

host = viper.GetString("core.host")
user = viper.GetString("core.user")
token = viper.GetString("core.token")
token = GetToken()
tlsSkipVerify := viper.GetBool("tls.skip_verify")
ca_file := viper.GetString("tls.ca_file")

Expand Down
114 changes: 108 additions & 6 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"bufio"
"bytes"
"fmt"
"io"
Expand Down Expand Up @@ -28,8 +29,8 @@ func TestNewConfig(t *testing.T) {
fmt.Fprintln(&buf, "https://gitlab.zaquestion.io")

oldreadPassword := readPassword
readPassword = func() (string, error) {
return "abcde12345", nil
readPassword = func(bufio.Reader) (string, string, error) {
return "abcde12345", "", nil
}
defer func() {
readPassword = oldreadPassword
Expand All @@ -54,7 +55,6 @@ func TestNewConfig(t *testing.T) {
out := <-outC

assert.Contains(t, out, "Enter GitLab host (default: https://gitlab.com): ")
assert.Contains(t, out, "Create a token here: https://gitlab.zaquestion.io/profile/personal_access_tokens\nEnter default GitLab token (scope: api):")

cfg, err := os.Open(path.Join(testconf, "lab.toml"))
if err != nil {
Expand Down Expand Up @@ -91,8 +91,8 @@ func TestNewConfigHostOverride(t *testing.T) {
os.Stdout = w

oldreadPassword := readPassword
readPassword = func() (string, error) {
return "abcde12345", nil
readPassword = func(bufio.Reader) (string, string, error) {
return "abcde12345", "", nil
}
defer func() {
readPassword = oldreadPassword
Expand All @@ -118,7 +118,6 @@ func TestNewConfigHostOverride(t *testing.T) {
out := <-outC

assert.NotContains(t, out, "Enter GitLab host")
assert.Contains(t, out, "Create a token here: https://gitlab2.zaquestion.io/profile/personal_access_tokens\nEnter default GitLab token (scope: api):")

cfg, err := os.Open(path.Join(testconf, "lab.toml"))
if err != nil {
Expand All @@ -138,6 +137,63 @@ func TestNewConfigHostOverride(t *testing.T) {
viper.Reset()
}

func TestNewLoadTokenConfig(t *testing.T) {
testconf := t.TempDir()

t.Run("create load_token config", func(t *testing.T) {
old := os.Stdout // keep backup of the real stdout
r, w, _ := os.Pipe()
os.Stdout = w

var buf bytes.Buffer
fmt.Fprintln(&buf, "https://gitlab.zaquestion.io")

oldreadPassword := readPassword
readPassword = func(bufio.Reader) (string, string, error) {
return "", "bash echo abcde12345", nil
}
defer func() {
readPassword = oldreadPassword
}()

err := New(path.Join(testconf, "lab.toml"), &buf)
if err != nil {
t.Fatal(err)
}

outC := make(chan string)
// copy the output in a separate goroutine so printing can't block indefinitely
go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
outC <- buf.String()
}()

// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC

assert.Contains(t, out, "Enter GitLab host (default: https://gitlab.com): ")

cfg, err := os.Open(path.Join(testconf, "lab.toml"))
if err != nil {
t.Fatal(err)
}

cfgData, err := ioutil.ReadAll(cfg)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, `
[core]
host = "https://gitlab.zaquestion.io"
load_token = "bash echo abcde12345"
`, string(cfgData))
})
viper.Reset()
}

func TestConvertHCLtoTOML(t *testing.T) {
tmpDir := t.TempDir()
oldCnfPath := filepath.Join(tmpDir, "lab.hcl")
Expand Down Expand Up @@ -176,3 +232,49 @@ func TestConvertHCLtoTOML(t *testing.T) {
user = "lab-testing"
`, string(cfgData))
}

func TestTokenTest(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "lab.toml")
config, err := os.Create(configPath)
if err != nil {
t.Fatal(err)
}
config.WriteString(`
[core]
host = "https://gitlab.com"
token = "foobar"
user = "lab-testing"
`)
viper.SetConfigName("lab")
viper.SetConfigType("toml")
viper.AddConfigPath(tmpDir)
viper.ReadInConfig()
token := GetToken()
os.Remove(configPath + "/lab.toml")
viper.Reset()
assert.Equal(t, "foobar", token)
}

func TestLoadTokenTest(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "lab.toml")
config, err := os.Create(configPath)
if err != nil {
t.Fatal(err)
}
config.WriteString(`
[core]
host = "https://gitlab.com"
load_token = "echo foobar"
user = "lab-testing"
`)
viper.SetConfigName("lab")
viper.SetConfigType("toml")
viper.AddConfigPath(tmpDir)
viper.ReadInConfig()
token := GetToken()
os.Remove(configPath + "/lab.toml")
viper.Reset()
assert.Equal(t, "foobar", token)
}

0 comments on commit 6298774

Please sign in to comment.