From b4a61a806047100470e6f8fdb08db04ce2316532 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 11:32:10 -0700 Subject: [PATCH 01/17] step 1: restore from our earlier iteration --- internal/auth0/auth0.go | 2 + internal/auth0/rule.go | 25 ++ internal/cli/rules.go | 513 ++++++++++++++++++++++++++++++++++++++ internal/display/rules.go | 47 ++++ 4 files changed, 587 insertions(+) create mode 100644 internal/auth0/rule.go create mode 100644 internal/cli/rules.go create mode 100644 internal/display/rules.go diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 4c69d64d3..5ba7c7201 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -15,6 +15,7 @@ type API struct { Client ClientAPI Log LogAPI ResourceServer ResourceServerAPI + Rule RuleAPI } func NewAPI(m *management.Management) *API { @@ -26,6 +27,7 @@ func NewAPI(m *management.Management) *API { Client: m.Client, Log: m.Log, ResourceServer: m.ResourceServer, + Rule: m.Rule, } } diff --git a/internal/auth0/rule.go b/internal/auth0/rule.go new file mode 100644 index 000000000..de0b48621 --- /dev/null +++ b/internal/auth0/rule.go @@ -0,0 +1,25 @@ +//go:generate mockgen -source=rule.go -destination=rule_mock.go -package=auth0 + +package auth0 + +import "gopkg.in/auth0.v5/management" + +type RuleAPI interface { + // Create a new rule. + // + // Note: Changing a rule's stage of execution from the default `login_success` + // can change the rule's function signature to have user omitted. + Create(r *management.Rule, opts ...management.RequestOption) error + + // Retrieve rule details. Accepts a list of fields to include or exclude in the result. + Read(id string, opts ...management.RequestOption) (r *management.Rule, err error) + + // Update an existing rule. + Update(id string, r *management.Rule, opts ...management.RequestOption) error + + // Delete a rule. + Delete(id string, opts ...management.RequestOption) error + + // List all rules. + List(opts ...management.RequestOption) (r *management.RuleList, err error) +} diff --git a/internal/cli/rules.go b/internal/cli/rules.go new file mode 100644 index 000000000..05e22e3a1 --- /dev/null +++ b/internal/cli/rules.go @@ -0,0 +1,513 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "os/user" + "regexp" + "strings" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5/management" +) + +const ( + ruleID = "id" + ruleName = "name" + ruleScript = "script" + ruleOrder = "order" + ruleEnabled = "enabled" +) + +func rulesCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "rules", + Short: "Manage rules for clients", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listRulesCmd(cli)) + cmd.AddCommand(enableRuleCmd(cli)) + cmd.AddCommand(disableRuleCmd(cli)) + cmd.AddCommand(createRulesCmd(cli)) + cmd.AddCommand(deleteRulesCmd(cli)) + cmd.AddCommand(updateRulesCmd(cli)) + + return cmd +} + +func listRulesCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List your rules", + Long: `List the rules in your current tenant.`, + RunE: func(cmd *cobra.Command, args []string) error { + var rules *management.RuleList + err := ansi.Spinner("Loading rules", func() error { + var err error + rules, err = getRules(cli) + return err + }) + + if err != nil { + return err + } + + cli.renderer.RulesList(rules) + return nil + }, + } + + return cmd +} + +func enableRuleCmd(cli *cli) *cobra.Command { + var flags struct { + Name string + } + + cmd := &cobra.Command{ + Use: "enable", + Short: "Enable a rule", + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, ruleName) { + input := prompt.TextInput(ruleName, "Name:", "Name of the rule.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + err := ansi.Spinner("Enabling rule", func() error { + var err error + data, err := getRules(cli) + if err != nil { + return err + } + + rule := findRuleByName(flags.Name, data.Rules) + if rule != nil { + err := enableRule(rule, cli) + if err != nil { + return err + } + } else { + return fmt.Errorf("No rule found with name: %q", flags.Name) + } + return nil + }) + + if err != nil { + return err + } + + // @TODO Only display modified rules + rules, err := getRules(cli) + + if err != nil { + return err + } + + cli.renderer.RulesList(rules) + + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of the rule.") + mustRequireFlags(cmd, ruleName) + + return cmd +} + +func disableRuleCmd(cli *cli) *cobra.Command { + var flags struct { + Name string + } + + cmd := &cobra.Command{ + Use: "disable", + Short: "Disable a rule", + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, ruleName) { + input := prompt.TextInput(ruleName, "Name:", "Name of the rule.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + err := ansi.Spinner("Disabling rule", func() error { + var err error + data, err := getRules(cli) + if err != nil { + return err + } + + rule := findRuleByName(flags.Name, data.Rules) + if rule != nil { + if err := disableRule(rule, cli); err != nil { + return err + } + } else { + return fmt.Errorf("No rule found with name: %q", flags.Name) + } + return nil + }) + + if err != nil { + return err + } + + // @TODO Only display modified rules + rules, err := getRules(cli) + + if err != nil { + return err + } + + cli.renderer.RulesList(rules) + + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "rule name") + mustRequireFlags(cmd, ruleName) + + return cmd +} + +func createRulesCmd(cli *cli) *cobra.Command { + var flags struct { + Name string + Script string + Order int + Enabled bool + } + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new rule", + Long: `Create a new rule: + + auth0 rules create --name "My Rule" --script "function (user, context, callback) { console.log( 'Hello, world!' ); return callback(null, user, context); }" + `, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, ruleName) { + input := prompt.TextInput( + "name", "Name:", + "Name of the rule. You can change the rule name later in the rule settings.", + true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleScript) { + input := prompt.TextInput(ruleScript, "Script:", "Script of the rule.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleOrder) { + input := prompt.TextInputDefault(ruleOrder, "Order:", "Order of the rule.", "0", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleEnabled) { + input := prompt.BoolInput(ruleEnabled, "Enabled:", "Enable the rule.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if strings.Contains(flags.Script, ".js") { + content, err := parseFileByName(flags.Script) + + if err != nil { + return err + } + + flags.Script = content + } + + r := &management.Rule{ + Name: &flags.Name, + Script: &flags.Script, + Order: &flags.Order, + Enabled: &flags.Enabled, + } + + err := ansi.Spinner("Creating rule", func() error { + return cli.api.Rule.Create(r) + }) + + if err != nil { + return err + } + + cli.renderer.Infof("Your rule `%s` was successfully created.", flags.Name) + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of this rule (required)") + cmd.Flags().StringVarP(&flags.Script, ruleScript, "s", "", "Code to be executed when this rule runs (required)") + cmd.Flags().IntVarP(&flags.Order, ruleOrder, "o", 0, "Order that this rule should execute in relative to other rules. Lower-valued rules execute first.") + cmd.Flags().BoolVarP(&flags.Enabled, ruleEnabled, "e", false, "Whether the rule is enabled (true), or disabled (false).") + mustRequireFlags(cmd, ruleName, ruleScript) + + return cmd +} + +func deleteRulesCmd(cli *cli) *cobra.Command { + var flags struct { + ID string + Name string + } + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a rule", + Long: `Delete a rule: + + auth0 rules delete --id "12345"`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if flags.ID != "" && flags.Name != "" { + return fmt.Errorf("TMI! 🤯 use either --name or --id") + } + + prepareInteractivity(cmd) + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, ruleID) && flags.Name == "" { + input := prompt.TextInput(ruleID, "Id:", "Id of the rule to delete.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleName) && flags.ID == "" { + input := prompt.TextInput(ruleName, "Name:", "Name of the rule to delete.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + var r *management.Rule + ruleIDPattern := "^rul_[A-Za-z0-9]{16}$" + re := regexp.MustCompile(ruleIDPattern) + + if flags.ID != "" { + if !re.Match([]byte(flags.ID)) { + return fmt.Errorf("Rule with id %q does not match pattern %s", flags.ID, ruleIDPattern) + } + + rule, err := cli.api.Rule.Read(flags.ID) + if err != nil { + return err + } + r = rule + } else { + data, err := getRules(cli) + if err != nil { + return err + } + if rule := findRuleByName(flags.Name, data.Rules); rule != nil { + r = rule + } else { + return fmt.Errorf("No rule found with name: %q", flags.Name) + } + } + + err := ansi.Spinner("Deleting rule", func() error { + return cli.api.Rule.Delete(*r.ID) + }) + + if err != nil { + return err + } + + return nil + }, + } + + cmd.Flags().StringVarP(&flags.ID, ruleID, "i", "", "ID of the rule to delete.") + cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of the rule to delete.") + + return cmd +} + +func updateRulesCmd(cli *cli) *cobra.Command { + var flags struct { + ID string + Name string + Script string + Order int + Enabled bool + } + + cmd := &cobra.Command{ + Use: "update", + Short: "update a rule", + Long: `Update a rule: + + auth0 rules update --id "12345" --name "My Updated Rule" --script "function (user, context, callback) { console.log( 'Hello, world!' ); return callback(null, user, context); }" --order 1 --enabled true + `, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, ruleID) { + input := prompt.TextInput(ruleID, "Id:", "Id of the rule.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleName) { + input := prompt.TextInput( + "name", "Name:", + "Name of the rule. You can change the rule name later in the rule settings.", + true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleScript) { + input := prompt.TextInput(ruleScript, "Script:", "Script of the rule.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleOrder) { + input := prompt.TextInputDefault(ruleOrder, "Order:", "Order of the rule.", "0", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, ruleEnabled) { + input := prompt.BoolInput(ruleEnabled, "Enabled:", "Enable the rule.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if strings.Contains(flags.Script, ".js") { + content, err := parseFileByName(flags.Script) + + if err != nil { + return err + } + + flags.Script = content + } + + r := &management.Rule{ + Name: &flags.Name, + Script: &flags.Script, + Order: &flags.Order, + Enabled: &flags.Enabled, + } + + err := ansi.Spinner("Updating rule", func() error { + return cli.api.Rule.Update(flags.ID, r) + }) + + if err != nil { + return err + } + + cli.renderer.Infof("Your rule `%s` was successfully updated.", flags.Name) + return nil + }, + } + + cmd.Flags().StringVarP(&flags.ID, ruleID, "i", "", "ID of the rule to update (required)") + cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of this rule") + cmd.Flags().StringVarP(&flags.Script, ruleScript, "s", "", "Code to be executed when this rule runs") + cmd.Flags().IntVarP(&flags.Order, ruleOrder, "o", 0, "Order that this rule should execute in relative to other rules. Lower-valued rules execute first.") + cmd.Flags().BoolVarP(&flags.Enabled, ruleEnabled, "e", false, "Whether the rule is enabled (true), or disabled (false).") + mustRequireFlags(cmd, ruleID) + + return cmd +} + +// @TODO move to rules package +func getRules(cli *cli) (list *management.RuleList, err error) { + return cli.api.Rule.List() +} + +func findRuleByName(name string, rules []*management.Rule) *management.Rule { + for _, r := range rules { + if auth0.StringValue(r.Name) == name { + return r + } + } + return nil +} + +func enableRule(rule *management.Rule, cli *cli) error { + return cli.api.Rule.Update(rule.GetID(), &management.Rule{Enabled: auth0.Bool(true)}) +} + +func disableRule(rule *management.Rule, cli *cli) error { + return cli.api.Rule.Update(rule.GetID(), &management.Rule{Enabled: auth0.Bool(false)}) +} + +func parseFileByName(inputFile string) (string, error) { + usr, _ := user.Current() + inputFile = strings.Replace(inputFile, "~/", usr.HomeDir+"/", -1) + f, err := ioutil.ReadFile(inputFile) + + if err != nil { + return "", fmt.Errorf("Error reading file: %s", err) + } + + if err != nil { + return "", fmt.Errorf("Cannot parse file %s: %w", f, err) + } + + if err != nil { + return "", err + } + + return string(f), nil +} diff --git a/internal/display/rules.go b/internal/display/rules.go new file mode 100644 index 000000000..31bebf4e1 --- /dev/null +++ b/internal/display/rules.go @@ -0,0 +1,47 @@ +package display + +import ( + "fmt" + "sort" + "strconv" + + "github.com/auth0/auth0-cli/internal/ansi" + "gopkg.in/auth0.v5/management" +) + +type ruleView struct { + Name string + Enabled bool + ID string + Order int +} + +func (v *ruleView) AsTableHeader() []string { + return []string{"Id", "Name", "Enabled", "Order"} +} + +func (v *ruleView) AsTableRow() []string { + return []string{v.ID, v.Name, strconv.FormatBool(v.Enabled), fmt.Sprintf("%d", v.Order)} +} + +func (r *Renderer) RulesList(ruleList *management.RuleList) { + r.Heading(ansi.Bold(r.Tenant), "rules\n") + var res []View + + //@TODO Provide sort options via flags + sort.Slice(ruleList.Rules, func(i, j int) bool { + return ruleList.Rules[i].GetOrder() < ruleList.Rules[j].GetOrder() + }) + + for _, rule := range ruleList.Rules { + res = append(res, &ruleView{ + Name: *rule.Name, + ID: *rule.ID, + Enabled: *rule.Enabled, + Order: *rule.Order, + }) + } + + r.Results(res) + +} From d574f1a3bdd2a1451185fac5544ded546264032a Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 12:21:19 -0700 Subject: [PATCH 02/17] Very rough first pass on create --- internal/auth/auth.go | 1 + internal/auth/auth_test.go | 1 + internal/cli/cli.go | 8 + internal/cli/data/rule-template-empty-rule.js | 3 + internal/cli/flags.go | 6 + internal/cli/root.go | 1 + internal/cli/rules.go | 137 +++++++++--------- internal/cli/rules_embed.go | 10 ++ internal/display/rules.go | 44 ++++++ 9 files changed, 139 insertions(+), 72 deletions(-) create mode 100644 internal/cli/data/rule-template-empty-rule.js create mode 100644 internal/cli/rules_embed.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index a7e5543e5..a85c4426e 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -27,6 +27,7 @@ var requiredScopes = []string{ "offline_access", // <-- to get a refresh token. "create:clients", "delete:clients", "read:clients", "update:clients", "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", + "create:rules", "delete:rules", "read:rules", "update:rules", "read:client_keys", "read:logs", } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 7e50244da..b05e91310 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -7,6 +7,7 @@ func TestRequiredScopes(t *testing.T) { crudResources := []string{ "clients", "resource_servers", + "rules", } crudPrefixes := []string{"create:", "delete:", "read:", "update:"} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 9cf7221ea..fab6bc684 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -381,3 +381,11 @@ func prepareInteractivity(cmd *cobra.Command) { }) } } + +func flagOptionsForMapping(mapping map[string]string) []string { + result := make([]string, 0, len(mapping)) + for k := range mapping { + result = append(result, k) + } + return result +} diff --git a/internal/cli/data/rule-template-empty-rule.js b/internal/cli/data/rule-template-empty-rule.js new file mode 100644 index 000000000..abcc2116a --- /dev/null +++ b/internal/cli/data/rule-template-empty-rule.js @@ -0,0 +1,3 @@ +function(user, context, cb) { + cb(null, user, context); +} diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 162219f9f..58b230a5f 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -78,6 +78,7 @@ func (f *Flag) RegisterBoolU(cmd *cobra.Command, value *bool, defaultValue bool) registerBool(cmd, f, value, defaultValue, true) } +// LOOK func askFlag(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) error { if shouldAsk(cmd, f, isUpdate) { return ask(cmd, f, value, isUpdate) @@ -86,6 +87,11 @@ func askFlag(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) erro return nil } +// LOOK +func (f *Flag) label() string { + return fmt.Sprintf("%s:", f.Name) +} + func selectFlag(cmd *cobra.Command, f *Flag, value interface{}, options []string, isUpdate bool) error { if shouldAsk(cmd, f, isUpdate) { return _select(cmd, f, value, options, isUpdate) diff --git a/internal/cli/root.go b/internal/cli/root.go index 30910be74..a21b8df22 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -80,6 +80,7 @@ func Execute() { rootCmd.AddCommand(loginCmd(cli)) rootCmd.AddCommand(tenantsCmd(cli)) rootCmd.AddCommand(appsCmd(cli)) + rootCmd.AddCommand(rulesCmd(cli)) rootCmd.AddCommand(quickstartsCmd(cli)) rootCmd.AddCommand(apisCmd(cli)) rootCmd.AddCommand(testCmd(cli)) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 05e22e3a1..12b155d2c 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -4,22 +4,47 @@ import ( "fmt" "io/ioutil" "os/user" - "regexp" "strings" "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" - "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" "gopkg.in/auth0.v5/management" ) const ( - ruleID = "id" - ruleName = "name" - ruleScript = "script" - ruleOrder = "order" - ruleEnabled = "enabled" + ruleID = "id" + ruleScript = "script" +) + +var ( + ruleName = Flag{ + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the rule.", + IsRequired: true, + } + + ruleTemplate = Flag{ + Name: "Template", + LongForm: "template", + ShortForm: "t", + Help: "Template to use for the rule.", + } + + ruleTemplateOptions = flagOptionsForMapping(ruleTemplateMappings) + + ruleEnabled = Flag{ + Name: "Enabled", + LongForm: "enabled", + ShortForm: "e", + Help: "Enable (or disable) a rule.", + } + + ruleTemplateMappings = map[string]string{ + "Empty Rule": ruleTemplateEmptyRule, + } ) func rulesCmd(cli *cli) *cobra.Command { @@ -30,11 +55,11 @@ func rulesCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(listRulesCmd(cli)) - cmd.AddCommand(enableRuleCmd(cli)) - cmd.AddCommand(disableRuleCmd(cli)) - cmd.AddCommand(createRulesCmd(cli)) - cmd.AddCommand(deleteRulesCmd(cli)) - cmd.AddCommand(updateRulesCmd(cli)) + // cmd.AddCommand(enableRuleCmd(cli)) + // cmd.AddCommand(disableRuleCmd(cli)) + cmd.AddCommand(createRuleCmd(cli)) + // cmd.AddCommand(deleteRuleCmd(cli)) + // cmd.AddCommand(updateRuleCmd(cli)) return cmd } @@ -64,6 +89,7 @@ func listRulesCmd(cli *cli) *cobra.Command { return cmd } +/* func enableRuleCmd(cli *cli) *cobra.Command { var flags struct { Name string @@ -125,7 +151,9 @@ func enableRuleCmd(cli *cli) *cobra.Command { return cmd } +*/ +/* func disableRuleCmd(cli *cli) *cobra.Command { var flags struct { Name string @@ -186,13 +214,14 @@ func disableRuleCmd(cli *cli) *cobra.Command { return cmd } +*/ -func createRulesCmd(cli *cli) *cobra.Command { +func createRuleCmd(cli *cli) *cobra.Command { var flags struct { - Name string - Script string - Order int - Enabled bool + Name string + Template string + Enabled bool + // Order int } cmd := &cobra.Command{ @@ -200,87 +229,48 @@ func createRulesCmd(cli *cli) *cobra.Command { Short: "Create a new rule", Long: `Create a new rule: - auth0 rules create --name "My Rule" --script "function (user, context, callback) { console.log( 'Hello, world!' ); return callback(null, user, context); }" +auth0 rules create --name "My Rule" --template [empty-rule]" `, PreRun: func(cmd *cobra.Command, args []string) { prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if shouldPrompt(cmd, ruleName) { - input := prompt.TextInput( - "name", "Name:", - "Name of the rule. You can change the rule name later in the rule settings.", - true) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if shouldPrompt(cmd, ruleScript) { - input := prompt.TextInput(ruleScript, "Script:", "Script of the rule.", true) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if shouldPrompt(cmd, ruleOrder) { - input := prompt.TextInputDefault(ruleOrder, "Order:", "Order of the rule.", "0", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if shouldPrompt(cmd, ruleEnabled) { - input := prompt.BoolInput(ruleEnabled, "Enabled:", "Enable the rule.", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } + if err := ruleName.Ask(cmd, &flags.Name); err != nil { + return err } - if strings.Contains(flags.Script, ".js") { - content, err := parseFileByName(flags.Script) - - if err != nil { - return err - } - - flags.Script = content + if err := ruleTemplate.Select(cmd, &flags.Template, ruleTemplateOptions); err != nil { + return err } - r := &management.Rule{ + rule := &management.Rule{ Name: &flags.Name, - Script: &flags.Script, - Order: &flags.Order, + Script: auth0.String(ruleTemplateMappings[flags.Template]), Enabled: &flags.Enabled, } err := ansi.Spinner("Creating rule", func() error { - return cli.api.Rule.Create(r) + return cli.api.Rule.Create(rule) }) if err != nil { - return err + return fmt.Errorf("Unable to create rule: %w", err) } - cli.renderer.Infof("Your rule `%s` was successfully created.", flags.Name) + cli.renderer.RulesCreate(rule) return nil }, } - cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of this rule (required)") - cmd.Flags().StringVarP(&flags.Script, ruleScript, "s", "", "Code to be executed when this rule runs (required)") - cmd.Flags().IntVarP(&flags.Order, ruleOrder, "o", 0, "Order that this rule should execute in relative to other rules. Lower-valued rules execute first.") - cmd.Flags().BoolVarP(&flags.Enabled, ruleEnabled, "e", false, "Whether the rule is enabled (true), or disabled (false).") - mustRequireFlags(cmd, ruleName, ruleScript) + ruleName.RegisterString(cmd, &flags.Name, "") + ruleTemplate.RegisterString(cmd, &flags.Template, "") + ruleEnabled.RegisterBool(cmd, &flags.Enabled, true) return cmd } -func deleteRulesCmd(cli *cli) *cobra.Command { +/* +func deleteRuleCmd(cli *cli) *cobra.Command { var flags struct { ID string Name string @@ -366,8 +356,10 @@ func deleteRulesCmd(cli *cli) *cobra.Command { return cmd } +*/ -func updateRulesCmd(cli *cli) *cobra.Command { +/* +func updateRuleCmd(cli *cli) *cobra.Command { var flags struct { ID string Name string @@ -469,6 +461,7 @@ func updateRulesCmd(cli *cli) *cobra.Command { return cmd } +*/ // @TODO move to rules package func getRules(cli *cli) (list *management.RuleList, err error) { diff --git a/internal/cli/rules_embed.go b/internal/cli/rules_embed.go new file mode 100644 index 000000000..584aee3a5 --- /dev/null +++ b/internal/cli/rules_embed.go @@ -0,0 +1,10 @@ +package cli + +import ( + _ "embed" +) + +var ( + //go:embed data/rule-template-empty-rule.js + ruleTemplateEmptyRule string +) diff --git a/internal/display/rules.go b/internal/display/rules.go index 31bebf4e1..0f23d171d 100644 --- a/internal/display/rules.go +++ b/internal/display/rules.go @@ -14,6 +14,9 @@ type ruleView struct { Enabled bool ID string Order int + Script string + + raw interface{} } func (v *ruleView) AsTableHeader() []string { @@ -24,6 +27,19 @@ func (v *ruleView) AsTableRow() []string { return []string{v.ID, v.Name, strconv.FormatBool(v.Enabled), fmt.Sprintf("%d", v.Order)} } +func (v *ruleView) KeyValues() [][]string { + return [][]string{ + []string{"NAME", v.Name}, + []string{"ID", v.ID}, + []string{"ENABLED", strconv.FormatBool(v.Enabled)}, + []string{"SCRIPT", v.Script}, + } +} + +func (v *ruleView) Object() interface{} { + return v.raw +} + func (r *Renderer) RulesList(ruleList *management.RuleList) { r.Heading(ansi.Bold(r.Tenant), "rules\n") var res []View @@ -45,3 +61,31 @@ func (r *Renderer) RulesList(ruleList *management.RuleList) { r.Results(res) } + +func (r *Renderer) RulesCreate(rule *management.Rule) { + r.Heading(ansi.Bold(r.Tenant), "rule created\n") + + v := &ruleView{ + Name: rule.GetName(), + ID: rule.GetID(), + Enabled: rule.GetEnabled(), + Order: rule.GetOrder(), + Script: rule.GetScript(), + + raw: rule, + } + + r.Result(v) + + r.Newline() + + // TODO(cyx): possibly guard this with a --no-hint flag. + r.Infof("%s: To edit this rule, do `auth0 rules update %s`", + ansi.Faint("Hint"), + rule.GetID(), + ) + + r.Infof("%s: You might wanna try `auth0 test login", + ansi.Faint("Hint"), + ) +} From e953448dfce4760b8060f3cf815742af953e3540 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 12:32:46 -0700 Subject: [PATCH 03/17] Better name --- internal/cli/cli.go | 2 +- internal/cli/rules.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index fab6bc684..8d768626e 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -382,7 +382,7 @@ func prepareInteractivity(cmd *cobra.Command) { } } -func flagOptionsForMapping(mapping map[string]string) []string { +func flagOptionsFromMapping(mapping map[string]string) []string { result := make([]string, 0, len(mapping)) for k := range mapping { result = append(result, k) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 12b155d2c..995f4c7bd 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -33,7 +33,7 @@ var ( Help: "Template to use for the rule.", } - ruleTemplateOptions = flagOptionsForMapping(ruleTemplateMappings) + ruleTemplateOptions = flagOptionsFromMapping(ruleTemplateMappings) ruleEnabled = Flag{ Name: "Enabled", From 91c43e098feb9f4427401e270f5a6cd65506f4ce Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 14:23:32 -0700 Subject: [PATCH 04/17] Wrap up editor prompt / rules create flow --- internal/cli/rules.go | 13 ++++- internal/prompt/editor.go | 106 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 internal/prompt/editor.go diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 995f4c7bd..5d3736e30 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -8,6 +8,7 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" "gopkg.in/auth0.v5/management" ) @@ -243,13 +244,21 @@ auth0 rules create --name "My Rule" --template [empty-rule]" return err } + script, err := prompt.CaptureInputViaEditor( + ruleTemplateMappings[flags.Template], + flags.Name+".*.js", + ) + if err != nil { + return fmt.Errorf("Failed to capture input from the editor: %w", err) + } + rule := &management.Rule{ Name: &flags.Name, - Script: auth0.String(ruleTemplateMappings[flags.Template]), + Script: auth0.String(script), Enabled: &flags.Enabled, } - err := ansi.Spinner("Creating rule", func() error { + err = ansi.Spinner("Creating rule", func() error { return cli.api.Rule.Create(rule) }) diff --git a/internal/prompt/editor.go b/internal/prompt/editor.go new file mode 100644 index 000000000..a063c0969 --- /dev/null +++ b/internal/prompt/editor.go @@ -0,0 +1,106 @@ +package prompt + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +const defaultEditor = "vim" + +var defaultEditorPrompt = &editorPrompt{defaultEditor: defaultEditor} + +// CaptureInputViaEditor is the high level function to use in this package in +// order to capture input from an editor. +// +// The arguments have been tailored for our use of strings mostly in the rest +// of the CLI even though internally we're using []byte. +func CaptureInputViaEditor(contents, pattern string) (result string, err error) { + v, err := defaultEditorPrompt.captureInput([]byte(contents), pattern) + return string(v), err +} + +type editorPrompt struct { + defaultEditor string +} + +// GetPreferredEditorFromEnvironment returns the user's editor as defined by the +// `$EDITOR` environment variable, or the `defaultEditor` if it is not set. +func (p *editorPrompt) getPreferredEditor() string { + editor := os.Getenv("EDITOR") + + if editor == "" { + return p.defaultEditor + } + + return editor +} + +func (p *editorPrompt) resolveEditorArguments(executable string, filename string) []string { + args := []string{filename} + + if strings.Contains(executable, "Visual Studio Code.app") { + args = append([]string{"--wait"}, args...) + } + + // TODO(cyx): add other common editors + + return args +} + +// openFile opens filename in the preferred text editor, resolving the +// arguments with editor specific logic. +func (p *editorPrompt) openFile(filename string) error { + // Get the full executable path for the editor. + executable, err := exec.LookPath(p.getPreferredEditor()) + if err != nil { + return err + } + + cmd := exec.Command(executable, p.resolveEditorArguments(executable, filename)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// captureInput opens a temporary file in a text editor and returns +// the written bytes on success or an error on failure. It handles deletion +// of the temporary file behind the scenes. +// +// If given default contents, it will write that to the file before popping +// open the editor. +func (p *editorPrompt) captureInput(contents []byte, pattern string) ([]byte, error) { + file, err := os.CreateTemp(os.TempDir(), pattern) + if err != nil { + return []byte{}, err + } + + filename := file.Name() + + if len(contents) > 0 { + if err := os.WriteFile(filename, contents, 0644); err != nil { + return nil, fmt.Errorf("Failed to write to file: %w", err) + } + } + + // Defer removal of the temporary file in case any of the next steps fail. + defer os.Remove(filename) + + if err = file.Close(); err != nil { + return nil, err + } + + if err = p.openFile(filename); err != nil { + return nil, err + } + + bytes, err := os.ReadFile(filename) + if err != nil { + return []byte{}, err + } + + return bytes, nil +} From b9673e75b8c9125c5fb38021626420f618320514 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 15:58:41 -0700 Subject: [PATCH 05/17] Rules: CRUD is done --- internal/cli/rules.go | 251 +++++++++++++++----------------------- internal/display/rules.go | 8 +- 2 files changed, 100 insertions(+), 159 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 5d3736e30..0cbef7b87 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -2,9 +2,6 @@ package cli import ( "fmt" - "io/ioutil" - "os/user" - "strings" "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" @@ -19,7 +16,7 @@ const ( ) var ( - ruleName = Flag{ + ruleNameRequired = Flag{ Name: "Name", LongForm: "name", ShortForm: "n", @@ -27,6 +24,13 @@ var ( IsRequired: true, } + ruleName = Flag{ + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the rule.", + } + ruleTemplate = Flag{ Name: "Template", LongForm: "template", @@ -56,11 +60,9 @@ func rulesCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(listRulesCmd(cli)) - // cmd.AddCommand(enableRuleCmd(cli)) - // cmd.AddCommand(disableRuleCmd(cli)) cmd.AddCommand(createRuleCmd(cli)) - // cmd.AddCommand(deleteRuleCmd(cli)) - // cmd.AddCommand(updateRuleCmd(cli)) + cmd.AddCommand(deleteRuleCmd(cli)) + cmd.AddCommand(updateRuleCmd(cli)) return cmd } @@ -71,7 +73,7 @@ func listRulesCmd(cli *cli) *cobra.Command { Short: "List your rules", Long: `List the rules in your current tenant.`, RunE: func(cmd *cobra.Command, args []string) error { - var rules *management.RuleList + var rules []*management.Rule err := ansi.Spinner("Loading rules", func() error { var err error rules, err = getRules(cli) @@ -222,7 +224,6 @@ func createRuleCmd(cli *cli) *cobra.Command { Name string Template string Enabled bool - // Order int } cmd := &cobra.Command{ @@ -236,7 +237,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if err := ruleName.Ask(cmd, &flags.Name); err != nil { + if err := ruleNameRequired.Ask(cmd, &flags.Name); err != nil { return err } @@ -244,6 +245,9 @@ auth0 rules create --name "My Rule" --template [empty-rule]" return err } + // TODO(cyx): we can re-think this once we have + // `--stdin` based commands. For now we don't have + // those yet, so keeping this simple. script, err := prompt.CaptureInputViaEditor( ruleTemplateMappings[flags.Template], flags.Name+".*.js", @@ -271,18 +275,16 @@ auth0 rules create --name "My Rule" --template [empty-rule]" }, } - ruleName.RegisterString(cmd, &flags.Name, "") + ruleNameRequired.RegisterString(cmd, &flags.Name, "") ruleTemplate.RegisterString(cmd, &flags.Template, "") ruleEnabled.RegisterBool(cmd, &flags.Enabled, true) return cmd } -/* func deleteRuleCmd(cli *cli) *cobra.Command { - var flags struct { - ID string - Name string + var inputs struct { + ID string } cmd := &cobra.Command{ @@ -290,66 +292,28 @@ func deleteRuleCmd(cli *cli) *cobra.Command { Short: "Delete a rule", Long: `Delete a rule: - auth0 rules delete --id "12345"`, - PreRunE: func(cmd *cobra.Command, args []string) error { - if flags.ID != "" && flags.Name != "" { - return fmt.Errorf("TMI! 🤯 use either --name or --id") - } - +auth0 rules delete rul_d2VSaGlyaW5n`, + PreRun: func(cmd *cobra.Command, args []string) { prepareInteractivity(cmd) - return nil }, RunE: func(cmd *cobra.Command, args []string) error { - if shouldPrompt(cmd, ruleID) && flags.Name == "" { - input := prompt.TextInput(ruleID, "Id:", "Id of the rule to delete.", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if shouldPrompt(cmd, ruleName) && flags.ID == "" { - input := prompt.TextInput(ruleName, "Name:", "Name of the rule to delete.", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if !cli.force && canPrompt(cmd) { - if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { - return nil - } - } - - var r *management.Rule - ruleIDPattern := "^rul_[A-Za-z0-9]{16}$" - re := regexp.MustCompile(ruleIDPattern) - - if flags.ID != "" { - if !re.Match([]byte(flags.ID)) { - return fmt.Errorf("Rule with id %q does not match pattern %s", flags.ID, ruleIDPattern) - } - - rule, err := cli.api.Rule.Read(flags.ID) - if err != nil { - return err - } - r = rule + if len(args) > 0 { + inputs.ID = args[0] } else { - data, err := getRules(cli) + var err error + inputs.ID, err = promptForRuleViaDropdown(cli, cmd) if err != nil { return err } - if rule := findRuleByName(flags.Name, data.Rules); rule != nil { - r = rule - } else { - return fmt.Errorf("No rule found with name: %q", flags.Name) + + if inputs.ID == "" { + cli.renderer.Infof("There are currently no rules.") + return nil } } err := ansi.Spinner("Deleting rule", func() error { - return cli.api.Rule.Delete(*r.ID) + return cli.api.Rule.Delete(inputs.ID) }) if err != nil { @@ -360,20 +324,16 @@ func deleteRuleCmd(cli *cli) *cobra.Command { }, } - cmd.Flags().StringVarP(&flags.ID, ruleID, "i", "", "ID of the rule to delete.") - cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of the rule to delete.") - return cmd } -*/ -/* func updateRuleCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + var flags struct { - ID string Name string - Script string - Order int Enabled bool } @@ -382,74 +342,63 @@ func updateRuleCmd(cli *cli) *cobra.Command { Short: "update a rule", Long: `Update a rule: - auth0 rules update --id "12345" --name "My Updated Rule" --script "function (user, context, callback) { console.log( 'Hello, world!' ); return callback(null, user, context); }" --order 1 --enabled true +auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=false `, PreRun: func(cmd *cobra.Command, args []string) { prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if shouldPrompt(cmd, ruleID) { - input := prompt.TextInput(ruleID, "Id:", "Id of the rule.", true) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - if shouldPrompt(cmd, ruleName) { - input := prompt.TextInput( - "name", "Name:", - "Name of the rule. You can change the rule name later in the rule settings.", - true) - - if err := prompt.AskOne(input, &flags); err != nil { + if len(args) > 0 { + inputs.ID = args[0] + } else { + var err error + inputs.ID, err = promptForRuleViaDropdown(cli, cmd) + if err != nil { return err } - } - - if shouldPrompt(cmd, ruleScript) { - input := prompt.TextInput(ruleScript, "Script:", "Script of the rule.", true) - if err := prompt.AskOne(input, &flags); err != nil { - return err + if inputs.ID == "" { + cli.renderer.Infof("There are currently no rules.") + return nil } } - if shouldPrompt(cmd, ruleOrder) { - input := prompt.TextInputDefault(ruleOrder, "Order:", "Order of the rule.", "0", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } + if err := ruleName.AskU(cmd, &flags.Name); err != nil { + return err } - if shouldPrompt(cmd, ruleEnabled) { - input := prompt.BoolInput(ruleEnabled, "Enabled:", "Enable the rule.", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } + var rule *management.Rule + err := ansi.Spinner("Fetching rule", func() error { + var err error + rule, err = cli.api.Rule.Read(inputs.ID) + return err + }) + if err != nil { + return fmt.Errorf("Failed to fetch rule with ID: %s %v", inputs.ID, err) } - if strings.Contains(flags.Script, ".js") { - content, err := parseFileByName(flags.Script) - - if err != nil { - return err - } - - flags.Script = content + // TODO(cyx): we can re-think this once we have + // `--stdin` based commands. For now we don't have + // those yet, so keeping this simple. + script, err := prompt.CaptureInputViaEditor( + rule.GetScript(), + rule.GetName()+".*.js", + ) + if err != nil { + return fmt.Errorf("Failed to capture input from the editor: %w", err) } - r := &management.Rule{ - Name: &flags.Name, - Script: &flags.Script, - Order: &flags.Order, - Enabled: &flags.Enabled, + // Since name is optional, no need to specify what they chose. + if flags.Name == "" { + flags.Name = rule.GetName() } - err := ansi.Spinner("Updating rule", func() error { - return cli.api.Rule.Update(flags.ID, r) + err = ansi.Spinner("Updating rule", func() error { + return cli.api.Rule.Update(inputs.ID, &management.Rule{ + Name: &flags.Name, + Script: &script, + Enabled: &flags.Enabled, + }) }) if err != nil { @@ -461,55 +410,47 @@ func updateRuleCmd(cli *cli) *cobra.Command { }, } - cmd.Flags().StringVarP(&flags.ID, ruleID, "i", "", "ID of the rule to update (required)") - cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of this rule") - cmd.Flags().StringVarP(&flags.Script, ruleScript, "s", "", "Code to be executed when this rule runs") - cmd.Flags().IntVarP(&flags.Order, ruleOrder, "o", 0, "Order that this rule should execute in relative to other rules. Lower-valued rules execute first.") - cmd.Flags().BoolVarP(&flags.Enabled, ruleEnabled, "e", false, "Whether the rule is enabled (true), or disabled (false).") - mustRequireFlags(cmd, ruleID) + ruleName.RegisterStringU(cmd, &flags.Name, "") + ruleEnabled.RegisterBool(cmd, &flags.Enabled, true) return cmd } -*/ // @TODO move to rules package -func getRules(cli *cli) (list *management.RuleList, err error) { - return cli.api.Rule.List() -} - -func findRuleByName(name string, rules []*management.Rule) *management.Rule { - for _, r := range rules { - if auth0.StringValue(r.Name) == name { - return r - } +func getRules(cli *cli) ([]*management.Rule, error) { + list, err := cli.api.Rule.List() + if err != nil { + return nil, err } - return nil + return list.Rules, nil } -func enableRule(rule *management.Rule, cli *cli) error { - return cli.api.Rule.Update(rule.GetID(), &management.Rule{Enabled: auth0.Bool(true)}) -} +func promptForRuleViaDropdown(cli *cli, cmd *cobra.Command) (id string, err error) { + dropdown := Flag{Name: "Rule"} -func disableRule(rule *management.Rule, cli *cli) error { - return cli.api.Rule.Update(rule.GetID(), &management.Rule{Enabled: auth0.Bool(false)}) -} + var rules []*management.Rule -func parseFileByName(inputFile string) (string, error) { - usr, _ := user.Current() - inputFile = strings.Replace(inputFile, "~/", usr.HomeDir+"/", -1) - f, err := ioutil.ReadFile(inputFile) + // == Start experimental dropdown for names => id. + // TODO(cyx): Consider extracting this + // pattern once we've done more of it. + err = ansi.Spinner("Fetching your rules", func() error { + rules, err = getRules(cli) + return err + }) - if err != nil { - return "", fmt.Errorf("Error reading file: %s", err) + if err != nil || len(rules) == 0 { + return "", err } - if err != nil { - return "", fmt.Errorf("Cannot parse file %s: %w", f, err) + mapping := map[string]string{} + for _, r := range rules { + mapping[r.GetName()] = r.GetID() } - if err != nil { + var name string + if err := dropdown.Select(cmd, &name, flagOptionsFromMapping(mapping)); err != nil { return "", err } - return string(f), nil + return mapping[name], nil } diff --git a/internal/display/rules.go b/internal/display/rules.go index 0f23d171d..a94b1dd10 100644 --- a/internal/display/rules.go +++ b/internal/display/rules.go @@ -40,16 +40,16 @@ func (v *ruleView) Object() interface{} { return v.raw } -func (r *Renderer) RulesList(ruleList *management.RuleList) { +func (r *Renderer) RulesList(rules []*management.Rule) { r.Heading(ansi.Bold(r.Tenant), "rules\n") var res []View //@TODO Provide sort options via flags - sort.Slice(ruleList.Rules, func(i, j int) bool { - return ruleList.Rules[i].GetOrder() < ruleList.Rules[j].GetOrder() + sort.Slice(rules, func(i, j int) bool { + return rules[i].GetOrder() < rules[j].GetOrder() }) - for _, rule := range ruleList.Rules { + for _, rule := range rules { res = append(res, &ruleView{ Name: *rule.Name, ID: *rule.ID, From 07e404876b4e6860464e423d684aa160d015ad1d Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 16:10:08 -0700 Subject: [PATCH 06/17] Cleanup --- internal/cli/rules.go | 186 ++++++------------------------------------ 1 file changed, 27 insertions(+), 159 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 0cbef7b87..723ca0798 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -75,60 +75,11 @@ func listRulesCmd(cli *cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { var rules []*management.Rule err := ansi.Spinner("Loading rules", func() error { - var err error - rules, err = getRules(cli) - return err - }) - - if err != nil { - return err - } - - cli.renderer.RulesList(rules) - return nil - }, - } - - return cmd -} - -/* -func enableRuleCmd(cli *cli) *cobra.Command { - var flags struct { - Name string - } - - cmd := &cobra.Command{ - Use: "enable", - Short: "Enable a rule", - PreRun: func(cmd *cobra.Command, args []string) { - prepareInteractivity(cmd) - }, - RunE: func(cmd *cobra.Command, args []string) error { - if shouldPrompt(cmd, ruleName) { - input := prompt.TextInput(ruleName, "Name:", "Name of the rule.", true) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - err := ansi.Spinner("Enabling rule", func() error { - var err error - data, err := getRules(cli) + ruleList, err := cli.api.Rule.List() if err != nil { return err } - - rule := findRuleByName(flags.Name, data.Rules) - if rule != nil { - err := enableRule(rule, cli) - if err != nil { - return err - } - } else { - return fmt.Errorf("No rule found with name: %q", flags.Name) - } + rules = ruleList.Rules return nil }) @@ -136,91 +87,16 @@ func enableRuleCmd(cli *cli) *cobra.Command { return err } - // @TODO Only display modified rules - rules, err := getRules(cli) - - if err != nil { - return err - } - cli.renderer.RulesList(rules) - return nil }, } - cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "Name of the rule.") - mustRequireFlags(cmd, ruleName) - return cmd } -*/ - -/* -func disableRuleCmd(cli *cli) *cobra.Command { - var flags struct { - Name string - } - - cmd := &cobra.Command{ - Use: "disable", - Short: "Disable a rule", - PreRun: func(cmd *cobra.Command, args []string) { - prepareInteractivity(cmd) - }, - RunE: func(cmd *cobra.Command, args []string) error { - if shouldPrompt(cmd, ruleName) { - input := prompt.TextInput(ruleName, "Name:", "Name of the rule.", true) - - if err := prompt.AskOne(input, &flags); err != nil { - return err - } - } - - err := ansi.Spinner("Disabling rule", func() error { - var err error - data, err := getRules(cli) - if err != nil { - return err - } - - rule := findRuleByName(flags.Name, data.Rules) - if rule != nil { - if err := disableRule(rule, cli); err != nil { - return err - } - } else { - return fmt.Errorf("No rule found with name: %q", flags.Name) - } - return nil - }) - - if err != nil { - return err - } - - // @TODO Only display modified rules - rules, err := getRules(cli) - - if err != nil { - return err - } - - cli.renderer.RulesList(rules) - - return nil - }, - } - - cmd.Flags().StringVarP(&flags.Name, ruleName, "n", "", "rule name") - mustRequireFlags(cmd, ruleName) - - return cmd -} -*/ func createRuleCmd(cli *cli) *cobra.Command { - var flags struct { + var inputs struct { Name string Template string Enabled bool @@ -237,11 +113,11 @@ auth0 rules create --name "My Rule" --template [empty-rule]" prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if err := ruleNameRequired.Ask(cmd, &flags.Name); err != nil { + if err := ruleNameRequired.Ask(cmd, &inputs.Name); err != nil { return err } - if err := ruleTemplate.Select(cmd, &flags.Template, ruleTemplateOptions); err != nil { + if err := ruleTemplate.Select(cmd, &inputs.Template, ruleTemplateOptions); err != nil { return err } @@ -249,17 +125,17 @@ auth0 rules create --name "My Rule" --template [empty-rule]" // `--stdin` based commands. For now we don't have // those yet, so keeping this simple. script, err := prompt.CaptureInputViaEditor( - ruleTemplateMappings[flags.Template], - flags.Name+".*.js", + ruleTemplateMappings[inputs.Template], + inputs.Name+".*.js", ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) } rule := &management.Rule{ - Name: &flags.Name, + Name: &inputs.Name, Script: auth0.String(script), - Enabled: &flags.Enabled, + Enabled: &inputs.Enabled, } err = ansi.Spinner("Creating rule", func() error { @@ -275,9 +151,9 @@ auth0 rules create --name "My Rule" --template [empty-rule]" }, } - ruleNameRequired.RegisterString(cmd, &flags.Name, "") - ruleTemplate.RegisterString(cmd, &flags.Template, "") - ruleEnabled.RegisterBool(cmd, &flags.Enabled, true) + ruleNameRequired.RegisterString(cmd, &inputs.Name, "") + ruleTemplate.RegisterString(cmd, &inputs.Template, "") + ruleEnabled.RegisterBool(cmd, &inputs.Enabled, true) return cmd } @@ -329,10 +205,7 @@ auth0 rules delete rul_d2VSaGlyaW5n`, func updateRuleCmd(cli *cli) *cobra.Command { var inputs struct { - ID string - } - - var flags struct { + ID string Name string Enabled bool } @@ -363,7 +236,7 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal } } - if err := ruleName.AskU(cmd, &flags.Name); err != nil { + if err := ruleName.AskU(cmd, &inputs.Name); err != nil { return err } @@ -389,15 +262,15 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal } // Since name is optional, no need to specify what they chose. - if flags.Name == "" { - flags.Name = rule.GetName() + if inputs.Name == "" { + inputs.Name = rule.GetName() } err = ansi.Spinner("Updating rule", func() error { return cli.api.Rule.Update(inputs.ID, &management.Rule{ - Name: &flags.Name, + Name: &inputs.Name, Script: &script, - Enabled: &flags.Enabled, + Enabled: &inputs.Enabled, }) }) @@ -405,26 +278,17 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal return err } - cli.renderer.Infof("Your rule `%s` was successfully updated.", flags.Name) + cli.renderer.Infof("Your rule `%s` was successfully updated.", inputs.Name) return nil }, } - ruleName.RegisterStringU(cmd, &flags.Name, "") - ruleEnabled.RegisterBool(cmd, &flags.Enabled, true) + ruleName.RegisterStringU(cmd, &inputs.Name, "") + ruleEnabled.RegisterBool(cmd, &inputs.Enabled, true) return cmd } -// @TODO move to rules package -func getRules(cli *cli) ([]*management.Rule, error) { - list, err := cli.api.Rule.List() - if err != nil { - return nil, err - } - return list.Rules, nil -} - func promptForRuleViaDropdown(cli *cli, cmd *cobra.Command) (id string, err error) { dropdown := Flag{Name: "Rule"} @@ -434,8 +298,12 @@ func promptForRuleViaDropdown(cli *cli, cmd *cobra.Command) (id string, err erro // TODO(cyx): Consider extracting this // pattern once we've done more of it. err = ansi.Spinner("Fetching your rules", func() error { - rules, err = getRules(cli) - return err + list, err := cli.api.Rule.List() + if err != nil { + return err + } + rules = list.Rules + return nil }) if err != nil || len(rules) == 0 { From 2c4d31b95ca34ec363c438c1ba6549c7dc8eb7e9 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 16:10:52 -0700 Subject: [PATCH 07/17] Lint --- internal/cli/rules.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 723ca0798..79d435cd4 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -10,11 +10,6 @@ import ( "gopkg.in/auth0.v5/management" ) -const ( - ruleID = "id" - ruleScript = "script" -) - var ( ruleNameRequired = Flag{ Name: "Name", From 6f45f2de06842b872aac82fa7a9ccc0b09409db9 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 22:19:43 -0700 Subject: [PATCH 08/17] Finalize rules Pick 5 templates to start. --- ...rule-template-add-email-to-access-token.js | 8 ++ ...rule-template-check-last-password-reset.js | 12 +++ .../rule-template-ip-address-allow-list.js | 12 +++ .../rule-template-ip-address-deny-list.js | 14 ++++ internal/cli/rules.go | 80 ++++++++++++++++--- internal/cli/rules_embed.go | 12 +++ internal/display/rules.go | 38 +++++---- 7 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 internal/cli/data/rule-template-add-email-to-access-token.js create mode 100644 internal/cli/data/rule-template-check-last-password-reset.js create mode 100644 internal/cli/data/rule-template-ip-address-allow-list.js create mode 100644 internal/cli/data/rule-template-ip-address-deny-list.js diff --git a/internal/cli/data/rule-template-add-email-to-access-token.js b/internal/cli/data/rule-template-add-email-to-access-token.js new file mode 100644 index 000000000..615e4964a --- /dev/null +++ b/internal/cli/data/rule-template-add-email-to-access-token.js @@ -0,0 +1,8 @@ +function addEmailToAccessToken(user, context, callback) { + // This rule adds the authenticated user's email address to the access token. + + var namespace = 'https://example.com/'; + + context.accessToken[namespace + 'email'] = user.email; + return callback(null, user, context); +} diff --git a/internal/cli/data/rule-template-check-last-password-reset.js b/internal/cli/data/rule-template-check-last-password-reset.js new file mode 100644 index 000000000..b7fb87220 --- /dev/null +++ b/internal/cli/data/rule-template-check-last-password-reset.js @@ -0,0 +1,12 @@ +function checkLastPasswordReset(user, context, callback) { + function daydiff(first, second) { + return (second - first) / (1000 * 60 * 60 * 24); + } + + const last_password_change = user.last_password_reset || user.created_at; + + if (daydiff(new Date(last_password_change), new Date()) > 30) { + return callback(new UnauthorizedError('please change your password')); + } + callback(null, user, context); +} diff --git a/internal/cli/data/rule-template-ip-address-allow-list.js b/internal/cli/data/rule-template-ip-address-allow-list.js new file mode 100644 index 000000000..94fd03980 --- /dev/null +++ b/internal/cli/data/rule-template-ip-address-allow-list.js @@ -0,0 +1,12 @@ +function ipAddressAllowList(user, context, callback) { + const allowlist = ['1.2.3.4', '2.3.4.5']; // authorized IPs + const userHasAccess = allowlist.some(function (ip) { + return context.request.ip === ip; + }); + + if (!userHasAccess) { + return callback(new Error('Access denied from this IP address.')); + } + + return callback(null, user, context); +} diff --git a/internal/cli/data/rule-template-ip-address-deny-list.js b/internal/cli/data/rule-template-ip-address-deny-list.js new file mode 100644 index 000000000..a47a8403f --- /dev/null +++ b/internal/cli/data/rule-template-ip-address-deny-list.js @@ -0,0 +1,14 @@ +function ipAddressDenylist(user, context, callback) { + const denylist = ['1.2.3.4', '2.3.4.5']; // unauthorized IPs + const notAuthorized = denylist.some(function (ip) { + return context.request.ip === ip; + }); + + if (notAuthorized) { + return callback( + new UnauthorizedError('Access denied from this IP address.') + ); + } + + return callback(null, user, context); +} diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 79d435cd4..5036f9bfe 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -43,7 +43,11 @@ var ( } ruleTemplateMappings = map[string]string{ - "Empty Rule": ruleTemplateEmptyRule, + "Empty rule": ruleTemplateEmptyRule, + "Add email to access token": ruleTemplateAddEmailToAccessToken, + "Check last password reset": ruleTemplateCheckLastPasswordReset, + "IP address allow list": ruleTemplateIPAddressAllowList, + "IP address deny list": ruleTemplateIPAddressDenyList, } ) @@ -56,8 +60,9 @@ func rulesCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(listRulesCmd(cli)) cmd.AddCommand(createRuleCmd(cli)) - cmd.AddCommand(deleteRuleCmd(cli)) + cmd.AddCommand(showRuleCmd(cli)) cmd.AddCommand(updateRuleCmd(cli)) + cmd.AddCommand(deleteRuleCmd(cli)) return cmd } @@ -141,7 +146,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" return fmt.Errorf("Unable to create rule: %w", err) } - cli.renderer.RulesCreate(rule) + cli.renderer.RuleCreate(rule) return nil }, } @@ -153,6 +158,58 @@ auth0 rules create --name "My Rule" --template [empty-rule]" return cmd } +func showRuleCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show a rule", + Long: `Show a rule: + +auth0 rules show +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.ID = args[0] + } else { + var err error + inputs.ID, err = promptForRuleViaDropdown(cli, cmd) + if err != nil { + return err + } + + if inputs.ID == "" { + cli.renderer.Infof("There are currently no rules.") + return nil + } + } + + var rule *management.Rule + + err := ansi.Spinner("Loading rule", func() error { + var err error + rule, err = cli.api.Rule.Read(inputs.ID) + return err + }) + + if err != nil { + return fmt.Errorf("Unable to load rule. The ID %v specified doesn't exist", inputs.ID) + } + + cli.renderer.RuleShow(rule) + return nil + }, + } + + return cmd +} + func deleteRuleCmd(cli *cli) *cobra.Command { var inputs struct { ID string @@ -261,19 +318,24 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal inputs.Name = rule.GetName() } + // Prepare rule payload for update. This will also be + // re-hydrated by the SDK, which we'll use below during + // display. + rule = &management.Rule{ + Name: &inputs.Name, + Script: &script, + Enabled: &inputs.Enabled, + } + err = ansi.Spinner("Updating rule", func() error { - return cli.api.Rule.Update(inputs.ID, &management.Rule{ - Name: &inputs.Name, - Script: &script, - Enabled: &inputs.Enabled, - }) + return cli.api.Rule.Update(inputs.ID, rule) }) if err != nil { return err } - cli.renderer.Infof("Your rule `%s` was successfully updated.", inputs.Name) + cli.renderer.RuleUpdate(rule) return nil }, } diff --git a/internal/cli/rules_embed.go b/internal/cli/rules_embed.go index 584aee3a5..14fc553b0 100644 --- a/internal/cli/rules_embed.go +++ b/internal/cli/rules_embed.go @@ -7,4 +7,16 @@ import ( var ( //go:embed data/rule-template-empty-rule.js ruleTemplateEmptyRule string + + //go:embed data/rule-template-add-email-to-access-token.js + ruleTemplateAddEmailToAccessToken string + + //go:embed data/rule-template-check-last-password-reset.js + ruleTemplateCheckLastPasswordReset string + + //go:embed data/rule-template-ip-address-allow-list.js + ruleTemplateIPAddressAllowList string + + //go:embed data/rule-template-ip-address-deny-list.js + ruleTemplateIPAddressDenyList string ) diff --git a/internal/display/rules.go b/internal/display/rules.go index a94b1dd10..f254e2036 100644 --- a/internal/display/rules.go +++ b/internal/display/rules.go @@ -62,21 +62,9 @@ func (r *Renderer) RulesList(rules []*management.Rule) { } -func (r *Renderer) RulesCreate(rule *management.Rule) { +func (r *Renderer) RuleCreate(rule *management.Rule) { r.Heading(ansi.Bold(r.Tenant), "rule created\n") - - v := &ruleView{ - Name: rule.GetName(), - ID: rule.GetID(), - Enabled: rule.GetEnabled(), - Order: rule.GetOrder(), - Script: rule.GetScript(), - - raw: rule, - } - - r.Result(v) - + r.Result(makeRuleView(rule)) r.Newline() // TODO(cyx): possibly guard this with a --no-hint flag. @@ -89,3 +77,25 @@ func (r *Renderer) RulesCreate(rule *management.Rule) { ansi.Faint("Hint"), ) } + +func (r *Renderer) RuleUpdate(rule *management.Rule) { + r.Heading(ansi.Bold(r.Tenant), "rule updated\n") + r.Result(makeRuleView(rule)) +} + +func (r *Renderer) RuleShow(rule *management.Rule) { + r.Heading(ansi.Bold(r.Tenant), "rule\n") + r.Result(makeRuleView(rule)) +} + +func makeRuleView(rule *management.Rule) *ruleView { + return &ruleView{ + Name: rule.GetName(), + ID: rule.GetID(), + Enabled: rule.GetEnabled(), + Order: rule.GetOrder(), + Script: rule.GetScript(), + + raw: rule, + } +} From 6a84442865c02b0876c6ca4da44793b2e282226b Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 22:27:31 -0700 Subject: [PATCH 09/17] Tweak flags so we don't repeat ourselves too much --- internal/cli/flags.go | 6 ++++++ internal/cli/flags_test.go | 18 ++++++++++++++++++ internal/cli/rules.go | 12 ++---------- 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 internal/cli/flags_test.go diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 58b230a5f..3bfbb27b7 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -30,6 +30,12 @@ func (f Flag) GetIsRequired() bool { return f.IsRequired } +func (f *Flag) Required() *Flag { + clone := *f + clone.IsRequired = true + return &clone +} + func (f *Flag) Ask(cmd *cobra.Command, value interface{}) error { return askFlag(cmd, f, value, false) } diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go new file mode 100644 index 000000000..fa9f2e3c1 --- /dev/null +++ b/internal/cli/flags_test.go @@ -0,0 +1,18 @@ +package cli + +import "testing" + +func TestFlagsRequiredClone(t *testing.T) { + orig := &Flag{ + Name: "some-flag", + } + + clone := orig.Required() + if !clone.IsRequired { + t.Fatal("wanted flag to be required") + } + + if orig.IsRequired { + t.Fatal("wanted original to be left intact") + } +} diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 5036f9bfe..7ebab0cec 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -11,14 +11,6 @@ import ( ) var ( - ruleNameRequired = Flag{ - Name: "Name", - LongForm: "name", - ShortForm: "n", - Help: "Name of the rule.", - IsRequired: true, - } - ruleName = Flag{ Name: "Name", LongForm: "name", @@ -113,7 +105,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if err := ruleNameRequired.Ask(cmd, &inputs.Name); err != nil { + if err := ruleName.Required().Ask(cmd, &inputs.Name); err != nil { return err } @@ -151,7 +143,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" }, } - ruleNameRequired.RegisterString(cmd, &inputs.Name, "") + ruleName.Required().RegisterString(cmd, &inputs.Name, "") ruleTemplate.RegisterString(cmd, &inputs.Template, "") ruleEnabled.RegisterBool(cmd, &inputs.Enabled, true) From 3855f83a78e2e48adc93797b6754872bc6dd1843 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 19 Mar 2021 22:28:44 -0700 Subject: [PATCH 10/17] Add docs for flags.Required() --- internal/cli/flags.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 3bfbb27b7..c506de6d5 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -30,6 +30,9 @@ func (f Flag) GetIsRequired() bool { return f.IsRequired } +// Required clones an existing flag and assigns `IsRequired` to true. This is +// useful when there are flags with several different use cases -- e.g. create +// requiring the flag, but update not requiring it. func (f *Flag) Required() *Flag { clone := *f clone.IsRequired = true From daeb4c56e4536e41c155fc2831ed5eef1c863631 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 09:39:33 -0700 Subject: [PATCH 11/17] Switch to using new ansi.Waiting for most cases --- internal/cli/rules.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 7ebab0cec..5f4394d7b 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -66,7 +66,7 @@ func listRulesCmd(cli *cli) *cobra.Command { Long: `List the rules in your current tenant.`, RunE: func(cmd *cobra.Command, args []string) error { var rules []*management.Rule - err := ansi.Spinner("Loading rules", func() error { + err := ansi.Waiting(func() error { ruleList, err := cli.api.Rule.List() if err != nil { return err @@ -130,7 +130,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" Enabled: &inputs.Enabled, } - err = ansi.Spinner("Creating rule", func() error { + err = ansi.Waiting(func() error { return cli.api.Rule.Create(rule) }) @@ -184,7 +184,7 @@ auth0 rules show var rule *management.Rule - err := ansi.Spinner("Loading rule", func() error { + err := ansi.Waiting(func() error { var err error rule, err = cli.api.Rule.Read(inputs.ID) return err @@ -285,7 +285,7 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal } var rule *management.Rule - err := ansi.Spinner("Fetching rule", func() error { + err := ansi.Waiting(func() error { var err error rule, err = cli.api.Rule.Read(inputs.ID) return err @@ -319,7 +319,7 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal Enabled: &inputs.Enabled, } - err = ansi.Spinner("Updating rule", func() error { + err = ansi.Waiting(func() error { return cli.api.Rule.Update(inputs.ID, rule) }) @@ -346,7 +346,7 @@ func promptForRuleViaDropdown(cli *cli, cmd *cobra.Command) (id string, err erro // == Start experimental dropdown for names => id. // TODO(cyx): Consider extracting this // pattern once we've done more of it. - err = ansi.Spinner("Fetching your rules", func() error { + err = ansi.Waiting(func() error { list, err := cli.api.Rule.List() if err != nil { return err From 4819ea8426ef0dbb7fca3c7ca01b5158f393a846 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 09:45:35 -0700 Subject: [PATCH 12/17] Ask name after fetching rule --- internal/cli/rules.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 5f4394d7b..eec10f1f0 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -280,10 +280,6 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal } } - if err := ruleName.AskU(cmd, &inputs.Name); err != nil { - return err - } - var rule *management.Rule err := ansi.Waiting(func() error { var err error @@ -294,6 +290,10 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal return fmt.Errorf("Failed to fetch rule with ID: %s %v", inputs.ID, err) } + if err := ruleName.AskU(cmd, &inputs.Name); err != nil { + return err + } + // TODO(cyx): we can re-think this once we have // `--stdin` based commands. For now we don't have // those yet, so keeping this simple. From 25a1c89cca8a912e3cc908ac3d752f4971449ded Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 09:45:42 -0700 Subject: [PATCH 13/17] Subtle fix: update && not required should not prompt --- internal/cli/flags.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/cli/flags.go b/internal/cli/flags.go index c506de6d5..66f1b60bc 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -149,6 +149,18 @@ func registerBool(cmd *cobra.Command, f *Flag, value *bool, defaultValue bool, i } } +func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool { + if isUpdate { + if !f.IsRequired { + return false + } + + return shouldPromptWhenFlagless(cmd, f.LongForm) + } + + return shouldPrompt(cmd, f.LongForm) +} + func markFlagRequired(cmd *cobra.Command, f *Flag, isUpdate bool) error { if f.IsRequired && !isUpdate { return cmd.MarkFlagRequired(f.LongForm) From 45626d54617f519be1404b795d3199b0945053db Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 10:15:16 -0700 Subject: [PATCH 14/17] Cleanup --- internal/cli/flags.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 66f1b60bc..532e9018c 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -87,7 +87,6 @@ func (f *Flag) RegisterBoolU(cmd *cobra.Command, value *bool, defaultValue bool) registerBool(cmd, f, value, defaultValue, true) } -// LOOK func askFlag(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) error { if shouldAsk(cmd, f, isUpdate) { return ask(cmd, f, value, isUpdate) @@ -96,11 +95,6 @@ func askFlag(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) erro return nil } -// LOOK -func (f *Flag) label() string { - return fmt.Sprintf("%s:", f.Name) -} - func selectFlag(cmd *cobra.Command, f *Flag, value interface{}, options []string, isUpdate bool) error { if shouldAsk(cmd, f, isUpdate) { return _select(cmd, f, value, options, isUpdate) @@ -109,14 +103,6 @@ func selectFlag(cmd *cobra.Command, f *Flag, value interface{}, options []string return nil } -func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool { - if isUpdate { - return shouldPromptWhenFlagless(cmd, f.LongForm) - } - - return shouldPrompt(cmd, f.LongForm) -} - func registerString(cmd *cobra.Command, f *Flag, value *string, defaultValue string, isUpdate bool) { cmd.Flags().StringVarP(value, f.LongForm, f.ShortForm, defaultValue, f.Help) From 3e767bda6346f2404b5e1aac57baa23f686cceb5 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 10:21:34 -0700 Subject: [PATCH 15/17] Add a note for now that we're going to do something for next time --- internal/cli/rules.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index eec10f1f0..5fe49739a 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -170,6 +170,8 @@ auth0 rules show if len(args) > 0 { inputs.ID = args[0] } else { + // TODO(cyx): Consider making a primitive for + // Argument to ask using a provided func. var err error inputs.ID, err = promptForRuleViaDropdown(cli, cmd) if err != nil { @@ -220,6 +222,8 @@ auth0 rules delete rul_d2VSaGlyaW5n`, if len(args) > 0 { inputs.ID = args[0] } else { + // TODO(cyx): Consider making a primitive for + // Argument to ask using a provided func. var err error inputs.ID, err = promptForRuleViaDropdown(cli, cmd) if err != nil { @@ -268,6 +272,8 @@ auth0 rules update --id rul_d2VSaGlyaW5n --name "My Updated Rule" --enabled=fal if len(args) > 0 { inputs.ID = args[0] } else { + // TODO(cyx): Consider making a primitive for + // Argument to ask using a provided func. var err error inputs.ID, err = promptForRuleViaDropdown(cli, cmd) if err != nil { From f249bd09b830e19d99683ea251f2164db0a2f99a Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 11:03:34 -0700 Subject: [PATCH 16/17] Remove `Required()` use --- internal/cli/rules.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 5fe49739a..6eb0029c6 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -12,10 +12,11 @@ import ( var ( ruleName = Flag{ - Name: "Name", - LongForm: "name", - ShortForm: "n", - Help: "Name of the rule.", + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the rule.", + IsRequired: true, } ruleTemplate = Flag{ @@ -105,7 +106,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if err := ruleName.Required().Ask(cmd, &inputs.Name); err != nil { + if err := ruleName.Ask(cmd, &inputs.Name); err != nil { return err } @@ -143,7 +144,7 @@ auth0 rules create --name "My Rule" --template [empty-rule]" }, } - ruleName.Required().RegisterString(cmd, &inputs.Name, "") + ruleName.RegisterString(cmd, &inputs.Name, "") ruleTemplate.RegisterString(cmd, &inputs.Template, "") ruleEnabled.RegisterBool(cmd, &inputs.Enabled, true) From 75c8fb3ea2667ca472aa47afe6904544ddcfa157 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Sat, 20 Mar 2021 15:28:12 -0700 Subject: [PATCH 17/17] Amend to have AlwaysPrompt logic --- internal/cli/apps.go | 11 ++++++----- internal/cli/flags.go | 22 +++++++--------------- internal/cli/flags_test.go | 18 ------------------ 3 files changed, 13 insertions(+), 38 deletions(-) delete mode 100644 internal/cli/flags_test.go diff --git a/internal/cli/apps.go b/internal/cli/apps.go index 358f843cb..403ae5e4f 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -49,11 +49,12 @@ var ( IsRequired: false, } appCallbacks = Flag{ - Name: "Callback URLs", - LongForm: "callbacks", - ShortForm: "c", - Help: "After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://.", - IsRequired: false, + Name: "Callback URLs", + LongForm: "callbacks", + ShortForm: "c", + Help: "After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://.", + IsRequired: false, + AlwaysPrompt: true, } appOrigins = Flag{ Name: "Allowed Origin URLs", diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 532e9018c..5ae7fbb77 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -7,11 +7,12 @@ import ( ) type Flag struct { - Name string - LongForm string - ShortForm string - Help string - IsRequired bool + Name string + LongForm string + ShortForm string + Help string + IsRequired bool + AlwaysPrompt bool } func (f Flag) GetName() string { @@ -30,15 +31,6 @@ func (f Flag) GetIsRequired() bool { return f.IsRequired } -// Required clones an existing flag and assigns `IsRequired` to true. This is -// useful when there are flags with several different use cases -- e.g. create -// requiring the flag, but update not requiring it. -func (f *Flag) Required() *Flag { - clone := *f - clone.IsRequired = true - return &clone -} - func (f *Flag) Ask(cmd *cobra.Command, value interface{}) error { return askFlag(cmd, f, value, false) } @@ -137,7 +129,7 @@ func registerBool(cmd *cobra.Command, f *Flag, value *bool, defaultValue bool, i func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool { if isUpdate { - if !f.IsRequired { + if !f.AlwaysPrompt { return false } diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go deleted file mode 100644 index fa9f2e3c1..000000000 --- a/internal/cli/flags_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package cli - -import "testing" - -func TestFlagsRequiredClone(t *testing.T) { - orig := &Flag{ - Name: "some-flag", - } - - clone := orig.Required() - if !clone.IsRequired { - t.Fatal("wanted flag to be required") - } - - if orig.IsRequired { - t.Fatal("wanted original to be left intact") - } -}