From d8758f53f91e9baaab84ff15712313a69f246223 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 19:48:47 -0800 Subject: [PATCH] Cleanup internal/cli package. Make it work --- internal/cli/cli.go | 154 ++++++++++++++++++++++++++++++------------ internal/cli/login.go | 12 +++- internal/cli/root.go | 6 ++ 3 files changed, 128 insertions(+), 44 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 0ed01f258..4b72ebfb7 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,66 +6,89 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "strings" "sync" + "time" "github.com/auth0/auth0-cli/internal/display" "gopkg.in/auth0.v5/management" ) -type data struct { +// config defines the exact set of tenants, access tokens, which only exists +// for a particular user's machine. +type config struct { DefaultTenant string `json:"default_tenant"` Tenants map[string]tenant `json:"tenants"` } +// tenant is the cli's concept of an auth0 tenant. The fields are tailor fit +// specifically for interacting with the management API. type tenant struct { - Domain string `json:"domain"` - - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - - // TODO(cyx): This will be what we do with device flow. - BearerToken string `json:"bearer_token,omitempty"` + Name string `json:"name"` + Domain string `json:"domain"` + AccessToken string `json:"access_token,omitempty"` + ExpiresAt time.Time `json:expires_at"` } +// cli provides all the foundational things for all the commands in the CLI, +// specifically: +// +// 1. A management API instance (e.g. go-auth0/auth0) +// 2. A renderer (which provides ansi, coloring, etc). +// +// In addition, it stores a reference to all the flags passed, e.g.: +// +// 1. --format +// 2. --tenant +// 3. --verbose +// type cli struct { + // core primitives exposed to command builders. api *management.Management renderer *display.Renderer + // set of flags which are user specified. verbose bool tenant string format string + // config state management. initOnce sync.Once path string - data data + config config } +// setup will try to initialize the config context, as well as figure out if +// there's a readily available tenant. A management API SDK instance is initialized IFF: +// +// 1. A tenant is found. +// 2. The tenant has an access token. func (c *cli) setup() error { + c.init() + t, err := c.getTenant() if err != nil { return err } - if t.BearerToken != "" { + if t.AccessToken != "" { c.api, err = management.New(t.Domain, - management.WithStaticToken(t.BearerToken), - management.WithDebug(c.verbose)) - } else { - c.api, err = management.New(t.Domain, - management.WithClientCredentials(t.ClientID, t.ClientSecret), + management.WithStaticToken(t.AccessToken), management.WithDebug(c.verbose)) } - return err + return nil } +// getTenant fetches the default tenant configured (or the tenant specified via +// the --tenant flag). func (c *cli) getTenant() (tenant, error) { if err := c.init(); err != nil { return tenant{}, err } - t, ok := c.data.Tenants[c.tenant] + t, ok := c.config.Tenants[c.tenant] if !ok { return tenant{}, fmt.Errorf("Unable to find tenant: %s", c.tenant) } @@ -73,44 +96,89 @@ func (c *cli) getTenant() (tenant, error) { return t, nil } -func (c *cli) init() error { - var err error - c.initOnce.Do(func() { - if c.path == "" { - c.path = path.Join(os.Getenv("HOME"), ".config", "auth0", "config.json") - } +// setTenant assigns an existing, or new tenant. This is expected to be called +// after a login has completed. +func (c *cli) setTenant(ten tenant) error { + c.init() - var buf []byte - if buf, err = ioutil.ReadFile(c.path); err != nil { - return - } + // If there's no existing DefaultTenant yet, might as well set the + // first successfully logged in tenant during onboarding. + if c.config.DefaultTenant == "" { + c.config.DefaultTenant = ten.Name + } - if err = json.Unmarshal(buf, &c.data); err != nil { - return - } + // If we're dealing with an empty file, we'll need to initialize this + // map. + if c.config.Tenants == nil { + c.config.Tenants = map[string]tenant{} + } - if c.tenant == "" && c.data.DefaultTenant == "" { - err = fmt.Errorf("Not yet configured. Try `auth0 login`.") - return - } + c.config.Tenants[ten.Name] = ten + + dir := filepath.Dir(c.path) + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.MkdirAll(dir, 0700) + } - if c.tenant == "" { - c.tenant = c.data.DefaultTenant + buf, err := json.MarshalIndent(c.config, "", " ") + if err != nil { + return err + } + + if err := ioutil.WriteFile(c.path, buf, 0600); err != nil { + return err + } + + return nil +} + +func (c *cli) init() error { + var err error + c.initOnce.Do(func() { + // Initialize the context -- e.g. the configuration + // information, tenants, etc. + if err = c.initContext(); err != nil { + return } + c.renderer.Tenant = c.tenant + // Determine what the desired output format is. format := strings.ToLower(c.format) if format != "" && format != string(display.OutputFormatJSON) { err = fmt.Errorf("Invalid format. Use `--format=json` or omit this option to use the default format.") return } - - c.renderer = &display.Renderer{ - Tenant: c.tenant, - MessageWriter: os.Stderr, - ResultWriter: os.Stdout, - Format: display.OutputFormat(format), - } + c.renderer.Format = display.OutputFormat(format) }) return err } + +func (c *cli) initContext() (err error) { + if c.path == "" { + c.path = path.Join(os.Getenv("HOME"), ".config", "auth0", "config.json") + } + + if _, err := os.Stat(c.path); os.IsNotExist(err) { + return fmt.Errorf("Not yet configured. Try `auth0 login`.") + } + + var buf []byte + if buf, err = ioutil.ReadFile(c.path); err != nil { + return err + } + + if err := json.Unmarshal(buf, &c.config); err != nil { + return err + } + + if c.tenant == "" && c.config.DefaultTenant == "" { + return fmt.Errorf("Not yet configured. Try `auth0 login`.") + } + + if c.tenant == "" { + c.tenant = c.config.DefaultTenant + } + + return nil +} diff --git a/internal/cli/login.go b/internal/cli/login.go index af0f37cea..46512b6ee 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "time" "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth" @@ -42,9 +43,18 @@ func loginCmd(cli *cli) *cobra.Command { return fmt.Errorf("login error: %w", err) } - // TODO(jfatta): update the configuration with the token, tenant, audience, etc cli.renderer.Infof("Successfully logged in.") cli.renderer.Infof("Tenant: %s", res.Tenant) + + cli.setTenant(tenant{ + Name: res.Tenant, + Domain: res.Domain, + AccessToken: res.AccessToken, + ExpiresAt: time.Now().Add( + time.Duration(res.ExpiresIn) * time.Second, + ), + }) + return nil }, } diff --git a/internal/cli/root.go b/internal/cli/root.go index 716b3ba02..7096c861a 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -31,6 +31,12 @@ func Execute() { Long: "Command-line tool to interact with Auth0.\n" + getLogin(&fs, cli), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // If the user is trying to login, no need to go + // through setup. + if cmd.Use == "login" { + return nil + } + // Initialize everything once. Later callers can then // freely assume that config is fully primed and ready // to go.