Skip to content

Commit

Permalink
feat: config init command (#291)
Browse files Browse the repository at this point in the history
* refactor: config generator for integration tests

* test: adjust scopes in request

* feat: config init command

* fix: command

* chore: friendlier error

* refactor: address comments

* style: remove var

* style: hide command

* refactor: address comments

Co-authored-by: Rita Zerrizuela <[email protected]>
  • Loading branch information
as-herzog and Widcket authored Jun 14, 2021
1 parent 059b42d commit 54fc3f3
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 234 deletions.
7 changes: 2 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,16 @@ mocks: $(GOBIN)/mockgen
$(GOBIN)/commander:
cd && GO111MODULE=auto go get github.com/commander-cli/commander/cmd/commander

$(GOBIN)/auth0-cli-config-generator:
go install ./pkg/auth0-cli-config-generator

run-integration:
auth0-cli-config-generator && commander test commander.yaml
auth0 config init && commander test commander.yaml
.PHONY: run-integration

# Delete all test apps created during integration testing
integration-cleanup:
./integration/test-cleanup.sh
.PHONY: integration-cleanup

integration: build $(GOBIN)/auth0-cli-config-generator $(GOBIN)/commander
integration: build $(GOBIN)/commander
$(MAKE) run-integration; \
ret=$$?; \
$(MAKE) integration-cleanup; \
Expand Down
11 changes: 11 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ var requiredScopes = []string{
// RequiredScopes returns the scopes used for login.
func RequiredScopes() []string { return requiredScopes }

// RequiredScopesMin returns minimum scopes used for login in integration tests.
func RequiredScopesMin() []string {
min := []string{}
for _, s := range requiredScopes {
if s != "offline_access" && s != "openid" {
min = append(min, s)
}
}
return min
}

// SecretStore provides access to stored sensitive data.
type SecretStore interface {
// Get gets the secret
Expand Down
14 changes: 13 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func (c *cli) init() error {

func (c *cli) initContext() (err error) {
if c.path == "" {
c.path = path.Join(os.Getenv("HOME"), ".config", "auth0", "config.json")
c.path = defaultConfigPath()
}

if _, err := os.Stat(c.path); os.IsNotExist(err) {
Expand All @@ -472,6 +472,18 @@ func (c *cli) initContext() (err error) {
return nil
}

func defaultConfigPath() string {
return path.Join(os.Getenv("HOME"), ".config", "auth0", "config.json")
}

func (c *cli) setPath(p string) {
if p == "" {
c.path = defaultConfigPath()
return
}
c.path = p
}

func canPrompt(cmd *cobra.Command) bool {
noInput, err := cmd.Root().Flags().GetBool("no-input")

Expand Down
132 changes: 132 additions & 0 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cli

import (
"context"
"fmt"
"net/url"
"strings"

"github.com/auth0/auth0-cli/internal/auth"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/oauth2/clientcredentials"
)

var desiredInputs = `Config init is intended for non-interactive use,
ensure the following env variables are set:
AUTH0_CLI_CLIENT_DOMAIN
AUTH0_CLI_CLIENT_ID
AUTH0_CLI_CLIENT_SECRET
Interactive logins should use "auth0 login" instead.`

type params struct {
filePath string
clientDomain string
clientID string
clientSecret string
}

func (p params) validate() error {
if p.clientDomain == "" {
return fmt.Errorf("missing client domain:\n%s", desiredInputs)
}

u, err := url.Parse(p.clientDomain)
if err != nil {
return fmt.Errorf("failed to parse client domain: %s", p.clientDomain)
}

if u.Scheme != "" {
return fmt.Errorf("client domain cant include a scheme: %s", p.clientDomain)
}

if p.clientID == "" {
return fmt.Errorf("missing client id:\n%s", desiredInputs)
}

if p.clientSecret == "" {
return fmt.Errorf("missing client secret:\n%s", desiredInputs)
}
return nil
}

func configCmd(cli *cli) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage auth0-cli config",
Long: "Manage auth0-cli config",
Hidden: true,
}

cmd.AddCommand(initCmd(cli))
return cmd
}

func initCmd(cli *cli) *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Configure the CLI from environment variables",
RunE: func(command *cobra.Command, args []string) error {
filePath := viper.GetString("FILEPATH")
clientDomain := viper.GetString("CLIENT_DOMAIN")
clientID := viper.GetString("CLIENT_ID")
clientSecret := viper.GetString("CLIENT_SECRET")

cli.setPath(filePath)
p := params{filePath, clientDomain, clientID, clientSecret}
if err := p.validate(); err != nil {
return err
}

u, err := url.Parse("https://" + p.clientDomain)
if err != nil {
return err
}

c := &clientcredentials.Config{
ClientID: p.clientID,
ClientSecret: p.clientSecret,
TokenURL: u.String() + "/oauth/token",
EndpointParams: url.Values{
"client_id": {p.clientID},
"scope": {strings.Join(auth.RequiredScopesMin(), " ")},
"audience": {u.String() + "/api/v2/"},
},
}

token, err := c.Token(context.Background())
if err != nil {
return err
}

t := tenant{
Name: p.clientDomain,
Domain: p.clientDomain,
AccessToken: token.AccessToken,
ExpiresAt: token.Expiry,
Scopes: auth.RequiredScopes(),
}

if err := cli.addTenant(t); err != nil {
return fmt.Errorf("unexpected error adding tenant to config: %w", err)
}
return nil
},
}
viper.SetEnvPrefix("AUTH0_CLI")
viper.AutomaticEnv()

flags := cmd.Flags()
flags.String("filepath", defaultConfigPath(), "Filepath for the auth0 cli config")
_ = viper.BindPFlag("FILEPATH", flags.Lookup("filepath"))
flags.String("client-id", "", "Client ID to set within config")
_ = viper.BindPFlag("CLIENT_ID", flags.Lookup("client-id"))
flags.String("client-secret", "", "Client secret to use to generate token which is set within config")
_ = viper.BindPFlag("CLIENT_SECRET", flags.Lookup("client-secret"))
flags.String("client-domain", "", "Client domain to use to generate token which is set within config")
_ = viper.BindPFlag("CLIENT_DOMAIN", flags.Lookup("client-domain"))

return cmd
}
8 changes: 7 additions & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func Execute() {
return nil
}

// config init shouldn't trigger a login.
if cmd.CalledAs() == "init" && cmd.Parent().Use == "config" {
return nil
}

defer cli.tracker.TrackCommandRun(cmd, cli.config.InstallID)

// Initialize everything once. Later callers can then
Expand Down Expand Up @@ -96,6 +101,7 @@ func Execute() {
// order of the comamnds here matters
// so add new commands in a place that reflect its relevance or relation with other commands:
rootCmd.AddCommand(loginCmd(cli))
rootCmd.AddCommand(configCmd(cli))
rootCmd.AddCommand(tenantsCmd(cli))
rootCmd.AddCommand(usersCmd(cli))
rootCmd.AddCommand(appsCmd(cli))
Expand Down Expand Up @@ -144,7 +150,7 @@ func Execute() {
os.Exit(1)
}

ctx, cancel := context.WithTimeout(cli.context, 3 * time.Second)
ctx, cancel := context.WithTimeout(cli.context, 3*time.Second)
// defers are executed in LIFO order
defer cancel()
defer cli.tracker.Wait(ctx) // No event should be tracked after this has run, or it will panic e.g. in earlier deferred functions
Expand Down
Loading

0 comments on commit 54fc3f3

Please sign in to comment.