Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/auth0_actions_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ auth0 actions create [flags]
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --module "module_id=mod_123,module_version_id=ver_456"
auth0 actions create -n myaction -t post-login -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json
auth0 actions create -n myaction -t post-login -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json-compact
```
Expand All @@ -38,6 +39,7 @@ auth0 actions create [flags]
-d, --dependency stringToString Third party npm module, and its version, that the action depends on. (default [])
--json Output in json format.
--json-compact Output in compact json format.
-m, --module stringArray Action module to associate with the action, as comma-separated key=value pairs matching the API fields: module_id and module_version_id (both required, UUIDs). Can be passed multiple times to associate several modules.
-n, --name string Name of the action.
-r, --runtime string Runtime to be used in the action. Possible values are: node22(recommended), node18, node16, node12
-s, --secret stringToString Secrets to be used in the action. (default [])
Expand Down
2 changes: 2 additions & 0 deletions docs/auth0_actions_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ auth0 actions update [flags]
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0"
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value"
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value"
auth0 actions update <action-id> --module "module_id=mod_123,module_version_id=ver_456"
auth0 actions update <action-id> -n myaction -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json
auth0 actions update <action-id> -n myaction -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json-compact
```
Expand All @@ -39,6 +40,7 @@ auth0 actions update [flags]
--force Skip confirmation.
--json Output in json format.
--json-compact Output in compact json format.
-m, --module stringArray Action module to associate with the action, as comma-separated key=value pairs matching the API fields: module_id and module_version_id (both required, UUIDs). Can be passed multiple times to associate several modules.
-n, --name string Name of the action.
-r, --runtime string Runtime to be used in the action. Possible values are: node22(recommended), node18, node16, node12
-s, --secret stringToString Secrets to be used in the action. (default [])
Expand Down
120 changes: 105 additions & 15 deletions internal/cli/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/url"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/auth0/go-auth0"
Expand Down Expand Up @@ -67,6 +68,13 @@ var (
Help: "Runtime to be used in the action. Possible values are: node22(recommended), node18, node16, node12",
}

actionModule = Flag{
Name: "Module",
LongForm: "module",
ShortForm: "m",
Help: "Action module to associate with the action, as comma-separated key=value pairs matching the API fields: module_id and module_version_id (both required, UUIDs). Can be passed multiple times to associate several modules.",
}

