Skip to content

Commit

Permalink
Merge pull request #47 from jetstack/file-perms
Browse files Browse the repository at this point in the history
Config file management refactor and permissions fix
  • Loading branch information
irbekrm authored Nov 1, 2022
2 parents 7d11dfa + d351811 commit bbad8ed
Show file tree
Hide file tree
Showing 16 changed files with 594 additions and 244 deletions.
50 changes: 20 additions & 30 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/gofrs/uuid"
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"

"github.com/jetstack/jsctl/internal/config"
)

const (
Expand Down Expand Up @@ -152,25 +154,18 @@ func WaitForOAuthToken(ctx context.Context, conf *oauth2.Config, state string) (
// SaveOAuthToken writes the provided token to a JSON file in the user's config directory. This location changes based
// on the host operating system. See the documentation for os.UserConfigDir for specifics on where the token file will
// be placed.
func SaveOAuthToken(token *oauth2.Token) error {
configDir, err := os.UserConfigDir()
func SaveOAuthToken(ctx context.Context, token *oauth2.Token) error {
jsonBytes, err := json.Marshal(token)
if err != nil {
return err
return fmt.Errorf("failed to marshal token to JSON: %w", err)
}

tokenDir := filepath.Join(configDir, "jsctl")
if err = os.MkdirAll(tokenDir, 0755); err != nil {
return err
}

tokenFile := filepath.Join(tokenDir, tokenFileName)
file, err := os.Create(tokenFile)
err = config.WriteConfigFile(ctx, tokenFileName, jsonBytes)
if err != nil {
return err
return fmt.Errorf("failed to write token file: %w", err)
}
defer file.Close()

return json.NewEncoder(file).Encode(token)
return nil
}

// ErrNoToken is the error given when attempting to load an oauth token from disk that cannot be found.
Expand All @@ -179,24 +174,19 @@ var ErrNoToken = errors.New("no oauth token")
// LoadOAuthToken attempts to load an oauth token from the configuration directory. The location of the token file changes
// based on the host operating system. See the documentation for os.UserConfigDir for specifics on where the token file will
// be loaded from. Returns ErrNoToken if a token file cannot be found.
func LoadOAuthToken() (*oauth2.Token, error) {
tokenFile, err := DetermineTokenFilePath()
if err != nil {
return &oauth2.Token{}, fmt.Errorf("failed to determine token file path: %w", err)
}

file, err := os.Open(tokenFile)
func LoadOAuthToken(ctx context.Context) (*oauth2.Token, error) {
data, err := config.ReadConfigFile(ctx, tokenFileName)
switch {
case errors.Is(err, os.ErrNotExist):
return nil, ErrNoToken
case err != nil:
return nil, err
}
defer file.Close()

var token oauth2.Token
if err = json.NewDecoder(file).Decode(&token); err != nil {
return nil, err
err = json.Unmarshal(data, &token)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal token from JSON: %w", err)
}

return &token, nil
Expand All @@ -205,8 +195,8 @@ func LoadOAuthToken() (*oauth2.Token, error) {
// DeleteOAuthToken attempts to remove an oauth token from the configuration directory. The location of the token file changes
// based on the host operating system. See the documentation for os.UserConfigDir for specifics on where the token file will
// be located. Returns ErrNoToken if a token file cannot be found.
func DeleteOAuthToken() error {
tokenFile, err := DetermineTokenFilePath()
func DeleteOAuthToken(ctx context.Context) error {
tokenFile, err := DetermineTokenFilePath(ctx)
if err != nil {
return fmt.Errorf("failed to determine token file path: %w", err)
}
Expand All @@ -223,13 +213,13 @@ func DeleteOAuthToken() error {
}

// DetermineTokenFilePath attempts to determine the path to the oauth token file.
func DetermineTokenFilePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("failed to determine user config directory: %w", err)
func DetermineTokenFilePath(ctx context.Context) (string, error) {
configDir, ok := ctx.Value(config.ContextKey{}).(string)
if !ok {
return "", fmt.Errorf("no config path provided")
}

tokenFile := filepath.Join(configDir, "jsctl", tokenFileName)
tokenFile := filepath.Join(configDir, tokenFileName)

return tokenFile, nil
}
Expand Down
29 changes: 24 additions & 5 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package auth_test

import (
"context"
"encoding/json"
"os"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/jetstack/jsctl/internal/auth"
"github.com/jetstack/jsctl/internal/config"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
Expand All @@ -19,8 +24,15 @@ func TestLoadSaveOAuthToken(t *testing.T) {
Expiry: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
}

assert.NoError(t, auth.SaveOAuthToken(expected))
actual, err := auth.LoadOAuthToken()
tempConfigDir, err := os.MkdirTemp("", "token-test-*")
require.NoError(t, err)
defer os.Remove(tempConfigDir)

ctx := config.ToContext(context.Background(), &config.Config{Organization: "example"})
ctx = context.WithValue(ctx, config.ContextKey{}, tempConfigDir)

assert.NoError(t, auth.SaveOAuthToken(ctx, expected))
actual, err := auth.LoadOAuthToken(ctx)
assert.NoError(t, err)
assert.EqualValues(t, expected, actual)
}
Expand All @@ -36,19 +48,26 @@ func TestGetOAuthConfig(t *testing.T) {
}

func TestDeleteOAuthToken(t *testing.T) {
tempConfigDir, err := os.MkdirTemp("", "token-test-*")
require.NoError(t, err)
defer os.Remove(tempConfigDir)

ctx := config.ToContext(context.Background(), &config.Config{Organization: "example"})
ctx = context.WithValue(ctx, config.ContextKey{}, tempConfigDir)

t.Run("It should remove an oauth token", func(t *testing.T) {
assert.NoError(t, auth.SaveOAuthToken(&oauth2.Token{
assert.NoError(t, auth.SaveOAuthToken(ctx, &oauth2.Token{
AccessToken: "test",
TokenType: "test",
RefreshToken: "test",
Expiry: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
}))

assert.NoError(t, auth.DeleteOAuthToken())
assert.NoError(t, auth.DeleteOAuthToken(ctx))
})

t.Run("It should return an error if there is no oauth token", func(t *testing.T) {
assert.Error(t, auth.DeleteOAuthToken())
assert.Error(t, auth.DeleteOAuthToken(ctx))
})
}

Expand Down
23 changes: 12 additions & 11 deletions internal/command/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ func authStatus() *cobra.Command {
return fmt.Errorf("failed to login with credentials file %q: %w", credentials, err)
}
} else {
tokenPath, err = auth.DetermineTokenFilePath()
tokenPath, err = auth.DetermineTokenFilePath(ctx)
if err != nil {
fmt.Println("Can't find token", err)
return fmt.Errorf("failed to determine token path: %w", err)
}
if _, err := os.Stat(tokenPath); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("token missing at %s", tokenPath)
}

fmt.Println("Token path:", tokenPath)

token, err = auth.LoadOAuthToken()
token, err = auth.LoadOAuthToken(ctx)
if err != nil {
fmt.Println("Not logged in")
return nil
Expand Down Expand Up @@ -117,18 +121,15 @@ func authLogin() *cobra.Command {
return fmt.Errorf("failed to obtain token: %w", err)
}

if err = auth.SaveOAuthToken(token); err != nil {
if err = auth.SaveOAuthToken(ctx, token); err != nil {
return fmt.Errorf("failed to save token: %w", err)
}

fmt.Println("Login succeeded")

err = config.Create(&config.Config{})
switch {
case errors.Is(err, config.ErrConfigExists):
break
case err != nil:
return fmt.Errorf("failed to create configuration file: %w", err)
err = config.Save(ctx, &config.Config{})
if err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}

cnf, ok := config.FromContext(ctx)
Expand Down Expand Up @@ -157,7 +158,7 @@ func authLogout() *cobra.Command {
Use: "logout",
Args: cobra.ExactArgs(0),
Run: run(func(ctx context.Context, args []string) error {
err := auth.DeleteOAuthToken()
err := auth.DeleteOAuthToken(ctx)
switch {
case errors.Is(err, auth.ErrNoToken):
return fmt.Errorf("host contains no authentication data")
Expand Down
44 changes: 42 additions & 2 deletions internal/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,32 @@ var (
stdout bool
kubeConfig string
apiURL string
configDir string

errNoOrganizationName = errors.New("You do not have an organization selected, select one using: \n\n\tjsctl config set organization [name]")
)

// Command returns the root cobra.Command instance for the entire command-line interface.
func Command() *cobra.Command {
var err error

cmd := &cobra.Command{
Use: "jsctl",
Short: "Command-line tool for the Jetstack Secure Control Plane",
}

// determine the default location of the jsctl config file
defaultConfigDir, err := config.DefaultConfigDir()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to determine default user config directory, using current directory")
defaultConfigDir = "."
}

flags := cmd.PersistentFlags()
flags.BoolVar(&stdout, "stdout", false, "If provided, manifests are written to stdout rather than applied to the current cluster")
flags.StringVar(&kubeConfig, "kubeconfig", defaultKubeConfig(), "Location of the user's kubeconfig file for applying directly to the cluster")
flags.StringVar(&apiURL, "api-url", "https://platform.jetstack.io", "Base URL of the control-plane API")
flags.StringVar(&configDir, "config", defaultConfigDir, "Base URL of the control-plane API")

cmd.AddCommand(
Auth(),
Expand All @@ -49,9 +60,38 @@ func Command() *cobra.Command {

func run(fn func(ctx context.Context, args []string) error) func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, args []string) {
var err error
ctx := cmd.Context()

token, err := auth.LoadOAuthToken()
defaultConfigDir, err := config.DefaultConfigDir()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to determine default user config directory, using current directory\n")
defaultConfigDir = "."
}

// if the user is using configDir defaulting, then we need to check for
// legacy config directories and migrate them if they exist
if configDir == defaultConfigDir {
err := config.MigrateDefaultConfig(configDir)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to migrate legacy config directory: %s\n", err)
os.Exit(1)
}
}

// write the configuration directory to the context so that it can be
// used in subcommands
ctx = context.WithValue(ctx, config.ContextKey{}, configDir)

// ensure that the config dir specified exists, this allows other
// commands to write to sub paths of this directory without concern
// for the config dir existing
err = os.MkdirAll(configDir, 0700)
if err != nil {
exitf("failed to create config directory: %s", err)
}

token, err := auth.LoadOAuthToken(ctx)
switch {
case errors.Is(err, auth.ErrNoToken):
break
Expand All @@ -61,7 +101,7 @@ func run(fn func(ctx context.Context, args []string) error) func(cmd *cobra.Comm
ctx = auth.TokenToContext(ctx, token)
}

cnf, err := config.Load()
cnf, err := config.Load(ctx)
switch {
case errors.Is(err, config.ErrNoConfiguration):
break
Expand Down
46 changes: 44 additions & 2 deletions internal/command/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import (
"context"
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
"gopkg.in/yaml.v2"

"github.com/jetstack/jsctl/internal/auth"
"github.com/jetstack/jsctl/internal/client"
"github.com/jetstack/jsctl/internal/config"
"github.com/jetstack/jsctl/internal/organization"
"github.com/spf13/cobra"
)

// Config returns a cobra.Command instance that is the root for all "jsctl config" subcommands.
Expand All @@ -21,6 +25,7 @@ func Config() *cobra.Command {

cmd.AddCommand(
configSet(),
configShow(),
)

return cmd
Expand All @@ -39,6 +44,36 @@ func configSet() *cobra.Command {
return cmd
}

func configShow() *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "View your current configuration values",
Args: cobra.ExactValidArgs(0),
Run: run(func(ctx context.Context, args []string) error {
cnf, ok := config.FromContext(ctx)
if !ok {
return errors.New("config was not present, have you logged in? try jsctl auth login")
}

configDir, ok := ctx.Value(config.ContextKey{}).(string)
if !ok {
configDir = "unknown"
}

fmt.Fprintln(os.Stderr, "Configuration loaded from", configDir)

yamlBytes, err := yaml.Marshal(cnf)
if err != nil {
return fmt.Errorf("failed to marshal configuration: %w", err)
}

fmt.Fprintf(os.Stderr, string(yamlBytes))

return nil
}),
}
}

func configSetOrganization() *cobra.Command {
return &cobra.Command{
Use: "organization [value]",
Expand All @@ -50,6 +85,13 @@ func configSetOrganization() *cobra.Command {
return errors.New("you must specify an organization name")
}

// users must be logged in to run this command. Organizations can
// only be selected from the current token's organizations.
_, ok := auth.TokenFromContext(ctx)
if !ok {
return fmt.Errorf("you must be logged in to run this command, run jsctl auth login")
}

http := client.New(ctx, apiURL)
organizations, err := organization.List(ctx, http)
if err != nil {
Expand All @@ -75,7 +117,7 @@ func configSetOrganization() *cobra.Command {
}

cnf.Organization = name
if err = config.Save(cnf); err != nil {
if err = config.Save(ctx, cnf); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}

Expand Down
Loading

0 comments on commit bbad8ed

Please sign in to comment.