From a13b6c4afb9b308aed2ae2342bfe98acb875f104 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 19:47:35 -0800 Subject: [PATCH 01/10] Amend scopes to add client list Need to figure out how to add all scopes eventually :) --- internal/auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 4b9d82c53..5a59906b6 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -33,7 +33,7 @@ const ( deviceCodeEndpoint = "https://auth0.auth0.com/oauth/device/code" oauthTokenEndpoint = "https://auth0.auth0.com/oauth/token" // TODO(jfatta) extend the scope as we extend the CLI: - scope = "openid read:roles" + scope = "openid read:roles read:clients" audiencePath = "/api/v2/" ) From ccd9d87b99073bda7eadcdf672a7318d6e0a821a Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 19:48:01 -0800 Subject: [PATCH 02/10] Amend auth to add the domain as part of the set of results --- internal/auth/auth.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5a59906b6..06465cb5b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -42,6 +42,7 @@ type Authenticator struct { type Result struct { Tenant string + Domain string AccessToken string ExpiresIn int64 } @@ -106,14 +107,15 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { return Result{}, errors.New(res.ErrorDescription) } - t, err := parseTenant(res.AccessToken) + ten, domain, err := parseTenant(res.AccessToken) if err != nil { return Result{}, fmt.Errorf("cannot parse tenant from the given access token: %w", err) } return Result{ AccessToken: res.AccessToken, ExpiresIn: res.ExpiresIn, - Tenant: t, + Tenant: ten, + Domain: domain, }, nil } } @@ -139,27 +141,27 @@ func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) { return res, nil } -func parseTenant(accessToken string) (string, error) { +func parseTenant(accessToken string) (tenant, domain string, err error) { parts := strings.Split(accessToken, ".") v, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { - return "", err + return "", "", err } var payload struct { AUDs []string `json:"aud"` } if err := json.Unmarshal([]byte(v), &payload); err != nil { - return "", err + return "", "", err } for _, aud := range payload.AUDs { u, err := url.Parse(aud) if err != nil { - return "", err + return "", "", err } if u.Path == audiencePath { parts := strings.Split(u.Host, ".") - return parts[0], nil + return parts[0], u.Host, nil } } - return "", fmt.Errorf("audience not found for %s", audiencePath) + return "", "", fmt.Errorf("audience not found for %s", audiencePath) } From d5ccd73646cbbc454c0ae8fd2bd6fd23f0d4e119 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 19:48:29 -0800 Subject: [PATCH 03/10] Ensure init is called in all Renderer methods --- internal/display/display.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/display/display.go b/internal/display/display.go index aebccf155..92a14d7b1 100644 --- a/internal/display/display.go +++ b/internal/display/display.go @@ -68,6 +68,8 @@ func (r *Renderer) Errorf(format string, a ...interface{}) { } func (r *Renderer) Heading(text ...string) { + r.init() + fmt.Fprintf(r.MessageWriter, "%s %s\n", ansi.Faint("==="), strings.Join(text, " ")) } @@ -77,6 +79,8 @@ type View interface { } func (r *Renderer) Results(data []View) { + r.init() + if len(data) > 0 { switch r.Format { case OutputFormatJSON: From d8758f53f91e9baaab84ff15712313a69f246223 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 19:48:47 -0800 Subject: [PATCH 04/10] 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. From 1e56c8b03dab551ac64f683573c106efcec4d6f8 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 20:09:23 -0800 Subject: [PATCH 05/10] Add isLoggedIn / errOnce concept --- internal/cli/cli.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 4b72ebfb7..3c9bd5c8a 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -55,17 +55,28 @@ type cli struct { // config state management. initOnce sync.Once + errOnce error path string config config } +// isLoggedIn encodes the domain logic for determining whether or not we're +// logged in. This might check our config storage, or just in memory. +func (c *cli) isLoggedIn() bool { + c.init() + + return c.tenant != "" +} + // 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() + if err := c.init(); err != nil { + return err + } t, err := c.getTenant() if err != nil { @@ -133,11 +144,10 @@ func (c *cli) setTenant(ten tenant) error { } 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 { + if c.errOnce = c.initContext(); c.errOnce != nil { return } c.renderer.Tenant = c.tenant @@ -145,13 +155,15 @@ func (c *cli) init() error { // 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.") + c.errOnce = fmt.Errorf("Invalid format. Use `--format=json` or omit this option to use the default format.") return } c.renderer.Format = display.OutputFormat(format) }) - return err + // Once initialized, we'll keep returning the same err that was + // originally encountered. + return c.errOnce } func (c *cli) initContext() (err error) { From 8b3ae5fb0ef40a321f2d5a056524b0181b2ca2c1 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 20:09:35 -0800 Subject: [PATCH 06/10] Cleanup tenant passing and getLogin helper method --- internal/cli/root.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/cli/root.go b/internal/cli/root.go index 7096c861a..ccfcd6541 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -5,15 +5,11 @@ import ( "os" "github.com/auth0/auth0-cli/internal/display" - "github.com/spf13/afero" "github.com/spf13/cobra" ) // Execute is the primary entrypoint of the CLI app. func Execute() { - // fs is a mock friendly os.File interface. - fs := afero.NewOsFs() - // cfg contains tenant related information, e.g. `travel0-dev`, // `travel0-prod`. some of its information can be sourced via: // 1. env var (e.g. AUTH0_API_KEY) @@ -28,7 +24,7 @@ func Execute() { SilenceUsage: true, SilenceErrors: true, Short: "Command-line tool to interact with Auth0.", - Long: "Command-line tool to interact with Auth0.\n" + getLogin(&fs, cli), + Long: "Command-line tool to interact with Auth0.\n" + getLogin(cli), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // If the user is trying to login, no need to go @@ -46,7 +42,7 @@ func Execute() { rootCmd.SetUsageTemplate(namespaceUsageTemplate()) rootCmd.PersistentFlags().StringVar(&cli.tenant, - "tenant", "", "Specific tenant to use.") + "tenant", cli.config.DefaultTenant, "Specific tenant to use.") rootCmd.PersistentFlags().BoolVar(&cli.verbose, "verbose", false, "Enable verbose mode.") From a201cf377e47f3805b20cd634d2fb3178214d3f2 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 20:09:50 -0800 Subject: [PATCH 07/10] Cleanup getLogin in helper template --- internal/cli/templates.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/internal/cli/templates.go b/internal/cli/templates.go index 9a801e123..c1f9f8779 100644 --- a/internal/cli/templates.go +++ b/internal/cli/templates.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/auth0/auth0-cli/internal/ansi" - "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/crypto/ssh/terminal" @@ -73,25 +72,13 @@ func WrappedNonRequestParamsFlagUsages(cmd *cobra.Command) string { // Private functions // -func getLogin(fs *afero.Fs, cli *cli) string { - // // We're checking against the path because we don't initialize the config - // // at this point of execution. - // path := cfg.GetConfigFolder(os.Getenv("XDG_CONFIG_HOME")) - // file := filepath.Join(path, "config.toml") - - // exists, _ := afero.Exists(*fs, file) - - exists := false - if !exists { - return ` +func getLogin(cli *cli) string { + if !cli.isLoggedIn() { + return ansi.Italic(` Before using the CLI, you'll need to login: $ auth0 login - -If you're working with multiple tenants, you can run -the login command with the --tenant and --region flag: - - $ auth0 login --tenant travel0 --region us` +`) } return "" From c0edbaf6dae39c3f862987fa3f0f5ca5f7e05a19 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 22 Jan 2021 20:16:09 -0800 Subject: [PATCH 08/10] Lint --- internal/cli/cli.go | 15 ++++++++++----- internal/cli/login.go | 4 +--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 3c9bd5c8a..c0c71d7d9 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -28,7 +28,7 @@ type tenant struct { Name string `json:"name"` Domain string `json:"domain"` AccessToken string `json:"access_token,omitempty"` - ExpiresAt time.Time `json:expires_at"` + ExpiresAt time.Time `json:"expires_at"` } // cli provides all the foundational things for all the commands in the CLI, @@ -63,7 +63,8 @@ type cli struct { // isLoggedIn encodes the domain logic for determining whether or not we're // logged in. This might check our config storage, or just in memory. func (c *cli) isLoggedIn() bool { - c.init() + // No need to check errors for initializing context. + _ = c.init() return c.tenant != "" } @@ -89,7 +90,7 @@ func (c *cli) setup() error { management.WithDebug(c.verbose)) } - return nil + return err } // getTenant fetches the default tenant configured (or the tenant specified via @@ -110,7 +111,9 @@ func (c *cli) getTenant() (tenant, error) { // 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() + // init will fail here with a `no tenant found` error if we're logging + // in for the first time and that's expected. + _ = c.init() // If there's no existing DefaultTenant yet, might as well set the // first successfully logged in tenant during onboarding. @@ -128,7 +131,9 @@ func (c *cli) setTenant(ten tenant) error { dir := filepath.Dir(c.path) if _, err := os.Stat(dir); os.IsNotExist(err) { - os.MkdirAll(dir, 0700) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } } buf, err := json.MarshalIndent(c.config, "", " ") diff --git a/internal/cli/login.go b/internal/cli/login.go index 46512b6ee..d6f25d8c3 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -46,7 +46,7 @@ func loginCmd(cli *cli) *cobra.Command { cli.renderer.Infof("Successfully logged in.") cli.renderer.Infof("Tenant: %s", res.Tenant) - cli.setTenant(tenant{ + return cli.setTenant(tenant{ Name: res.Tenant, Domain: res.Domain, AccessToken: res.AccessToken, @@ -54,8 +54,6 @@ func loginCmd(cli *cli) *cobra.Command { time.Duration(res.ExpiresIn) * time.Second, ), }) - - return nil }, } From a15c820d18ecc26dc17e20cd2b373f9197edd4ba Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 25 Jan 2021 09:13:47 -0800 Subject: [PATCH 09/10] Rename to addTenant --- internal/cli/cli.go | 4 ++-- internal/cli/login.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index c0c71d7d9..2cb4c5d1f 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -108,9 +108,9 @@ func (c *cli) getTenant() (tenant, error) { return t, nil } -// setTenant assigns an existing, or new tenant. This is expected to be called +// addTenant assigns an existing, or new tenant. This is expected to be called // after a login has completed. -func (c *cli) setTenant(ten tenant) error { +func (c *cli) addTenant(ten tenant) error { // init will fail here with a `no tenant found` error if we're logging // in for the first time and that's expected. _ = c.init() diff --git a/internal/cli/login.go b/internal/cli/login.go index d6f25d8c3..328276b3f 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -46,7 +46,7 @@ func loginCmd(cli *cli) *cobra.Command { cli.renderer.Infof("Successfully logged in.") cli.renderer.Infof("Tenant: %s", res.Tenant) - return cli.setTenant(tenant{ + return cli.addTenant(tenant{ Name: res.Tenant, Domain: res.Domain, AccessToken: res.AccessToken, From 6f91d5414e53e854063e6ffce53a644d470f5a6f Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 25 Jan 2021 09:25:26 -0800 Subject: [PATCH 10/10] Add read:logs scopes --- internal/auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 06465cb5b..131b2d96b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -33,7 +33,7 @@ const ( deviceCodeEndpoint = "https://auth0.auth0.com/oauth/device/code" oauthTokenEndpoint = "https://auth0.auth0.com/oauth/token" // TODO(jfatta) extend the scope as we extend the CLI: - scope = "openid read:roles read:clients" + scope = "openid read:roles read:clients read:logs" audiencePath = "/api/v2/" )