Skip to content

Commit

Permalink
Merge branch 'v2.9' into v2.7
Browse files Browse the repository at this point in the history
  • Loading branch information
HarrisonWAffel authored Feb 9, 2024
2 parents c23b0e6 + a12fd3b commit 8e4a8c2
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Dockerfile.dapper
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM registry.suse.com/bci/golang:1.19
FROM registry.suse.com/bci/golang:1.20

RUN zypper -n install docker rsync xz zip

Expand Down
25 changes: 6 additions & 19 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -259,27 +258,15 @@ func verifyCert(caCert []byte) (string, error) {
return string(caCert), nil
}

func loadConfig(ctx *cli.Context) (config.Config, error) {
func GetConfigPath(ctx *cli.Context) string {
// path will always be set by the global flag default
path := ctx.GlobalString("config")
path = filepath.Join(path, cfgFile)

cf := config.Config{
Path: path,
Servers: make(map[string]*config.ServerConfig),
}

content, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return cf, nil
} else if err != nil {
return cf, err
}

err = json.Unmarshal(content, &cf)
cf.Path = path
return filepath.Join(path, cfgFile)
}

return cf, err
func loadConfig(ctx *cli.Context) (config.Config, error) {
path := GetConfigPath(ctx)
return config.LoadFromPath(path)
}

func lookupConfig(ctx *cli.Context) (*config.ServerConfig, error) {
Expand Down
3 changes: 3 additions & 0 deletions cmd/kubectl_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ func cacheCredential(ctx *cli.Context, cred *config.ExecCredential, id string) e
return err
}

if sc.KubeCredentials[id] == nil {
sc.KubeCredentials = make(map[string]*config.ExecCredential)
}
sc.KubeCredentials[id] = cred
cf.Servers[server] = sc
return cf.Write()
Expand Down
8 changes: 6 additions & 2 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ func serverCurrent(ctx *cli.Context) error {
}

serverName := cf.CurrentServer
URL := cf.Servers[serverName].URL
fmt.Printf("Name: %s URL: %s\n", serverName, URL)
currentServer, found := cf.Servers[serverName]
if !found {
return errors.New("Current server not set")
}

fmt.Printf("Name: %s URL: %s\n", serverName, currentServer.URL)
return nil
}

Expand Down
53 changes: 52 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -32,6 +33,56 @@ type ServerConfig struct {
KubeConfigs map[string]*api.Config `json:"kubeConfigs"`
}

// LoadFromPath attempts to load a config from the given file path. If the file
// doesn't exist, an empty config is returned.
func LoadFromPath(path string) (Config, error) {
cf := Config{
Path: path,
Servers: make(map[string]*ServerConfig),
}

content, err := os.ReadFile(path)
if err != nil {
// it's okay if the file is empty, we still return a valid config
if os.IsNotExist(err) {
return cf, nil
}

return cf, err
}

if err := json.Unmarshal(content, &cf); err != nil {
return cf, fmt.Errorf("unmarshaling %s: %w", path, err)
}
cf.Path = path

return cf, nil
}

// GetFilePermissionWarnings returns the following warnings based on the file permission:
// - one warning if the file is group-readable
// - one warning if the file is world-readable
// We want this because configuration may have sensitive information (eg: creds).
// A nil error is returned if the file doesn't exist.
func GetFilePermissionWarnings(path string) ([]string, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return []string{}, fmt.Errorf("get file info: %w", err)
}

var warnings []string
if info.Mode()&0040 > 0 {
warnings = append(warnings, fmt.Sprintf("Rancher configuration file %s is group-readable. This is insecure.", path))
}
if info.Mode()&0004 > 0 {
warnings = append(warnings, fmt.Sprintf("Rancher configuration file %s is world-readable. This is insecure.", path))
}
return warnings, nil
}