actionTemplates = map[string]string{
"post-login": actionTemplatePostLogin,
"credentials-exchange": actionTemplateCredentialsExchange,
Expand Down Expand Up @@ -190,6 +198,7 @@ func createActionCmd(cli *cli) *cobra.Command {
Dependencies map[string]string
Secrets map[string]string
Runtime string
Modules []string
}

cmd := &cobra.Command{
Expand All @@ -206,6 +215,7 @@ func createActionCmd(cli *cli) *cobra.Command {
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value"
auth0 actions create --name myaction --trigger post-login --code "$(cat path/to/code.js)" --module "module_id=mod_123,module_version_id=ver_456"
auth0 actions create -n myaction -t post-login -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json
auth0 actions create -n myaction -t post-login -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json-compact`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -259,6 +269,14 @@ func createActionCmd(cli *cli) *cobra.Command {
Secrets: inputSecretsToActionSecrets(inputs.Secrets),
}

if len(inputs.Modules) != 0 {
modules, err := inputModulesToActionModules(inputs.Modules)
if err != nil {
return err
}
action.Modules = modules
}

if err := ansi.Waiting(func() error {
return cli.api.Action.Create(cmd.Context(), action)
}); err != nil {
Expand All @@ -279,6 +297,7 @@ func createActionCmd(cli *cli) *cobra.Command {
actionDependency.RegisterStringMap(cmd, &inputs.Dependencies, nil)
actionSecret.RegisterStringMap(cmd, &inputs.Secrets, nil)
actionRuntime.RegisterString(cmd, &inputs.Runtime, "")
actionModule.RegisterStringArray(cmd, &inputs.Modules, nil)

return cmd
}
Expand All @@ -291,6 +310,7 @@ func updateActionCmd(cli *cli) *cobra.Command {
Dependencies map[string]string
Secrets map[string]string
Runtime string
Modules []string
}

cmd := &cobra.Command{
Expand All @@ -308,6 +328,7 @@ func updateActionCmd(cli *cli) *cobra.Command {
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0"
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --secret "SECRET=value"
auth0 actions update <action-id> --name myaction --code "$(cat path/to/code.js)" --dependency "lodash=4.0.0" --dependency "uuid=9.0.0" --secret "API_KEY=value" --secret "SECRET=value"
auth0 actions update <action-id> --module "module_id=mod_123,module_version_id=ver_456"
auth0 actions update <action-id> -n myaction -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json
auth0 actions update <action-id> -n myaction -c "$(cat path/to/code.js)" -r node18 -d "lodash=4.0.0" -d "uuid=9.0.0" -s "API_KEY=value" -s "SECRET=value" --json-compact`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -332,26 +353,33 @@ func updateActionCmd(cli *cli) *cobra.Command {
return err
}

if err := actionCode.OpenEditorU(
cmd,
&inputs.Code,
oldAction.GetCode(),
inputs.Name+".*.js",
); err != nil {
return fmt.Errorf("failed to capture input from the editor: %w", err)
}

if !cli.force && canPrompt(cmd) {
var confirmed bool
if err := prompt.AskBool("Do you want to save the action code?", &confirmed, true); err != nil {
return fmt.Errorf("failed to capture prompt input: %w", err)
// When the update is driven purely by non-code flags (e.g. --module),
// skip the interactive code editor and its save confirmation so those
// changes aren't discarded by answering "No".
editingCode := actionCode.IsSet(cmd) || !hasNonCodeFlagSet(cmd)
if editingCode {
if err := actionCode.OpenEditorU(
cmd,
&inputs.Code,
oldAction.GetCode(),
inputs.Name+".*.js",
); err != nil {
return fmt.Errorf("failed to capture input from the editor: %w", err)
}
if !confirmed {
return nil

if !cli.force && canPrompt(cmd) {
var confirmed bool
if err := prompt.AskBool("Do you want to save the action code?", &confirmed, true); err != nil {
return fmt.Errorf("failed to capture prompt input: %w", err)
}
if !confirmed {
return nil
}
}
}

updatedAction := &management.Action{
Name: oldAction.Name,
SupportedTriggers: oldAction.SupportedTriggers,
}
if inputs.Name != "" {
Expand All @@ -367,6 +395,14 @@ func updateActionCmd(cli *cli) *cobra.Command {
updatedAction.Secrets = inputSecretsToActionSecrets(inputs.Secrets)
}

if len(inputs.Modules) != 0 {
modules, err := inputModulesToActionModules(inputs.Modules)
if err != nil {
return err
}
updatedAction.Modules = modules
}

if inputs.Runtime != "" {
updatedAction.Runtime = &inputs.Runtime
}
Expand All @@ -391,10 +427,21 @@ func updateActionCmd(cli *cli) *cobra.Command {
actionDependency.RegisterStringMapU(cmd, &inputs.Dependencies, nil)
actionSecret.RegisterStringMapU(cmd, &inputs.Secrets, nil)
actionRuntime.RegisterStringU(cmd, &inputs.Runtime, "")
actionModule.RegisterStringArrayU(cmd, &inputs.Modules, nil)

return cmd
}

// hasNonCodeFlagSet reports whether the user set any update flag other than the
// action code, so a non-code update (e.g. --module) can skip the code editor.
func hasNonCodeFlagSet(cmd *cobra.Command) bool {
return actionName.IsSet(cmd) ||
actionDependency.IsSet(cmd) ||
actionSecret.IsSet(cmd) ||
actionRuntime.IsSet(cmd) ||
actionModule.IsSet(cmd)
}

func deleteActionCmd(cli *cli) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Expand Down Expand Up @@ -722,6 +769,49 @@ func inputSecretsToActionSecrets(secrets map[string]string) *[]management.Action
return &actionSecrets
}

// inputModulesToActionModules parses repeatable --module flag values into the
// action's Modules payload. Each value is a comma-separated set of key=value
// pairs whose keys mirror the Management API field names: module_id and
// module_version_id, both required. The API identifies the version to attach by
// its UUID (module_version_id); the version number is derived server-side and
// returned on read, so it is not accepted as input here.
func inputModulesToActionModules(modules []string) (*[]management.ActionModules, error) {
actionModules := make([]management.ActionModules, 0, len(modules))

for _, raw := range modules {
var module management.ActionModules

for _, pair := range strings.Split(raw, ",") {
key, value, found := strings.Cut(pair, "=")
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if !found || key == "" || value == "" {
return nil, fmt.Errorf("invalid --module value %q: expected comma-separated key=value pairs (e.g. \"module_id=<uuid>,module_version_id=<uuid>\")", raw)
}

switch key {
case "module_id":
module.ModuleID = auth0.String(value)
case "module_version_id":
module.ModuleVersionID = auth0.String(value)
default:
return nil, fmt.Errorf("invalid --module value %q: unknown key %q (supported keys: module_id, module_version_id)", raw, key)
}
}

if module.ModuleID == nil {
return nil, fmt.Errorf("invalid --module value %q: module_id is required", raw)
}
if module.ModuleVersionID == nil {
return nil, fmt.Errorf("invalid --module value %q: module_version_id is required (the UUID of a specific module version)", raw)
}

actionModules = append(actionModules, module)
}

return &actionModules, nil
}

func printColorDiff(code1, code2 string, fromVersion, toVersion int) {
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(code1),
Expand Down
101 changes: 101 additions & 0 deletions internal/cli/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,56 @@ func TestActionsDeployCmd(t *testing.T) {
})
}

func TestActionsUpdateCmd(t *testing.T) {
t.Run("it carries the existing name forward on a module-only update", func(t *testing.T) {
actionID := "1221c74c-cfd6-40db-af13-7bc9bb1c38db"
ctrl := gomock.NewController(t)
defer ctrl.Finish()

actionAPI := mock.NewMockActionAPI(ctrl)
actionAPI.EXPECT().
Read(context.Background(), actionID).
Return(&management.Action{
ID: auth0.String(actionID),
Name: auth0.String("existing-name"),
SupportedTriggers: []management.ActionTrigger{
{ID: auth0.String("post-login")},
},
Code: auth0.String("function () {}"),
}, nil)

var captured *management.Action
actionAPI.EXPECT().
Update(context.Background(), actionID, gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, a *management.Action, _ ...management.RequestOption) error {
captured = a
return nil
})

stdout := &bytes.Buffer{}
cli := &cli{
renderer: &display.Renderer{
MessageWriter: io.Discard,
ResultWriter: stdout,
},
api: &auth0.API{Action: actionAPI},
}

cmd := updateActionCmd(cli)
cmd.SetArgs([]string{
actionID,
"--module", "module_id=mod_123,module_version_id=ver_456",
})
err := cmd.Execute()

assert.NoError(t, err)
assert.Equal(t, "existing-name", captured.GetName())
assert.Equal(t, []management.ActionModules{
{ModuleID: auth0.String("mod_123"), ModuleVersionID: auth0.String("ver_456")},
}, *captured.Modules)
})
}

func TestActionsPickerOptions(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -354,3 +404,54 @@ func TestActionsInputDependenciesToActionDependencies(t *testing.T) {
assert.Equal(t, expected, *res)
})
}

func TestActionsInputModulesToActionModules(t *testing.T) {
t.Run("it maps a single module with id and version id", func(t *testing.T) {
res, err := inputModulesToActionModules([]string{"module_id=mod_123,module_version_id=ver_456"})

assert.NoError(t, err)
assert.Equal(t, []management.ActionModules{
{ModuleID: auth0.String("mod_123"), ModuleVersionID: auth0.String("ver_456")},
}, *res)
})

t.Run("it maps multiple modules", func(t *testing.T) {
res, err := inputModulesToActionModules([]string{
"module_id=mod_123,module_version_id=ver_456",
"module_id=mod_789,module_version_id=ver_abc",
})

assert.NoError(t, err)
assert.Equal(t, []management.ActionModules{
{ModuleID: auth0.String("mod_123"), ModuleVersionID: auth0.String("ver_456")},
{ModuleID: auth0.String("mod_789"), ModuleVersionID: auth0.String("ver_abc")},
}, *res)
})

t.Run("it handles empty input modules", func(t *testing.T) {
res, err := inputModulesToActionModules([]string{})
expected := []management.ActionModules{}
assert.NoError(t, err)
assert.Equal(t, &expected, res)
})

t.Run("it errors on a malformed pair", func(t *testing.T) {
_, err := inputModulesToActionModules([]string{"mod_123"})
assert.ErrorContains(t, err, "expected comma-separated key=value pairs")
})

t.Run("it errors when module_id is missing", func(t *testing.T) {
_, err := inputModulesToActionModules([]string{"module_version_id=ver_456"})
assert.ErrorContains(t, err, "module_id is required")
})

t.Run("it errors when module_version_id is missing", func(t *testing.T) {
_, err := inputModulesToActionModules([]string{"module_id=mod_123"})
assert.ErrorContains(t, err, "module_version_id is required")
})

t.Run("it errors on an unknown key", func(t *testing.T) {
_, err := inputModulesToActionModules([]string{"module_id=mod_123,foo=bar"})
assert.ErrorContains(t, err, "unknown key")
})
}
26 changes: 26 additions & 0 deletions internal/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ func (f *Flag) RegisterStringSliceU(cmd *cobra.Command, value *[]string, default
registerStringSlice(cmd, f, value, defaultValue, true)
}

func (f *Flag) RegisterStringArray(cmd *cobra.Command, value *[]string, defaultValue []string) {
registerStringArray(cmd, f, value, defaultValue, false)
}

func (f *Flag) RegisterStringArrayU(cmd *cobra.Command, value *[]string, defaultValue []string) {
registerStringArray(cmd, f, value, defaultValue, true)
}

func (f *Flag) RegisterStringMap(cmd *cobra.Command, value *map[string]string, defaultValue map[string]string) {
registerStringMap(cmd, f, value, defaultValue, false)
}
Expand Down Expand Up @@ -325,6 +333,24 @@ func registerStringSlice(cmd *cobra.Command, f *Flag, value *[]string, defaultVa
}
}

func registerStringArray(cmd *cobra.Command, f *Flag, value *[]string, defaultValue []string, isUpdate bool) {
cmd.Flags().StringArrayVarP(value, f.LongForm, f.ShortForm, defaultValue, f.Help)

// Set up flag aliases if specified.
if len(f.AlsoKnownAs) > 0 {
flag := cmd.Flags().Lookup(f.LongForm)
if flag != nil {
for _, alias := range f.AlsoKnownAs {
cmd.Flags().StringArrayVar(value, alias, defaultValue, f.Help)
}
}
}

if err := markFlagRequired(cmd, f, isUpdate); err != nil {
panic(auth0.Error(err, "failed to register string array flag"))
}
}

func registerStringMap(cmd *cobra.Command, f *Flag, value *map[string]string, defaultValue map[string]string, isUpdate bool) {
cmd.Flags().StringToStringVarP(value, f.LongForm, f.ShortForm, defaultValue, f.Help)

Expand Down
Loading
Loading