func (c Config) Write() error {
err := os.MkdirAll(filepath.Dir(c.Path), 0700)
if err != nil {
Expand All @@ -40,7 +91,7 @@ func (c Config) Write() error {
logrus.Infof("Saving config to %s", c.Path)
p := c.Path
c.Path = ""
output, err := os.Create(p)
output, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
Expand Down
211 changes: 211 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package config

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

const (
validFile = `
{
"Servers": {
"rancherDefault": {
"accessKey": "the-access-key",
"secretKey": "the-secret-key",
"tokenKey": "the-token-key",
"url": "https://example.com",
"project": "cluster-id:project-id",
"cacert": "",
"kubeCredentials": null,
"kubeConfigs": null
}
},
"CurrentServer": "rancherDefault"
}`
invalidFile = `invalid config file`
)

func Test_GetFilePermissionWarnings(t *testing.T) {
t.Parallel()

tests := []struct {
name string
mode os.FileMode
expectedWarnings int
}{
{
name: "neither group-readable nor world-readable",
mode: os.FileMode(0600),
expectedWarnings: 0,
},
{
name: "group-readable and world-readable",
mode: os.FileMode(0644),
expectedWarnings: 2,
},
{
name: "group-readable",
mode: os.FileMode(0640),
expectedWarnings: 1,
},
{
name: "world-readable",
mode: os.FileMode(0604),
expectedWarnings: 1,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert := assert.New(t)

dir, err := os.MkdirTemp("", "rancher-cli-test-*")
assert.NoError(err)
defer os.RemoveAll(dir)

path := filepath.Join(dir, "cli2.json")
err = os.WriteFile(path, []byte(validFile), tt.mode)
assert.NoError(err)

warnings, err := GetFilePermissionWarnings(path)
assert.NoError(err)
assert.Len(warnings, tt.expectedWarnings)
})
}
}

func Test_Permission(t *testing.T) {
t.Parallel()

// New config files should have 0600 permissions
t.Run("new config file", func(t *testing.T) {
t.Parallel()
assert := assert.New(t)

dir, err := os.MkdirTemp("", "rancher-cli-test-*")
assert.NoError(err)
defer os.RemoveAll(dir)

path := filepath.Join(dir, "cli2.json")
conf, err := LoadFromPath(path)
assert.NoError(err)

err = conf.Write()
assert.NoError(err)

info, err := os.Stat(path)
assert.NoError(err)
assert.Equal(os.FileMode(0600), info.Mode())

// make sure new file doesn't create permission warnings
warnings, err := GetFilePermissionWarnings(path)
assert.NoError(err)
assert.Len(warnings, 0)
})
// Already existing config files should keep their current permissions
t.Run("existing config file", func(t *testing.T) {
t.Parallel()
assert := assert.New(t)

dir, err := os.MkdirTemp("", "rancher-cli-test-*")
assert.NoError(err)
defer os.RemoveAll(dir)

path := filepath.Join(dir, "cli2.json")
err = os.WriteFile(path, []byte(validFile), 0700)
assert.NoError(err)

conf, err := LoadFromPath(path)
assert.NoError(err)

err = conf.Write()
assert.NoError(err)

info, err := os.Stat(path)
assert.NoError(err)
assert.Equal(os.FileMode(0700), info.Mode())
})
}

func Test_LoadFromPath(t *testing.T) {
t.Parallel()

tests := []struct {
name string
content string
expectedConf Config
expectedErr bool
}{
{
name: "valid config",
content: validFile,
expectedConf: Config{
Servers: map[string]*ServerConfig{
"rancherDefault": {
AccessKey: "the-access-key",
SecretKey: "the-secret-key",
TokenKey: "the-token-key",
URL: "https://example.com",
Project: "cluster-id:project-id",
CACerts: "",
},
},
CurrentServer: "rancherDefault",
},
},
{
name: "invalid config",
content: invalidFile,
expectedConf: Config{
Servers: map[string]*ServerConfig{},
},
expectedErr: true,
},
{
name: "non existing file",
content: "",
expectedConf: Config{
Servers: map[string]*ServerConfig{},
CurrentServer: "",
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert := assert.New(t)

dir, err := os.MkdirTemp("", "rancher-cli-test-*")
assert.NoError(err)
defer os.RemoveAll(dir)

path := filepath.Join(dir, "cli2.json")
// make sure the path points to the temp dir created in the test
tt.expectedConf.Path = path

if tt.content != "" {
err = os.WriteFile(path, []byte(tt.content), 0600)
assert.NoError(err)
}

conf, err := LoadFromPath(path)
if tt.expectedErr {
assert.Error(err)
// We kept the old behavior of returning a valid config even in
// case of an error so we assert it here. If you change this
// behavior, make sure there aren't any regressions.
assert.Equal(tt.expectedConf, conf)
return
}

assert.NoError(err)
assert.Equal(tt.expectedConf, conf)
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/rancher/cli

go 1.19
go 1.20

replace k8s.io/client-go => k8s.io/client-go v0.20.1

Expand Down
12 changes: 12 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/pkg/errors"
"github.com/rancher/cli/cmd"
"github.com/rancher/cli/config"
rancherprompt "github.com/rancher/cli/rancher_prompt"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
Expand Down Expand Up @@ -70,6 +71,17 @@ func mainErr() error {
if ctx.GlobalBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}

path := cmd.GetConfigPath(ctx)
warnings, err := config.GetFilePermissionWarnings(path)
if err != nil {
// We don't want to block the execution of the CLI in that case
logrus.Errorf("Unable to verify config file permission: %s. Continuing.", err)
}
for _, warning := range warnings {
logrus.Warning(warning)
}

return nil
}
app.Version = VERSION
Expand Down

0 comments on commit 8e4a8c2

Please sign in to comment.