diff --git a/Makefile b/Makefile index 6434cb882..5adda9fa3 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ test-unit: ## Run unit tests ${call print, "Running unit tests"} @go test -v -race ${GO_PACKAGES} -coverprofile="coverage-unit-tests.out" -test-integration: install-with-cover $(GO_BIN)/auth0 $(GO_BIN)/commander ## Run integration tests. To run a specific test pass the FILTER var. Usage: `make test-integration FILTER="attack protection"` +test-integration: install-with-cover $(GO_BIN)/auth0 $(GO_BIN)/commander ## Run integration tests. To run a specific test pass the FILTER var. Usage: `make test-integration FILTER="attack protection"`. To run a single suite file pass the FILE var. Usage: `make test-integration FILE="./test/integration/forms-test-cases.yaml"` ${call print, "Running integration tests"} @mkdir -p "coverage" @PATH=$(GO_BIN):$$PATH \ diff --git a/README.md b/README.md index 2c867757e..daacf5d96 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,7 @@ Select **y** to proceed with your default tenant, or **N** to choose a different - [auth0 completion](https://auth0.github.io/auth0-cli/auth0_completion.html) - Setup autocomplete features for this CLI on your terminal - [auth0 domains](https://auth0.github.io/auth0-cli/auth0_domains.html) - Manage custom domains - [auth0 email](https://auth0.github.io/auth0-cli/auth0_email.html) - Manage email settings +- [auth0 flows](https://auth0.github.io/auth0-cli/auth0_flows.html) - Manage Flows - [auth0 login](https://auth0.github.io/auth0-cli/auth0_login.html) - Authenticate the Auth0 CLI - [auth0 logout](https://auth0.github.io/auth0-cli/auth0_logout.html) - Log out of a tenant's session - [auth0 logs](https://auth0.github.io/auth0-cli/auth0_logs.html) - View tenant logs diff --git a/docs/auth0_flows.md b/docs/auth0_flows.md new file mode 100644 index 000000000..2c9fbfe0b --- /dev/null +++ b/docs/auth0_flows.md @@ -0,0 +1,20 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 flows + +Flows let you orchestrate custom logic during authentication and other journeys, chaining actions such as HTTP requests and vault-backed integrations. + +## Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + diff --git a/docs/auth0_flows_create.md b/docs/auth0_flows_create.md new file mode 100644 index 000000000..b5d5484f2 --- /dev/null +++ b/docs/auth0_flows_create.md @@ -0,0 +1,60 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows create + +Create a new flow. + +A name is required, supplied with `--name` or the interactive prompt. The actions graph is authored interactively or supplied with `--actions-file`; the file must contain only the actions body, not a name. Run `auth0 flows create --actions-template > flow.json` to generate an actions template. + +## Usage +``` +auth0 flows create [flags] +``` + +## Examples + +``` + auth0 flows create + auth0 flows create --name "My Flow" + auth0 flows create --actions-template > flow.json + auth0 flows create --name "My Flow" --actions-file ./flow.json +``` + + +## Flags + +``` + -f, --actions-file string Path to a JSON file containing the flow actions body. Run with --actions-template to see the expected format. + --actions-template Print the actions template for --actions-file and exit. + --json Output in json format. + --json-compact Output in compact json format. + --name string Name of the Flow. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_delete.md b/docs/auth0_flows_delete.md new file mode 100644 index 000000000..9a767b4ad --- /dev/null +++ b/docs/auth0_flows_delete.md @@ -0,0 +1,59 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows delete + +Delete a flow. + +To delete interactively, use `auth0 flows delete` with no arguments. + +To delete non-interactively, supply the flow id and the `--force` flag to skip confirmation. + +## Usage +``` +auth0 flows delete [flags] +``` + +## Examples + +``` + auth0 flows delete + auth0 flows rm + auth0 flows delete + auth0 flows delete --force + auth0 flows delete +``` + + +## Flags + +``` + --force Skip confirmation. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_executions.md b/docs/auth0_flows_executions.md new file mode 100644 index 000000000..7344e45d7 --- /dev/null +++ b/docs/auth0_flows_executions.md @@ -0,0 +1,15 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 flows executions + +Inspect the runtime executions produced when a flow runs. + +## Commands + +- [auth0 flows executions delete](auth0_flows_executions_delete.md) - Delete a flow execution +- [auth0 flows executions list](auth0_flows_executions_list.md) - List a flow's executions +- [auth0 flows executions show](auth0_flows_executions_show.md) - Show a flow execution + diff --git a/docs/auth0_flows_executions_delete.md b/docs/auth0_flows_executions_delete.md new file mode 100644 index 000000000..ab8b43cba --- /dev/null +++ b/docs/auth0_flows_executions_delete.md @@ -0,0 +1,50 @@ +--- +layout: default +parent: auth0 flows executions +has_toc: false +--- +# auth0 flows executions delete + +Delete one or more executions of a flow. + +Supply the flow id followed by the execution ids. Use `--force` to skip confirmation. + +## Usage +``` +auth0 flows executions delete [flags] +``` + +## Examples + +``` + auth0 flows executions delete + auth0 flows executions rm --force + auth0 flows executions delete +``` + + +## Flags + +``` + --force Skip confirmation. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows executions delete](auth0_flows_executions_delete.md) - Delete a flow execution +- [auth0 flows executions list](auth0_flows_executions_list.md) - List a flow's executions +- [auth0 flows executions show](auth0_flows_executions_show.md) - Show a flow execution + + diff --git a/docs/auth0_flows_executions_list.md b/docs/auth0_flows_executions_list.md new file mode 100644 index 000000000..d4bda76db --- /dev/null +++ b/docs/auth0_flows_executions_list.md @@ -0,0 +1,53 @@ +--- +layout: default +parent: auth0 flows executions +has_toc: false +--- +# auth0 flows executions list + +List the executions produced by a flow. + +## Usage +``` +auth0 flows executions list [flags] +``` + +## Examples + +``` + auth0 flows executions list + auth0 flows executions ls --number 100 + auth0 flows executions list --json +``` + + +## Flags + +``` + --csv Output in csv format. + --from string Cursor id from which to start selection. + --json Output in json format. + --json-compact Output in compact json format. + -n, --number int Number of executions to retrieve. Fetched across pages. (default 100) + --take int Number of executions to retrieve per page. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows executions delete](auth0_flows_executions_delete.md) - Delete a flow execution +- [auth0 flows executions list](auth0_flows_executions_list.md) - List a flow's executions +- [auth0 flows executions show](auth0_flows_executions_show.md) - Show a flow execution + + diff --git a/docs/auth0_flows_executions_show.md b/docs/auth0_flows_executions_show.md new file mode 100644 index 000000000..74196248d --- /dev/null +++ b/docs/auth0_flows_executions_show.md @@ -0,0 +1,48 @@ +--- +layout: default +parent: auth0 flows executions +has_toc: false +--- +# auth0 flows executions show + +Display information about a flow execution. + +## Usage +``` +auth0 flows executions show [flags] +``` + +## Examples + +``` + auth0 flows executions show + auth0 flows executions show --json +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows executions delete](auth0_flows_executions_delete.md) - Delete a flow execution +- [auth0 flows executions list](auth0_flows_executions_list.md) - List a flow's executions +- [auth0 flows executions show](auth0_flows_executions_show.md) - Show a flow execution + + diff --git a/docs/auth0_flows_list.md b/docs/auth0_flows_list.md new file mode 100644 index 000000000..a6de50a57 --- /dev/null +++ b/docs/auth0_flows_list.md @@ -0,0 +1,60 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows list + +List your existing flows. To create one, run: `auth0 flows create`. + +## Usage +``` +auth0 flows list [flags] +``` + +## Examples + +``` + auth0 flows list + auth0 flows ls + auth0 flows ls --number 100 + auth0 flows ls --hydrate + auth0 flows ls --json +``` + + +## Flags + +``` + --csv Output in csv format. + --hydrate Hydrate the response with the number of forms referencing each flow. + --json Output in json format. + --json-compact Output in compact json format. + -n, --number int Number of flows to retrieve. Fetched across pages. (default 100) + --synchronous Filter to synchronous (true) or asynchronous (false) flows. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_open.md b/docs/auth0_flows_open.md new file mode 100644 index 000000000..27409dd4c --- /dev/null +++ b/docs/auth0_flows_open.md @@ -0,0 +1,47 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows open + +Open a flow's page in the Auth0 Dashboard flow builder. + +## Usage +``` +auth0 flows open [flags] +``` + +## Examples + +``` + auth0 flows open + auth0 flows open +``` + + + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_show.md b/docs/auth0_flows_show.md new file mode 100644 index 000000000..53f1fcaa3 --- /dev/null +++ b/docs/auth0_flows_show.md @@ -0,0 +1,54 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows show + +Display information about a flow. + +## Usage +``` +auth0 flows show [flags] +``` + +## Examples + +``` + auth0 flows show + auth0 flows show + auth0 flows show --json +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_update.md b/docs/auth0_flows_update.md new file mode 100644 index 000000000..379f40961 --- /dev/null +++ b/docs/auth0_flows_update.md @@ -0,0 +1,57 @@ +--- +layout: default +parent: auth0 flows +has_toc: false +--- +# auth0 flows update + +Update a flow. + +Passing `--actions-file` replaces the flow's actions graph; the file must contain only the actions body, not a name. Passing `--name` renames the flow; omit it to keep the current name. + +## Usage +``` +auth0 flows update [flags] +``` + +## Examples + +``` + auth0 flows update --name "New Name" + auth0 flows update --actions-file ./flow.json +``` + + +## Flags + +``` + -f, --actions-file string Path to a JSON file containing the flow actions body. Run with --actions-template to see the expected format. + --json Output in json format. + --json-compact Output in compact json format. + --name string Name of the Flow. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows create](auth0_flows_create.md) - Create a new flow +- [auth0 flows delete](auth0_flows_delete.md) - Delete a flow +- [auth0 flows executions](auth0_flows_executions.md) - Manage Flow executions +- [auth0 flows list](auth0_flows_list.md) - List your flows +- [auth0 flows open](auth0_flows_open.md) - Open a flow in the Auth0 Dashboard +- [auth0 flows show](auth0_flows_show.md) - Show a flow +- [auth0 flows update](auth0_flows_update.md) - Update a flow +- [auth0 flows vault](auth0_flows_vault.md) - Manage Flow vault connections + + diff --git a/docs/auth0_flows_vault.md b/docs/auth0_flows_vault.md new file mode 100644 index 000000000..5c1d73774 --- /dev/null +++ b/docs/auth0_flows_vault.md @@ -0,0 +1,14 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 flows vault + +Manage the vault connections that store credentials for flow integrations. + +## Commands + +- [auth0 flows vault connections](auth0_flows_vault_connections.md) - Manage Flow vault connections. +- [auth0 flows vault open](auth0_flows_vault_open.md) - Open the Vault in the Auth0 Dashboard + diff --git a/docs/auth0_flows_vault_connections.md b/docs/auth0_flows_vault_connections.md new file mode 100644 index 000000000..f271927c5 --- /dev/null +++ b/docs/auth0_flows_vault_connections.md @@ -0,0 +1,17 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 flows vault connections + +List, inspect, create, update, and delete flow vault connections. + +## Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + diff --git a/docs/auth0_flows_vault_connections_create.md b/docs/auth0_flows_vault_connections_create.md new file mode 100644 index 000000000..a306a847e --- /dev/null +++ b/docs/auth0_flows_vault_connections_create.md @@ -0,0 +1,58 @@ +--- +layout: default +parent: auth0 flows vault connections +has_toc: false +--- +# auth0 flows vault connections create + +Create a new vault connection. + +A name is required, supplied with `--name` or the interactive prompt, along with an app id via `--app-id`. Setup credentials are authored interactively or supplied with `--setup-file`; the file must contain only the setup body, not a name. Run `--setup-template --app-id ` to print the setup credentials template for a given app. + +## Usage +``` +auth0 flows vault connections create [flags] +``` + +## Examples + +``` + auth0 flows vault connections create + auth0 flows vault connections create --name "My Connection" --app-id SLACK + auth0 flows vault connections create --name "My Connection" --app-id SLACK --setup-file ./setup.json + auth0 flows vault connections create --setup-template --app-id SLACK > setup.json +``` + + +## Flags + +``` + --app-id string Identifier of the app the Vault connection integrates with (e.g. HTTP, SLACK). + --json Output in json format. + --json-compact Output in compact json format. + --name string Name of the Vault connection. + -f, --setup-file string Path to a JSON file containing the vault connection setup credentials. Run with --setup-template --app-id to see the expected setup schema for a given app. + --setup-template Print the setup credentials template for the given --app-id and exit. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + + diff --git a/docs/auth0_flows_vault_connections_delete.md b/docs/auth0_flows_vault_connections_delete.md new file mode 100644 index 000000000..d319f3ce9 --- /dev/null +++ b/docs/auth0_flows_vault_connections_delete.md @@ -0,0 +1,55 @@ +--- +layout: default +parent: auth0 flows vault connections +has_toc: false +--- +# auth0 flows vault connections delete + +Delete a vault connection. + +To delete interactively, use `auth0 flows vault connections delete` with no arguments. + +To delete non-interactively, supply the connection id and the `--force` flag. + +## Usage +``` +auth0 flows vault connections delete [flags] +``` + +## Examples + +``` + auth0 flows vault connections delete + auth0 flows vault connections rm + auth0 flows vault connections delete + auth0 flows vault connections delete --force +``` + + +## Flags + +``` + --force Skip confirmation. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + + diff --git a/docs/auth0_flows_vault_connections_list.md b/docs/auth0_flows_vault_connections_list.md new file mode 100644 index 000000000..e93f01f01 --- /dev/null +++ b/docs/auth0_flows_vault_connections_list.md @@ -0,0 +1,53 @@ +--- +layout: default +parent: auth0 flows vault connections +has_toc: false +--- +# auth0 flows vault connections list + +List your existing vault connections. To create one, run: `auth0 flows vault connections create`. + +## Usage +``` +auth0 flows vault connections list [flags] +``` + +## Examples + +``` + auth0 flows vault connections list + auth0 flows vault connections ls --number 100 + auth0 flows vault connections ls --json +``` + + +## Flags + +``` + --csv Output in csv format. + --json Output in json format. + --json-compact Output in compact json format. + -n, --number int Number of connections to retrieve. Fetched across pages. (default 100) +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + + diff --git a/docs/auth0_flows_vault_connections_show.md b/docs/auth0_flows_vault_connections_show.md new file mode 100644 index 000000000..25f80140a --- /dev/null +++ b/docs/auth0_flows_vault_connections_show.md @@ -0,0 +1,51 @@ +--- +layout: default +parent: auth0 flows vault connections +has_toc: false +--- +# auth0 flows vault connections show + +Display information about a vault connection. Secret values are never returned by the API. + +## Usage +``` +auth0 flows vault connections show [flags] +``` + +## Examples + +``` + auth0 flows vault connections show + auth0 flows vault connections show + auth0 flows vault connections show --json +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + + diff --git a/docs/auth0_flows_vault_connections_update.md b/docs/auth0_flows_vault_connections_update.md new file mode 100644 index 000000000..f1a35d07e --- /dev/null +++ b/docs/auth0_flows_vault_connections_update.md @@ -0,0 +1,54 @@ +--- +layout: default +parent: auth0 flows vault connections +has_toc: false +--- +# auth0 flows vault connections update + +Update a vault connection. + +Passing `--setup-file` replaces the connection's setup credentials; the file must contain only the setup body, not a name. Passing `--name` renames the connection; omit it to keep the current name. Run `auth0 flows vault connections create --setup-template --app-id ` to see the setup schema. + +## Usage +``` +auth0 flows vault connections update [flags] +``` + +## Examples + +``` + auth0 flows vault connections update --name "New Name" + auth0 flows vault connections update --setup-file ./setup.json +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. + --name string Name of the Vault connection. + -f, --setup-file string Path to a JSON file containing the vault connection setup credentials. Run with --setup-template --app-id to see the expected setup schema for a given app. +``` + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections create](auth0_flows_vault_connections_create.md) - Create a new vault connection +- [auth0 flows vault connections delete](auth0_flows_vault_connections_delete.md) - Delete a vault connection +- [auth0 flows vault connections list](auth0_flows_vault_connections_list.md) - List your vault connections +- [auth0 flows vault connections show](auth0_flows_vault_connections_show.md) - Show a vault connection +- [auth0 flows vault connections update](auth0_flows_vault_connections_update.md) - Update a vault connection + + diff --git a/docs/auth0_flows_vault_open.md b/docs/auth0_flows_vault_open.md new file mode 100644 index 000000000..69c2a64a2 --- /dev/null +++ b/docs/auth0_flows_vault_open.md @@ -0,0 +1,42 @@ +--- +layout: default +parent: auth0 flows vault +has_toc: false +--- +# auth0 flows vault open + +Open a Vault app's page in the Auth0 Dashboard. This opens the app's Vault page (for example AUTH0, JWT, HTTP, or SLACK), not a specific connection. + +## Usage +``` +auth0 flows vault open [flags] +``` + +## Examples + +``` + auth0 flows vault open + auth0 flows vault open HTTP + auth0 flows vault open SLACK +``` + + + + +## Inherited Flags + +``` + --agent-mode Output JSON, disable prompts and colors. Auto-enabled for AI agents; set AUTH0_AGENT_MODE=false to disable. + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 flows vault connections](auth0_flows_vault_connections.md) - Manage Flow vault connections. +- [auth0 flows vault open](auth0_flows_vault_open.md) - Open the Vault in the Auth0 Dashboard + + diff --git a/docs/index.md b/docs/index.md index df88b74f0..c08e6ed14 100644 --- a/docs/index.md +++ b/docs/index.md @@ -97,6 +97,7 @@ The help for any command can also be emitted as JSON by combining `--help` with - [auth0 domains](auth0_domains.md) - Manage custom domains - [auth0 email](auth0_email.md) - Manage email settings and configure email providers - [auth0 event-streams](auth0_event-streams.md) - Manage Event Stream +- [auth0 flows](auth0_flows.md) - Manage Flows - [auth0 login](auth0_login.md) - Authenticate the Auth0 CLI - [auth0 logout](auth0_logout.md) - Log out of a tenant's session - [auth0 logs](auth0_logs.md) - View tenant logs diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 89c5245f9..87c24ce7d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -133,7 +133,9 @@ var RequiredScopes = []string{ "read:phone_templates", "create:email_templates", "read:email_templates", "update:email_templates", "create:email_provider", "read:email_provider", "update:email_provider", "delete:email_provider", - "read:flows", "read:forms", "read:flows_vault_connections", + "read:flows", "create:flows", "update:flows", "delete:flows", + "read:flows_executions", "read:forms", + "read:flows_vault_connections", "create:flows_vault_connections", "update:flows_vault_connections", "delete:flows_vault_connections", "read:connections", "update:connections", "read:connections_options", "update:connections_options", "read:client_keys", "read:logs", "read:tenant_settings", "update:tenant_settings", "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", diff --git a/internal/auth/scopes_test.go b/internal/auth/scopes_test.go index 6d013ed64..b35e0bd39 100644 --- a/internal/auth/scopes_test.go +++ b/internal/auth/scopes_test.go @@ -33,6 +33,8 @@ func TestRequiredScopes(t *testing.T) { t.Run("Verify special scopes", func(t *testing.T) { list := []string{ "read:branding", "update:branding", + "read:flows", "create:flows", "update:flows", "delete:flows", "read:flows_executions", + "read:flows_vault_connections", "create:flows_vault_connections", "update:flows_vault_connections", "delete:flows_vault_connections", "read:connections", "update:connections", "read:connections_options", "update:connections_options", "read:email_templates", "update:email_templates", "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index f566abae5..de30fc442 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -80,6 +80,9 @@ type APIV3 struct { ClientGrant ClientGrantAPIV3 ClientGrantOrganization ClientGrantOrganizationAPIV3 Events EventsAPIV3 + Flow FlowAPIV3 + FlowExecution FlowExecutionAPIV3 + FlowVaultConnection FlowVaultConnectionAPIV3 PhoneNotificationTemplate PhoneNotificationTemplateAPI Session SessionAPIV3 RefreshToken RefreshTokenAPIV3 @@ -95,6 +98,9 @@ func NewAPIV3(m *managementv3.Management) *APIV3 { ClientGrant: m.ClientGrants, ClientGrantOrganization: m.ClientGrants.Organizations, Events: m.Events, + Flow: m.Flows, + FlowExecution: m.Flows.Executions, + FlowVaultConnection: m.Flows.Vault.Connections, PhoneNotificationTemplate: m.Branding.Phone.Templates, Session: m.Sessions, RefreshToken: m.RefreshTokens, diff --git a/internal/auth0/flow_v3.go b/internal/auth0/flow_v3.go new file mode 100644 index 000000000..053f6f012 --- /dev/null +++ b/internal/auth0/flow_v3.go @@ -0,0 +1,94 @@ +//go:generate mockgen -source=flow_v3.go -destination=mock/flow_v3_mock.go -package=mock + +package auth0 + +import ( + "context" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/auth0/go-auth0/v3/management/core" + "github.com/auth0/go-auth0/v3/management/option" +) + +// FlowSummaryPage aliases the paginated flows list response. The alias keeps the +// interface return type a single identifier so mockgen's source parser can handle +// it (it cannot parse the multi-type-parameter generic inline). +type FlowSummaryPage = core.Page[*int, *managementv3.FlowSummary, *managementv3.ListFlowsOffsetPaginatedResponseContent] + +// FlowExecutionSummaryPage aliases the paginated flow-executions list response. +type FlowExecutionSummaryPage = core.Page[*string, *managementv3.FlowExecutionSummary, *managementv3.ListFlowExecutionsPaginatedResponseContent] + +// FlowsVaultConnectionSummaryPage aliases the paginated vault-connections list response. +type FlowsVaultConnectionSummaryPage = core.Page[*int, *managementv3.FlowsVaultConnectionSummary, *managementv3.ListFlowsVaultConnectionsOffsetPaginatedResponseContent] + +// FlowAPIV3 is the V3 SDK interface for the /flows endpoint. Create, read, and +// update go through the raw HTTP client to preserve the flow action graph that +// the typed request models would drop, so only paging and delete live here. +type FlowAPIV3 interface { + // List flows. + // + // Required scope: `read:flows`. + List( + ctx context.Context, + request *managementv3.ListFlowsRequestParameters, + opts ...option.RequestOption, + ) (*FlowSummaryPage, error) + + // Delete a flow. + // + // Required scope: `delete:flows`. + Delete( + ctx context.Context, + id string, + opts ...option.RequestOption, + ) error +} + +// FlowExecutionAPIV3 is the V3 SDK interface for the /flows/{id}/executions +// endpoint. Executions are runtime-produced, so the surface is read and delete +// only. +type FlowExecutionAPIV3 interface { + // List flow executions. + // + // Required scope: `read:flows_executions`. + List( + ctx context.Context, + flowID string, + request *managementv3.ListFlowExecutionsRequestParameters, + opts ...option.RequestOption, + ) (*FlowExecutionSummaryPage, error) + + // Delete a flow execution. + // + // Required scope: `delete:flows_executions`. + Delete( + ctx context.Context, + flowID string, + executionID string, + opts ...option.RequestOption, + ) error +} + +// FlowVaultConnectionAPIV3 is the V3 SDK interface for the +// /flows/vault/connections endpoint. Create and update go through the raw HTTP +// client because the typed request model is a large per-provider union, so only +// paging and delete live here. +type FlowVaultConnectionAPIV3 interface { + // List vault connections. + // + // Required scope: `read:flows_vault_connections`. + List( + ctx context.Context, + request *managementv3.ListFlowsVaultConnectionsRequestParameters, + opts ...option.RequestOption, + ) (*FlowsVaultConnectionSummaryPage, error) + + // Delete a vault connection. + // + // Required scope: `delete:flows_vault_connections`. + Delete( + ctx context.Context, + id string, + opts ...option.RequestOption, + ) error +} diff --git a/internal/cli/flows.go b/internal/cli/flows.go new file mode 100644 index 000000000..fe726a9b8 --- /dev/null +++ b/internal/cli/flows.go @@ -0,0 +1,757 @@ +package cli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/auth0/go-auth0/v3/management/core" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" +) + +// flowCreateSkeleton seeds the editor for interactive flow creation. +const flowCreateSkeleton = `{ + "actions": [] +} +` + +const flowCreateExample = `{ + "actions": [ + { + "id": "step_http", + "type": "HTTP", + "action": "SEND_REQUEST", + "allow_failure": false, + "mask_output": false, + "params": { + "method": "GET", + "url": "https://api.example.com/enrich", + "content_type": "JSON" + } + } + ] +} +` + +var ( + flowID = Argument{ + Name: "Id", + Help: "Id of the Flow.", + } + + flowExecutionID = Argument{ + Name: "Execution Id", + Help: "Id of the Flow execution.", + } + + flowName = Flag{ + Name: "Name", + LongForm: "name", + Help: "Name of the Flow.", + } + + flowFile = Flag{ + Name: "Actions File", + LongForm: "actions-file", + ShortForm: "f", + Help: "Path to a JSON file containing the flow actions body. Run with --actions-template to see the expected format.", + } + + flowActionsTemplate = Flag{ + Name: "Actions Template", + LongForm: "actions-template", + Help: "Print the actions template for --actions-file and exit.", + } + + flowHydrate = Flag{ + Name: "Hydrate", + LongForm: "hydrate", + Help: "Hydrate the response with the number of forms referencing each flow.", + } +) + +func flowsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "flows", + Short: "Manage Flows", + Long: "Flows let you orchestrate custom logic during authentication and other journeys, " + + "chaining actions such as HTTP requests and vault-backed integrations.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listFlowsCmd(cli)) + cmd.AddCommand(showFlowCmd(cli)) + cmd.AddCommand(createFlowCmd(cli)) + cmd.AddCommand(updateFlowCmd(cli)) + cmd.AddCommand(deleteFlowCmd(cli)) + cmd.AddCommand(openFlowCmd(cli)) + cmd.AddCommand(flowExecutionsCmd(cli)) + cmd.AddCommand(flowVaultCmd(cli)) + + return cmd +} + +func listFlowsCmd(cli *cli) *cobra.Command { + var inputs struct { + Number int + Hydrate bool + Synchronous bool + } + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List your flows", + Long: "List your existing flows. To create one, run: `auth0 flows create`.", + Example: ` auth0 flows list + auth0 flows ls + auth0 flows ls --number 100 + auth0 flows ls --hydrate + auth0 flows ls --json`, + RunE: func(cmd *cobra.Command, args []string) error { + params := &managementv3.ListFlowsRequestParameters{} + if inputs.Hydrate { + params.Hydrate = []*managementv3.ListFlowsRequestParametersHydrateEnum{ + managementv3.ListFlowsRequestParametersHydrateEnumFormCount.Ptr(), + } + } + if cmd.Flags().Changed("synchronous") { + params.Synchronous = &inputs.Synchronous + } + + var flows []*managementv3.FlowSummary + if err := ansi.Waiting(func() (err error) { + flows, err = collectFlows(cmd.Context(), cli, params, inputs.Number) + return err + }); err != nil { + return fmt.Errorf("failed to list flows: %w", err) + } + + return cli.renderer.FlowsList(flows) + }, + } + + cmd.Flags().IntVarP(&inputs.Number, "number", "n", 100, "Number of flows to retrieve. Fetched across pages.") + flowHydrate.RegisterBool(cmd, &inputs.Hydrate, false) + cmd.Flags().BoolVar(&inputs.Synchronous, "synchronous", false, "Filter to synchronous (true) or asynchronous (false) flows.") + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + + return cmd +} + +func showFlowCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show a flow", + Long: "Display information about a flow.", + Example: ` auth0 flows show + auth0 flows show + auth0 flows show --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := flowID.Pick(cmd, &inputs.ID, cli.flowPickerOptions); err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + flow, err := cli.flowRawGet(cmd.Context(), inputs.ID) + if err != nil { + return fmt.Errorf("failed to read flow with ID %q: %w", inputs.ID, err) + } + + return cli.renderer.FlowShowRaw(flow) + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func createFlowCmd(cli *cli) *cobra.Command { + var inputs struct { + Name string + File string + ActionsTemplate bool + } + + cmd := &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new flow", + Long: "Create a new flow.\n\n" + + "A name is required, supplied with `--name` or the interactive prompt. The actions graph " + + "is authored interactively or supplied with `--actions-file`; the file must contain only the " + + "actions body, not a name. " + + "Run `auth0 flows create --actions-template > flow.json` to generate an actions template.", + Example: ` auth0 flows create + auth0 flows create --name "My Flow" + auth0 flows create --actions-template > flow.json + auth0 flows create --name "My Flow" --actions-file ./flow.json`, + RunE: func(cmd *cobra.Command, args []string) error { + if inputs.ActionsTemplate { + cli.renderer.Warnf("Flow actions body. Save to a file and pass with --actions-file.") + cli.renderer.FlowExport(flowCreateExample) + return nil + } + + if err := flowName.Ask(cmd, &inputs.Name, nil); err != nil { + return err + } + if inputs.Name == "" { + return errors.New("a flow name is required; supply --name") + } + + var rawBody json.RawMessage + if inputs.File != "" { + fileBody, err := os.ReadFile(inputs.File) + if err != nil { + return fmt.Errorf("failed to read flow file %q: %w", inputs.File, err) + } + if err := json.Unmarshal(fileBody, &rawBody); err != nil { + return fmt.Errorf("failed to parse flow body: %w", err) + } + if err := rejectRawNameField(rawBody, "actions file"); err != nil { + return err + } + } else { + var editActions bool + if err := prompt.AskBool("Do you want to edit the actions now?", &editActions, false); err != nil { + return err + } + if editActions { + if err := editJSONBody(cli, "flow", flowCreateSkeleton, &rawBody); err != nil { + return err + } + if err := rejectRawNameField(rawBody, "flow actions"); err != nil { + return err + } + } else { + rawBody = json.RawMessage(flowCreateSkeleton) + } + } + + rawBody, err := applyRawNameOverride(rawBody, inputs.Name) + if err != nil { + return fmt.Errorf("failed to parse flow body: %w", err) + } + + created, err := cli.flowRawCreate(cmd.Context(), rawBody) + if err != nil { + return fmt.Errorf("failed to create flow: %w", err) + } + return cli.renderer.FlowCreateRaw(created) + }, + } + + flowName.RegisterString(cmd, &inputs.Name, "") + flowFile.RegisterString(cmd, &inputs.File, "") + flowActionsTemplate.RegisterBool(cmd, &inputs.ActionsTemplate, false) + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func updateFlowCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + Name string + File string + } + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.MaximumNArgs(1), + Short: "Update a flow", + Long: "Update a flow.\n\n" + + "Passing `--actions-file` replaces the flow's actions graph; the file must contain only the " + + "actions body, not a name. Passing `--name` renames the flow; omit it to keep the current name.", + Example: ` auth0 flows update --name "New Name" + auth0 flows update --actions-file ./flow.json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.ID = args[0] + } else { + if err := flowID.Pick(cmd, &inputs.ID, cli.flowPickerOptions); err != nil { + return err + } + } + + current, err := cli.flowRawGet(cmd.Context(), inputs.ID) + if err != nil { + return fmt.Errorf("failed to read flow with ID %q: %w", inputs.ID, err) + } + + currentName, err := rawJSONStringField(current, "name") + if err != nil { + return fmt.Errorf("failed to read flow name: %w", err) + } + + if err := flowName.AskU(cmd, &inputs.Name, ¤tName); err != nil { + return err + } + if inputs.Name == "" { + inputs.Name = currentName + } + + var rawBody json.RawMessage + if inputs.File != "" { + fileBody, err := os.ReadFile(inputs.File) + if err != nil { + return fmt.Errorf("failed to read flow file %q: %w", inputs.File, err) + } + if err := json.Unmarshal(fileBody, &rawBody); err != nil { + return fmt.Errorf("failed to parse flow body: %w", err) + } + if err := rejectRawNameField(rawBody, "actions file"); err != nil { + return err + } + } else { + var updateActions bool + if err := prompt.AskBool("Do you want to update the actions?", &updateActions, false); err != nil { + return err + } + if updateActions { + var currentBody map[string]json.RawMessage + if err := json.Unmarshal(current, ¤tBody); err != nil { + return fmt.Errorf("failed to parse flow with ID %q: %w", inputs.ID, err) + } + actions := currentBody["actions"] + if actions == nil { + actions = json.RawMessage(`[]`) + } + seedBytes, err := json.MarshalIndent(map[string]json.RawMessage{"actions": actions}, "", " ") + if err != nil { + return fmt.Errorf("failed to build flow actions seed: %w", err) + } + if err := editJSONBody(cli, "flow", string(seedBytes), &rawBody); err != nil { + return err + } + if err := rejectRawNameField(rawBody, "flow actions"); err != nil { + return err + } + } else { + rawBody = json.RawMessage(`{}`) + } + } + + rawBody, err = applyRawNameOverride(rawBody, inputs.Name) + if err != nil { + return fmt.Errorf("failed to parse flow body: %w", err) + } + + updated, err := cli.flowRawUpdate(cmd.Context(), inputs.ID, rawBody) + if err != nil { + return fmt.Errorf("failed to update flow with ID %q: %w", inputs.ID, err) + } + return cli.renderer.FlowUpdateRaw(updated) + }, + } + + flowName.RegisterStringU(cmd, &inputs.Name, "") + flowFile.RegisterStringU(cmd, &inputs.File, "") + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func deleteFlowCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Aliases: []string{"rm"}, + Args: cobra.ArbitraryArgs, + Short: "Delete a flow", + Long: "Delete a flow.\n\n" + + "To delete interactively, use `auth0 flows delete` with no arguments.\n\n" + + "To delete non-interactively, supply the flow id and the `--force` flag to skip confirmation.", + Example: ` auth0 flows delete + auth0 flows rm + auth0 flows delete + auth0 flows delete --force + auth0 flows delete `, + RunE: func(cmd *cobra.Command, args []string) error { + var ids []string + if len(args) == 0 { + if err := flowID.PickMany(cmd, &ids, cli.flowPickerOptions); err != nil { + return err + } + } else { + ids = args + } + + if !cli.force && cli.agentMode { + return errDestructiveNoConfirm + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.ProgressBar("Deleting flow(s)", ids, func(_ int, id string) error { + if id == "" { + return nil + } + if err := cli.apiv3.Flow.Delete(cmd.Context(), id); err != nil { + return fmt.Errorf("failed to delete flow with ID %q: %w", id, err) + } + return nil + }) + }, + } + + cmd.Flags().BoolVar(&cli.force, "force", false, "Skip confirmation.") + + return cmd +} + +// --- Executions ---. + +func flowExecutionsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "executions", + Short: "Manage Flow executions", + Long: "Inspect the runtime executions produced when a flow runs.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listFlowExecutionsCmd(cli)) + cmd.AddCommand(showFlowExecutionCmd(cli)) + cmd.AddCommand(deleteFlowExecutionCmd(cli)) + + return cmd +} + +func listFlowExecutionsCmd(cli *cli) *cobra.Command { + var inputs struct { + FlowID string + Number int + From string + Take int + } + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), + Short: "List a flow's executions", + Long: "List the executions produced by a flow.", + Example: ` auth0 flows executions list + auth0 flows executions ls --number 100 + auth0 flows executions list --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.FlowID = args[0] + } else { + if err := flowID.Pick(cmd, &inputs.FlowID, cli.flowPickerOptions); err != nil { + return err + } + } + + params := &managementv3.ListFlowExecutionsRequestParameters{} + if inputs.From != "" { + params.From = &inputs.From + } + if cmd.Flags().Changed("take") { + params.Take = &inputs.Take + } + + var executions []*managementv3.FlowExecutionSummary + if err := ansi.Waiting(func() (err error) { + executions, err = collectFlowExecutions(cmd.Context(), cli, inputs.FlowID, params, inputs.Number) + return err + }); err != nil { + return fmt.Errorf("failed to list flow executions: %w", err) + } + + return cli.renderer.FlowExecutionsList(executions) + }, + } + + cmd.Flags().IntVarP(&inputs.Number, "number", "n", 100, "Number of executions to retrieve. Fetched across pages.") + cmd.Flags().StringVar(&inputs.From, "from", "", "Cursor id from which to start selection.") + cmd.Flags().IntVar(&inputs.Take, "take", 0, "Number of executions to retrieve per page.") + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + + return cmd +} + +func showFlowExecutionCmd(cli *cli) *cobra.Command { + var inputs struct { + FlowID string + ExecutionID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(2), + Short: "Show a flow execution", + Long: "Display information about a flow execution.", + Example: ` auth0 flows executions show + auth0 flows executions show --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.FlowID = args[0] + } else { + if err := flowID.Pick(cmd, &inputs.FlowID, cli.flowPickerOptions); err != nil { + return err + } + } + + if len(args) > 1 { + inputs.ExecutionID = args[1] + } else { + if err := flowExecutionID.Pick(cmd, &inputs.ExecutionID, cli.flowExecutionPickerOptions(inputs.FlowID)); err != nil { + return err + } + } + + execution, err := cli.flowExecutionRawGet(cmd.Context(), inputs.FlowID, inputs.ExecutionID) + if err != nil { + return fmt.Errorf("failed to read flow execution with ID %q: %w", inputs.ExecutionID, err) + } + + return cli.renderer.FlowExecutionShowRaw(execution) + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func deleteFlowExecutionCmd(cli *cli) *cobra.Command { + var inputs struct { + FlowID string + } + + cmd := &cobra.Command{ + Use: "delete", + Aliases: []string{"rm"}, + Args: cobra.ArbitraryArgs, + Short: "Delete a flow execution", + Long: "Delete one or more executions of a flow.\n\n" + + "Supply the flow id followed by the execution ids. Use `--force` to skip confirmation.", + Example: ` auth0 flows executions delete + auth0 flows executions rm --force + auth0 flows executions delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.FlowID = args[0] + } else { + if err := flowID.Pick(cmd, &inputs.FlowID, cli.flowPickerOptions); err != nil { + return err + } + } + + var ids []string + if len(args) > 1 { + ids = args[1:] + } else { + if err := flowExecutionID.PickMany(cmd, &ids, cli.flowExecutionPickerOptions(inputs.FlowID)); err != nil { + return err + } + } + + if !cli.force && cli.agentMode { + return errDestructiveNoConfirm + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.ProgressBar("Deleting flow execution(s)", ids, func(_ int, id string) error { + if id == "" { + return nil + } + if err := cli.apiv3.FlowExecution.Delete(cmd.Context(), inputs.FlowID, id); err != nil { + return fmt.Errorf("failed to delete flow execution with ID %q: %w", id, err) + } + return nil + }) + }, + } + + cmd.Flags().BoolVar(&cli.force, "force", false, "Skip confirmation.") + + return cmd +} + +// --- Open in Dashboard ---. + +func openFlowCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "open", + Args: cobra.MaximumNArgs(1), + Short: "Open a flow in the Auth0 Dashboard", + Long: "Open a flow's page in the Auth0 Dashboard flow builder.", + Example: ` auth0 flows open + auth0 flows open `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := flowID.Pick(cmd, &inputs.ID, cli.flowPickerOptions); err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + openBuilderURL(cli, fmt.Sprintf("flows/%s/edit", inputs.ID)) + + return nil + }, + } + + return cmd +} + +// --- Raw HTTP helpers ---. + +// flowRawGet fetches a flow through the v1 client's HTTP layer without using the +// v3 SDK's lossy flow-action unions. +func (c *cli) flowRawGet(ctx context.Context, id string) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodGet, c.api.HTTPClient.URI("flows", id), nil) +} + +func (c *cli) flowRawCreate(ctx context.Context, body json.RawMessage) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodPost, c.api.HTTPClient.URI("flows"), body) +} + +func (c *cli) flowRawUpdate(ctx context.Context, id string, body json.RawMessage) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodPatch, c.api.HTTPClient.URI("flows", id), body) +} + +func (c *cli) flowExecutionRawGet(ctx context.Context, flowID, executionID string) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodGet, c.api.HTTPClient.URI("flows", flowID, "executions", executionID), nil) +} + +// --- Paging + pickers ---. + +func collectFlows(ctx context.Context, cli *cli, params *managementv3.ListFlowsRequestParameters, limit int) ([]*managementv3.FlowSummary, error) { + page, err := cli.apiv3.Flow.List(ctx, params) + if err != nil { + return nil, err + } + + var out []*managementv3.FlowSummary + for page != nil { + for _, f := range page.Results { + out = append(out, f) + if limit > 0 && len(out) >= limit { + return out, nil + } + } + + page, err = page.GetNextPage(ctx) + if errors.Is(err, core.ErrNoPages) { + break + } + if err != nil { + return out, err + } + } + + return out, nil +} + +func collectFlowExecutions(ctx context.Context, cli *cli, flowID string, params *managementv3.ListFlowExecutionsRequestParameters, limit int) ([]*managementv3.FlowExecutionSummary, error) { + page, err := cli.apiv3.FlowExecution.List(ctx, flowID, params) + if err != nil { + return nil, err + } + + var out []*managementv3.FlowExecutionSummary + for page != nil { + for _, e := range page.Results { + out = append(out, e) + if limit > 0 && len(out) >= limit { + return out, nil + } + } + + page, err = page.GetNextPage(ctx) + if errors.Is(err, core.ErrNoPages) { + break + } + if err != nil { + return out, err + } + } + + return out, nil +} + +func (c *cli) flowPickerOptions(ctx context.Context) (pickerOptions, error) { + flows, err := collectFlows(ctx, c, &managementv3.ListFlowsRequestParameters{}, 0) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, f := range flows { + label := fmt.Sprintf("%s %s", f.GetName(), ansi.Faint("("+f.GetID()+")")) + opts = append(opts, pickerOption{value: f.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("there are currently no flows to choose from. Create one by running: `auth0 flows create`") + } + + return opts, nil +} + +// flowExecutionPickerOptions returns a picker over the executions of a specific +// flow, so it must be bound to the flow id before use. +func (c *cli) flowExecutionPickerOptions(flowID string) pickerOptionsFunc { + return func(ctx context.Context) (pickerOptions, error) { + executions, err := collectFlowExecutions(ctx, c, flowID, &managementv3.ListFlowExecutionsRequestParameters{}, 0) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, e := range executions { + label := fmt.Sprintf("%s %s", e.GetStatus(), ansi.Faint("("+e.GetID()+")")) + opts = append(opts, pickerOption{value: e.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("this flow has no executions to choose from") + } + + return opts, nil + } +} diff --git a/internal/cli/flows_test.go b/internal/cli/flows_test.go new file mode 100644 index 000000000..2b845b433 --- /dev/null +++ b/internal/cli/flows_test.go @@ -0,0 +1,250 @@ +package cli + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/auth0/go-auth0/management" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/config" + "github.com/auth0/auth0-cli/internal/display" +) + +type rawHTTPClientStub struct { + method string + payload interface{} + response json.RawMessage +} + +func (s *rawHTTPClientStub) NewRequest( + ctx context.Context, + method string, + uri string, + payload interface{}, + _ ...management.RequestOption, +) (*http.Request, error) { + s.method = method + s.payload = payload + return http.NewRequestWithContext(ctx, method, uri, nil) +} + +func (s *rawHTTPClientStub) Do(_ *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(string(s.response))), + }, nil +} + +func (s *rawHTTPClientStub) Request( + context.Context, + string, + string, + interface{}, + ...management.RequestOption, +) error { + return nil +} + +func (s *rawHTTPClientStub) URI(path ...string) string { + return "https://example.test/api/v2/" + strings.Join(path, "/") +} + +func newRawTestCLI(stub *rawHTTPClientStub, stdout *bytes.Buffer) *cli { + return &cli{ + api: &auth0.API{HTTPClient: stub}, + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: stdout, + }, + } +} + +func TestApplyRawNameOverride(t *testing.T) { + body := json.RawMessage(`{"name":"Original","actions":[{"id":"a1","type":"HTTP"}]}`) + + got, err := applyRawNameOverride(body, "Renamed") + require.NoError(t, err) + + var obj map[string]json.RawMessage + require.NoError(t, json.Unmarshal(got, &obj)) + assert.JSONEq(t, `"Renamed"`, string(obj["name"])) + assert.Contains(t, string(obj["actions"]), `"HTTP"`) +} + +func TestApplyRawNameOverrideNoopWhenEmpty(t *testing.T) { + body := json.RawMessage(`{"actions":[]}`) + + got, err := applyRawNameOverride(body, "") + require.NoError(t, err) + assert.Equal(t, body, got) +} + +func TestApplyRawNameOverrideRejectsNonObject(t *testing.T) { + _, err := applyRawNameOverride(json.RawMessage(`[]`), "New") + assert.ErrorContains(t, err, "cannot unmarshal array") +} + +func TestFormatBuilderPageURL(t *testing.T) { + cfg := &config.Config{ + Tenants: config.Tenants{ + "example.us.auth0.com": {Name: "example"}, + "my-tenant.eu.auth0.com": {Name: "my-tenant"}, + "dev-tti06f6y.auth0.com": {Name: "dev-tti06f6y"}, + "no-name.us.auth0.com": {Name: ""}, + }, + } + + tests := []struct { + name string + tenant string + path string + expected string + }{ + { + name: "builds a flow edit URL", + tenant: "example.us.auth0.com", + path: "flows/af_123/edit", + expected: "https://forms.auth0.com/tenants/us/example/flows/af_123/edit", + }, + { + name: "builds a vault app URL in a non-us region", + tenant: "my-tenant.eu.auth0.com", + path: "vault/apps/HTTP/edit", + expected: "https://forms.auth0.com/tenants/eu/my-tenant/vault/apps/HTTP/edit", + }, + { + name: "defaults to us for a three-part PUS1 domain", + tenant: "dev-tti06f6y.auth0.com", + path: "vault/apps/JWT/edit", + expected: "https://forms.auth0.com/tenants/us/dev-tti06f6y/vault/apps/JWT/edit", + }, + { + name: "returns empty when the path is missing", + tenant: "example.us.auth0.com", + path: "", + expected: "", + }, + { + name: "returns empty when the tenant is missing", + tenant: "", + path: "flows/af_123/edit", + expected: "", + }, + { + name: "returns empty when the domain has too few parts", + tenant: "invalid", + path: "flows/af_123/edit", + expected: "", + }, + { + name: "returns empty when the tenant name is unknown", + tenant: "no-name.us.auth0.com", + path: "flows/af_123/edit", + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, formatBuilderPageURL(test.tenant, cfg, test.path)) + }) + } +} + +func TestCreateFlowCmdScaffoldFromName(t *testing.T) { + stub := &rawHTTPClientStub{ + response: json.RawMessage(`{"id":"flow_1","name":"My Flow","actions":[]}`), + } + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createFlowCmd(c) + cmd.SetArgs([]string{"--name", "My Flow"}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, stub.method) + require.IsType(t, json.RawMessage{}, stub.payload) + assert.JSONEq(t, `{"name":"My Flow","actions":[]}`, string(stub.payload.(json.RawMessage))) + assert.Contains(t, stdout.String(), "My Flow") +} + +func TestCreateFlowCmdFromFilePreservesActions(t *testing.T) { + body := []byte(`{"actions":[{"id":"a1","type":"HTTP","action":"SEND_REQUEST","params":{"method":"GET","url":"https://x.test"}}]}`) + path := filepath.Join(t.TempDir(), "flow.json") + require.NoError(t, os.WriteFile(path, body, 0600)) + + stub := &rawHTTPClientStub{ + response: json.RawMessage(`{"id":"flow_2","name":"Rich Flow","actions":[{"id":"a1","type":"HTTP"}]}`), + } + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createFlowCmd(c) + cmd.SetArgs([]string{"--actions-file", path, "--name", "Rich Flow"}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, stub.method) + require.IsType(t, json.RawMessage{}, stub.payload) + assert.Contains(t, string(stub.payload.(json.RawMessage)), `"SEND_REQUEST"`) + // The name comes from --name and is injected into the body, not read from the file. + assert.Contains(t, string(stub.payload.(json.RawMessage)), `"Rich Flow"`) + assert.Contains(t, stdout.String(), "1 actions") +} + +func TestCreateFlowCmdRejectsNameInActionsFile(t *testing.T) { + body := []byte(`{"name":"Rich Flow","actions":[]}`) + path := filepath.Join(t.TempDir(), "flow.json") + require.NoError(t, os.WriteFile(path, body, 0600)) + + stub := &rawHTTPClientStub{} + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createFlowCmd(c) + cmd.SetArgs([]string{"--actions-file", path, "--name", "Rich Flow"}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "must not contain a top-level \"name\" field") +} + +func TestCreateFlowCmdRequiresName(t *testing.T) { + stub := &rawHTTPClientStub{} + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createFlowCmd(c) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "flow name is required") +} + +func TestUpdateFlowCmdNameOnlyMergePreservesActions(t *testing.T) { + stub := &rawHTTPClientStub{ + response: json.RawMessage(`{"id":"flow_3","name":"New Name","actions":[{"id":"a1","type":"HTTP"}]}`), + } + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := updateFlowCmd(c) + cmd.SetArgs([]string{"flow_3", "--name", "New Name"}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPatch, stub.method) + require.IsType(t, json.RawMessage{}, stub.payload) + // A name-only merge must send only the name so the API preserves the actions graph. + assert.JSONEq(t, `{"name":"New Name"}`, string(stub.payload.(json.RawMessage))) +} diff --git a/internal/cli/flows_vault.go b/internal/cli/flows_vault.go new file mode 100644 index 000000000..b5f696d9f --- /dev/null +++ b/internal/cli/flows_vault.go @@ -0,0 +1,639 @@ +package cli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strings" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/auth0/go-auth0/v3/management/core" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" +) + +var ( + vaultConnectionID = Argument{ + Name: "Id", + Help: "Id of the Vault connection.", + } + + vaultAppID = Argument{ + Name: "App Id", + Help: "Identifier of the Vault app to open (e.g. AUTH0, JWT, HTTP, SLACK).", + } + + vaultConnectionName = Flag{ + Name: "Name", + LongForm: "name", + Help: "Name of the Vault connection.", + } + + vaultConnectionAppID = Flag{ + Name: "App Id", + LongForm: "app-id", + Help: "Identifier of the app the Vault connection integrates with (e.g. HTTP, SLACK).", + } + + vaultConnectionFile = Flag{ + Name: "Setup File", + LongForm: "setup-file", + ShortForm: "f", + Help: "Path to a JSON file containing the vault connection setup credentials. Run with --setup-template --app-id to see the expected setup schema for a given app.", + } + + vaultConnectionSetupTemplate = Flag{ + Name: "Setup Template", + LongForm: "setup-template", + Help: "Print the setup credentials template for the given --app-id and exit.", + } +) + +// vaultConnectionCreateSkeleton is the fallback editor seed when the app_id is not recognized. +const vaultConnectionCreateSkeleton = `{ + "setup": { + "type": "BEARER", + "token": "REPLACE_WITH_YOUR_TOKEN" + } +} +` + +// vaultConnectionSkeletons maps each known Vault app_id to an editor seed that +// matches the setup type(s) the Management API accepts for that app. +var vaultConnectionSkeletons = map[string]string{ + "ACTIVECAMPAIGN": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"api_key\": \"REPLACE_WITH_API_KEY\",\n \"base_url\": \"https://REPLACE_WITH_YOUR_INSTANCE.api-us1.com\"\n }\n}\n", + "AIRTABLE": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"api_key\": \"REPLACE_WITH_API_KEY\"\n }\n}\n", + "AUTH0": "{\n \"setup\": {\n \"type\": \"OAUTH_APP\",\n \"client_id\": \"REPLACE_WITH_CLIENT_ID\",\n \"client_secret\": \"REPLACE_WITH_CLIENT_SECRET\",\n \"domain\": \"REPLACE_WITH_YOUR_DOMAIN.auth0.com\"\n }\n}\n", + "BIGQUERY": "{\n \"setup\": {\n \"type\": \"OAUTH_JWT\",\n \"project_id\": \"REPLACE_WITH_PROJECT_ID\",\n \"private_key\": \"REPLACE_WITH_PRIVATE_KEY\",\n \"client_email\": \"REPLACE@PROJECT.iam.gserviceaccount.com\"\n }\n}\n", + "CLEARBIT": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"secret_key\": \"REPLACE_WITH_SECRET_KEY\"\n }\n}\n", + "DOCUSIGN": "{\n \"setup\": {\n \"type\": \"OAUTH_CODE\",\n \"code\": \"REPLACE_WITH_AUTHORIZATION_CODE\"\n }\n}\n", + "GOOGLE_SHEETS": "{\n \"setup\": {\n \"type\": \"OAUTH_CODE\",\n \"code\": \"REPLACE_WITH_AUTHORIZATION_CODE\"\n }\n}\n", + "HTTP": "{\n \"setup\": {\n \"type\": \"BEARER\",\n \"token\": \"REPLACE_WITH_YOUR_TOKEN\"\n }\n}\n", + "HUBSPOT": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"api_key\": \"REPLACE_WITH_API_KEY\"\n }\n}\n", + "JWT": "{\n \"setup\": {\n \"type\": \"JWT\",\n \"algorithm\": \"RS256\"\n }\n}\n", + "MAILCHIMP": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"secret_key\": \"REPLACE_WITH_SECRET_KEY\"\n }\n}\n", + "MAILJET": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"api_key\": \"REPLACE_WITH_API_KEY\",\n \"secret_key\": \"REPLACE_WITH_SECRET_KEY\"\n }\n}\n", + "PIPEDRIVE": "{\n \"setup\": {\n \"type\": \"TOKEN\",\n \"token\": \"REPLACE_WITH_YOUR_TOKEN\"\n }\n}\n", + "SALESFORCE": "{\n \"setup\": {\n \"type\": \"OAUTH_CODE\",\n \"code\": \"REPLACE_WITH_AUTHORIZATION_CODE\"\n }\n}\n", + "SENDGRID": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"api_key\": \"REPLACE_WITH_API_KEY\"\n }\n}\n", + "SLACK": "{\n \"setup\": {\n \"type\": \"WEBHOOK\",\n \"url\": \"https://hooks.slack.com/services/REPLACE_WITH_YOUR_WEBHOOK_URL\"\n }\n}\n", + "STRIPE": "{\n \"setup\": {\n \"type\": \"KEY_PAIR\",\n \"private_key\": \"sk_REPLACE_WITH_PRIVATE_KEY\",\n \"public_key\": \"pk_REPLACE_WITH_PUBLIC_KEY\"\n }\n}\n", + "TELEGRAM": "{\n \"setup\": {\n \"type\": \"TOKEN\",\n \"token\": \"REPLACE_WITH_BOT_TOKEN\"\n }\n}\n", + "TWILIO": "{\n \"setup\": {\n \"type\": \"API_KEY\",\n \"account_id\": \"REPLACE_WITH_ACCOUNT_ID\",\n \"api_key\": \"REPLACE_WITH_API_KEY\"\n }\n}\n", + "WHATSAPP": "{\n \"setup\": {\n \"type\": \"TOKEN\",\n \"token\": \"REPLACE_WITH_YOUR_TOKEN\"\n }\n}\n", + "ZAPIER": "{\n \"setup\": {\n \"type\": \"WEBHOOK\",\n \"url\": \"https://hooks.zapier.com/hooks/catch/REPLACE_WITH_YOUR_WEBHOOK_PATH\"\n }\n}\n", +} + +func vaultConnectionSeedForApp(appID string) string { + if s, ok := vaultConnectionSkeletons[strings.ToUpper(appID)]; ok { + return s + } + return vaultConnectionCreateSkeleton +} + +func flowVaultCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "vault", + Short: "Manage Flow vault connections", + Long: "Manage the vault connections that store credentials for flow integrations.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(flowVaultConnectionsCmd(cli)) + cmd.AddCommand(openVaultAppCmd(cli)) + + return cmd +} + +func flowVaultConnectionsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "connections", + Short: "Manage Flow vault connections.", + Long: "List, inspect, create, update, and delete flow vault connections.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listVaultConnectionsCmd(cli)) + cmd.AddCommand(showVaultConnectionCmd(cli)) + cmd.AddCommand(createVaultConnectionCmd(cli)) + cmd.AddCommand(updateVaultConnectionCmd(cli)) + cmd.AddCommand(deleteVaultConnectionCmd(cli)) + + return cmd +} + +func listVaultConnectionsCmd(cli *cli) *cobra.Command { + var inputs struct { + Number int + } + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List your vault connections", + Long: "List your existing vault connections. To create one, run: `auth0 flows vault connections create`.", + Example: ` auth0 flows vault connections list + auth0 flows vault connections ls --number 100 + auth0 flows vault connections ls --json`, + RunE: func(cmd *cobra.Command, args []string) error { + params := &managementv3.ListFlowsVaultConnectionsRequestParameters{} + + var connections []*managementv3.FlowsVaultConnectionSummary + if err := ansi.Waiting(func() (err error) { + connections, err = collectVaultConnections(cmd.Context(), cli, params, inputs.Number) + return err + }); err != nil { + return fmt.Errorf("failed to list vault connections: %w", err) + } + + return cli.renderer.FlowVaultConnectionsList(connections) + }, + } + + cmd.Flags().IntVarP(&inputs.Number, "number", "n", 100, "Number of connections to retrieve. Fetched across pages.") + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + + return cmd +} + +func showVaultConnectionCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show a vault connection", + Long: "Display information about a vault connection. Secret values are never returned by the API.", + Example: ` auth0 flows vault connections show + auth0 flows vault connections show + auth0 flows vault connections show --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := vaultConnectionID.Pick(cmd, &inputs.ID, cli.flowsVaultConnectionPickerOptions); err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + connection, err := cli.vaultConnectionRawGet(cmd.Context(), inputs.ID) + if err != nil { + return fmt.Errorf("failed to read vault connection with ID %q: %w", inputs.ID, err) + } + + return cli.renderer.FlowVaultConnectionShowRaw("vault connection", connection) + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func createVaultConnectionCmd(cli *cli) *cobra.Command { + var inputs struct { + Name string + AppID string + File string + SetupTemplate bool + } + + cmd := &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new vault connection", + Long: "Create a new vault connection.\n\n" + + "A name is required, supplied with `--name` or the interactive prompt, along with an app id via `--app-id`. " + + "Setup credentials are authored interactively or supplied with `--setup-file`; the file must contain only the setup body, not a name. " + + "Run `--setup-template --app-id ` to print the setup credentials template for a given app.", + Example: ` auth0 flows vault connections create + auth0 flows vault connections create --name "My Connection" --app-id SLACK + auth0 flows vault connections create --name "My Connection" --app-id SLACK --setup-file ./setup.json + auth0 flows vault connections create --setup-template --app-id SLACK > setup.json`, + RunE: func(cmd *cobra.Command, args []string) error { + inputs.AppID = strings.ToUpper(inputs.AppID) + + if inputs.SetupTemplate { + if err := vaultConnectionAppID.Pick(cmd, &inputs.AppID, cli.vaultAppIDPickerOptions); err != nil { + return err + } + if inputs.AppID == "" { + return errors.New("an app id is required") + } + cli.renderer.Warnf("Setup schema for %s. Save to a file and pass with --setup-file.", inputs.AppID) + cli.renderer.FlowExport(vaultConnectionSeedForApp(inputs.AppID)) + return nil + } + + if err := vaultConnectionName.Ask(cmd, &inputs.Name, nil); err != nil { + return err + } + if inputs.Name == "" { + return errors.New("a name is required") + } + if err := vaultConnectionAppID.Pick(cmd, &inputs.AppID, cli.vaultAppIDPickerOptions); err != nil { + return err + } + if inputs.AppID == "" { + return errors.New("an app id is required") + } + + var setupBody json.RawMessage + if inputs.File != "" { + fileBody, err := os.ReadFile(inputs.File) + if err != nil { + return fmt.Errorf("failed to read setup file %q: %w", inputs.File, err) + } + if err = json.Unmarshal(fileBody, &setupBody); err != nil { + return fmt.Errorf("setup file is not valid JSON: %w\n\nRun 'auth0 flows vault connections create --setup-template --app-id ' to see the expected setup schema", err) + } + if err := rejectRawNameField(setupBody, "setup file"); err != nil { + return err + } + } else { + var addSetup bool + if err := prompt.AskBool("Do you want to add setup credentials now?", &addSetup, false); err != nil { + return err + } + + if addSetup { + if err := editJSONBody(cli, "vault connection", vaultConnectionSeedForApp(inputs.AppID), &setupBody); err != nil { + return err + } + if err := rejectRawNameField(setupBody, "setup body"); err != nil { + return err + } + } else { + setupBody = json.RawMessage(`{}`) + } + } + + m := make(map[string]json.RawMessage) + if err := json.Unmarshal(setupBody, &m); err != nil { + return fmt.Errorf("failed to parse vault connection body: %w", err) + } + nameJSON, _ := json.Marshal(inputs.Name) + appIDJSON, _ := json.Marshal(inputs.AppID) + m["name"] = nameJSON + m["app_id"] = appIDJSON + rawBody, err := json.Marshal(m) + if err != nil { + return fmt.Errorf("failed to build vault connection body: %w", err) + } + + created, err := cli.vaultConnectionRawCreate(cmd.Context(), rawBody) + if err != nil { + if strings.Contains(err.Error(), "oneOf") { + cli.renderer.Warnf("The setup payload does not match the expected schema for %s.\nRun 'auth0 flows vault connections create --setup-template --app-id %s' to see the correct setup format.", inputs.AppID, inputs.AppID) + } + return fmt.Errorf("failed to create vault connection: %w", err) + } + + return cli.renderer.FlowVaultConnectionShowRaw("vault connection created", created) + }, + } + + vaultConnectionName.RegisterString(cmd, &inputs.Name, "") + vaultConnectionAppID.RegisterString(cmd, &inputs.AppID, "") + vaultConnectionFile.RegisterString(cmd, &inputs.File, "") + vaultConnectionSetupTemplate.RegisterBool(cmd, &inputs.SetupTemplate, false) + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func updateVaultConnectionCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + Name string + File string + } + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.MaximumNArgs(1), + Short: "Update a vault connection", + Long: "Update a vault connection.\n\n" + + "Passing `--setup-file` replaces the connection's setup credentials; the file must contain only the setup body, not a name. " + + "Passing `--name` renames the connection; omit it to keep the current name. " + + "Run `auth0 flows vault connections create --setup-template --app-id ` to see the setup schema.", + Example: ` auth0 flows vault connections update --name "New Name" + auth0 flows vault connections update --setup-file ./setup.json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.ID = args[0] + } else { + if err := vaultConnectionID.Pick(cmd, &inputs.ID, cli.flowsVaultConnectionPickerOptions); err != nil { + return err + } + } + + current, err := cli.vaultConnectionRawGet(cmd.Context(), inputs.ID) + if err != nil { + return fmt.Errorf("failed to read vault connection with ID %q: %w", inputs.ID, err) + } + + currentName, err := rawJSONStringField(current, "name") + if err != nil { + return fmt.Errorf("failed to read vault connection name: %w", err) + } + + if err := vaultConnectionName.AskU(cmd, &inputs.Name, ¤tName); err != nil { + return err + } + if inputs.Name == "" { + inputs.Name = currentName + } + + var appID string + var rawBody json.RawMessage + if inputs.File != "" { + fileBody, err := os.ReadFile(inputs.File) + if err != nil { + return fmt.Errorf("failed to read setup file %q: %w", inputs.File, err) + } + var setupBody json.RawMessage + if err := json.Unmarshal(fileBody, &setupBody); err != nil { + return fmt.Errorf("setup file is not valid JSON: %w", err) + } + if err := rejectRawNameField(setupBody, "setup file"); err != nil { + return err + } + m := make(map[string]json.RawMessage) + if err := json.Unmarshal(setupBody, &m); err != nil { + return fmt.Errorf("failed to parse setup body: %w", err) + } + nameJSON, _ := json.Marshal(inputs.Name) + m["name"] = nameJSON + rawBody, err = json.Marshal(m) + if err != nil { + return fmt.Errorf("failed to build update body: %w", err) + } + } else { + var updateSetup bool + if err := prompt.AskBool("Do you want to update the setup details?", &updateSetup, false); err != nil { + return err + } + + if updateSetup { + appID, err = rawJSONStringField(current, "app_id") + if err != nil { + return fmt.Errorf("failed to read app_id from vault connection %q: %w", inputs.ID, err) + } + if err := editJSONBody(cli, "vault connection", vaultConnectionSeedForApp(appID), &rawBody); err != nil { + return err + } + if err := rejectRawNameField(rawBody, "setup body"); err != nil { + return err + } + } else { + rawBody = json.RawMessage(`{}`) + } + + rawBody, err = applyRawNameOverride(rawBody, inputs.Name) + if err != nil { + return fmt.Errorf("failed to apply name override: %w", err) + } + } + + updated, err := cli.vaultConnectionRawUpdate(cmd.Context(), inputs.ID, rawBody) + if err != nil { + if strings.Contains(err.Error(), "oneOf") { + hint := "" + if appID != "" { + hint = appID + } + cli.renderer.Warnf("The setup payload does not match the expected schema for %s.\nRun 'auth0 flows vault connections create --setup-template --app-id %s' to see the correct setup format.", hint, hint) + } + return fmt.Errorf("failed to update vault connection with ID %q: %w", inputs.ID, err) + } + + return cli.renderer.FlowVaultConnectionShowRaw("vault connection updated", updated) + }, + } + + vaultConnectionName.RegisterStringU(cmd, &inputs.Name, "") + vaultConnectionFile.RegisterStringU(cmd, &inputs.File, "") + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func deleteVaultConnectionCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Aliases: []string{"rm"}, + Args: cobra.ArbitraryArgs, + Short: "Delete a vault connection", + Long: "Delete a vault connection.\n\n" + + "To delete interactively, use `auth0 flows vault connections delete` with no arguments.\n\n" + + "To delete non-interactively, supply the connection id and the `--force` flag.", + Example: ` auth0 flows vault connections delete + auth0 flows vault connections rm + auth0 flows vault connections delete + auth0 flows vault connections delete --force`, + RunE: func(cmd *cobra.Command, args []string) error { + var ids []string + if len(args) == 0 { + if err := vaultConnectionID.PickMany(cmd, &ids, cli.flowsVaultConnectionPickerOptions); err != nil { + return err + } + } else { + ids = args + } + + if !cli.force && cli.agentMode { + return errDestructiveNoConfirm + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.ProgressBar("Deleting vault connection(s)", ids, func(_ int, id string) error { + if id == "" { + return nil + } + if err := cli.apiv3.FlowVaultConnection.Delete(cmd.Context(), id); err != nil { + return fmt.Errorf("failed to delete vault connection with ID %q: %w", id, err) + } + return nil + }) + }, + } + + cmd.Flags().BoolVar(&cli.force, "force", false, "Skip confirmation.") + + return cmd +} + +func openVaultAppCmd(cli *cli) *cobra.Command { + var inputs struct { + AppID string + } + + cmd := &cobra.Command{ + Use: "open", + Args: cobra.MaximumNArgs(1), + Short: "Open the Vault in the Auth0 Dashboard", + Long: "Open a Vault app's page in the Auth0 Dashboard. This opens the app's Vault page " + + "(for example AUTH0, JWT, HTTP, or SLACK), not a specific connection.", + Example: ` auth0 flows vault open + auth0 flows vault open HTTP + auth0 flows vault open SLACK`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := vaultAppID.Pick(cmd, &inputs.AppID, cli.vaultAppPickerOptions); err != nil { + return err + } + } else { + inputs.AppID = args[0] + } + + openBuilderURL(cli, fmt.Sprintf("vault/apps/%s/edit", inputs.AppID)) + + return nil + }, + } + + return cmd +} + +// knownVaultAppIDs is the static list of app IDs recognised by the Flows Vault, +// derived from the FlowsVaultConnectionAppID* enums in the v3 SDK. +var knownVaultAppIDs = []string{ + "ACTIVECAMPAIGN", + "AIRTABLE", + "AUTH0", + "BIGQUERY", + "CLEARBIT", + "DOCUSIGN", + "GOOGLE_SHEETS", + "HTTP", + "HUBSPOT", + "JWT", + "MAILCHIMP", + "MAILJET", + "PIPEDRIVE", + "SALESFORCE", + "SENDGRID", + "SLACK", + "STRIPE", + "TELEGRAM", + "TWILIO", + "WHATSAPP", + "ZAPIER", +} + +// vaultAppIDPickerOptions returns the static list of Vault app IDs for vault connection creation. +func (c *cli) vaultAppIDPickerOptions(_ context.Context) (pickerOptions, error) { + var opts pickerOptions + for _, id := range knownVaultAppIDs { + opts = append(opts, pickerOption{value: id, label: id}) + } + return opts, nil +} + +func (c *cli) flowsVaultConnectionPickerOptions(ctx context.Context) (pickerOptions, error) { + connections, err := collectVaultConnections(ctx, c, &managementv3.ListFlowsVaultConnectionsRequestParameters{}, 0) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, conn := range connections { + label := fmt.Sprintf("%s %s", conn.GetName(), ansi.Faint("("+conn.GetID()+")")) + opts = append(opts, pickerOption{value: conn.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("there are no vault connections to choose from; create one with `auth0 flows vault connections create`") + } + + return opts, nil +} + +// vaultAppPickerOptions offers the distinct app ids among existing vault +// connections, so `auth0 flows vault open` can be run without arguments. +func (c *cli) vaultAppPickerOptions(ctx context.Context) (pickerOptions, error) { + connections, err := collectVaultConnections(ctx, c, &managementv3.ListFlowsVaultConnectionsRequestParameters{}, 0) + if err != nil { + return nil, err + } + + seen := map[string]bool{} + var opts pickerOptions + for _, conn := range connections { + appID := conn.GetAppID() + if appID == "" || seen[appID] { + continue + } + seen[appID] = true + opts = append(opts, pickerOption{value: appID, label: appID}) + } + + if len(opts) == 0 { + return nil, errors.New("there are no vault apps to choose from; supply an app id, e.g. `auth0 flows vault open HTTP`") + } + + return opts, nil +} + +// --- Raw HTTP helpers ---. + +func (c *cli) vaultConnectionRawGet(ctx context.Context, id string) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodGet, c.api.HTTPClient.URI("flows", "vault", "connections", id), nil) +} + +func (c *cli) vaultConnectionRawCreate(ctx context.Context, body json.RawMessage) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodPost, c.api.HTTPClient.URI("flows", "vault", "connections"), body) +} + +func (c *cli) vaultConnectionRawUpdate(ctx context.Context, id string, body json.RawMessage) (json.RawMessage, error) { + return c.rawJSONRequest(ctx, http.MethodPatch, c.api.HTTPClient.URI("flows", "vault", "connections", id), body) +} + +// --- Paging + pickers ---. + +func collectVaultConnections(ctx context.Context, cli *cli, params *managementv3.ListFlowsVaultConnectionsRequestParameters, limit int) ([]*managementv3.FlowsVaultConnectionSummary, error) { + page, err := cli.apiv3.FlowVaultConnection.List(ctx, params) + if err != nil { + return nil, err + } + + var out []*managementv3.FlowsVaultConnectionSummary + for page != nil { + for _, c := range page.Results { + out = append(out, c) + if limit > 0 && len(out) >= limit { + return out, nil + } + } + + page, err = page.GetNextPage(ctx) + if errors.Is(err, core.ErrNoPages) { + break + } + if err != nil { + return out, err + } + } + + return out, nil +} diff --git a/internal/cli/flows_vault_test.go b/internal/cli/flows_vault_test.go new file mode 100644 index 000000000..8e0aa8b27 --- /dev/null +++ b/internal/cli/flows_vault_test.go @@ -0,0 +1,55 @@ +package cli + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateVaultConnectionCmdUsesRawClient(t *testing.T) { + body := []byte(`{"setup":{"type":"BEARER","token":"secret"}}`) + path := filepath.Join(t.TempDir(), "conn.json") + require.NoError(t, os.WriteFile(path, body, 0600)) + + stub := &rawHTTPClientStub{ + response: json.RawMessage(`{"id":"ac_1","app_id":"HTTP","name":"Renamed","ready":true}`), + } + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createVaultConnectionCmd(c) + cmd.SetArgs([]string{"--setup-file", path, "--name", "Renamed", "--app-id", "HTTP"}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, stub.method) + require.IsType(t, json.RawMessage{}, stub.payload) + sent := string(stub.payload.(json.RawMessage)) + assert.Contains(t, sent, `"Renamed"`) + assert.Contains(t, sent, `"HTTP"`) + assert.Contains(t, sent, `"setup"`) + // The rendered output must never echo the setup secrets back. + assert.NotContains(t, stdout.String(), "secret") +} + +func TestCreateVaultConnectionCmdRejectsNameInSetupFile(t *testing.T) { + body := []byte(`{"name":"Renamed","setup":{"type":"BEARER","token":"secret"}}`) + path := filepath.Join(t.TempDir(), "conn.json") + require.NoError(t, os.WriteFile(path, body, 0600)) + + stub := &rawHTTPClientStub{} + stdout := &bytes.Buffer{} + c := newRawTestCLI(stub, stdout) + + cmd := createVaultConnectionCmd(c) + cmd.SetArgs([]string{"--setup-file", path, "--name", "Renamed", "--app-id", "HTTP"}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "must not contain a top-level \"name\" field") +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 00faf6b29..61fe62ab2 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -299,6 +299,7 @@ func addSubCommands(rootCmd *cobra.Command, cli *cli) { rootCmd.AddCommand(apiCmd(cli)) rootCmd.AddCommand(terraformCmd(cli)) rootCmd.AddCommand(eventStreamsCmd(cli)) + rootCmd.AddCommand(flowsCmd(cli)) rootCmd.AddCommand(networkACLCmd(cli)) rootCmd.AddCommand(tenantSettingsCmd(cli)) rootCmd.AddCommand(tokenExchangeCmd(cli)) diff --git a/internal/cli/utils_shared.go b/internal/cli/utils_shared.go index eafbff7f9..f3a3d53c4 100644 --- a/internal/cli/utils_shared.go +++ b/internal/cli/utils_shared.go @@ -5,7 +5,9 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "fmt" + "io" "net/http" "net/url" "strconv" @@ -468,3 +470,188 @@ func collectV3Pages[C comparable, T any, R any]( return items, err } + +// --- Shared raw-JSON + builder helpers ---. +// +// Forms and Flows share a hybrid approach: they use the v3 SDK for list/delete +// but send create/update/show through the raw HTTP layer, because the typed +// request bodies drop provider-specific config in their union types. The helpers +// below are the resource-agnostic pieces of that approach. + +// builderURL is the host for the Auth0 visual builders (Flows and Vault). +// Both live on a dedicated host rather than under the main management dashboard. +const builderURL = "https://forms.auth0.com" + +// rawJSONRequest sends a raw JSON request to the Management API and returns the +// response body, surfacing API errors the same way the `api` command does. +func (c *cli) rawJSONRequest( + ctx context.Context, + method string, + uri string, + body json.RawMessage, +) (json.RawMessage, error) { + var payload interface{} + if len(body) > 0 { + payload = body + } + + request, err := c.api.HTTPClient.NewRequest(ctx, method, uri, payload) + if err != nil { + return nil, err + } + + var out json.RawMessage + if err := ansi.Waiting(func() error { + response, err := c.api.HTTPClient.Do(request) + if err != nil { + return err + } + defer func() { + _ = response.Body.Close() + }() + + data, err := io.ReadAll(response.Body) + if err != nil { + return err + } + if response.StatusCode >= http.StatusBadRequest { + return newAPIResponseError(response.StatusCode, response.Header, data) + } + out = data + return nil + }); err != nil { + return nil, err + } + + return out, nil +} + +// applyRawNameOverride sets the top-level "name" field when a non-empty override +// is supplied, without deserializing the rest of the body. +func applyRawNameOverride(body json.RawMessage, name string) (json.RawMessage, error) { + if name == "" { + return body, nil + } + + var obj map[string]json.RawMessage + if err := json.Unmarshal(body, &obj); err != nil { + return nil, err + } + if obj == nil { + return nil, errors.New("body must be a JSON object") + } + + encoded, err := json.Marshal(name) + if err != nil { + return nil, err + } + obj["name"] = encoded + + return json.Marshal(obj) +} + +func rejectRawNameField(body json.RawMessage, source string) error { + if len(body) == 0 { + return nil + } + + var obj map[string]json.RawMessage + if err := json.Unmarshal(body, &obj); err != nil { + return nil + } + if _, ok := obj["name"]; ok { + return fmt.Errorf("the %s must not contain a top-level \"name\" field; set the name with --name instead", source) + } + + return nil +} + +// rawJSONStringField extracts a top-level string field from a raw JSON object, +// returning an empty string when the field is absent or null. +func rawJSONStringField(body json.RawMessage, field string) (string, error) { + var obj map[string]json.RawMessage + if err := json.Unmarshal(body, &obj); err != nil { + return "", err + } + if obj == nil { + return "", errors.New("body must be a JSON object") + } + + raw, ok := obj[field] + if !ok || string(raw) == "null" { + return "", nil + } + var value string + if err := json.Unmarshal(raw, &value); err != nil { + return "", fmt.Errorf("%s must be a string: %w", field, err) + } + return value, nil +} + +// editJSONBody opens an editor seeded with `seed` and unmarshals the result into +// `target`, re-opening on invalid JSON so a typo never costs the user's edits. +// `resource` names the object in prompts and error messages. +func editJSONBody(cli *cli, resource, seed string, target interface{}) error { + content := seed + for { + var edited string + if err := openCreateEditor(&edited, content, resource+".*.json", nil, nil); err != nil { + return err + } + + if err := json.Unmarshal([]byte(edited), target); err != nil { + cli.renderer.Warnf("The %s body is not valid JSON: %s", resource, err) + if !prompt.Confirm("Re-open the editor to fix it?") { + return fmt.Errorf("aborted; the %s was not saved", resource) + } + content = edited + continue + } + + return nil + } +} + +// openBuilderURL opens a Forms/Flows builder page in a browser, or prints the URL +// when interactivity is disabled. +func openBuilderURL(cli *cli, path string) { + url := formatBuilderPageURL(cli.Config.DefaultTenant, &cli.Config, path) + if url == "" { + cli.renderer.Warnf("Failed to format the correct URL, please ensure you have run 'auth0 login' and try again.") + return + } + + if cli.noInput { + cli.renderer.Infof("Open the following URL in a browser: %s", url) + return + } + + if err := browser.OpenURL(url); err != nil { + cli.renderer.Warnf("Couldn't open the URL, please do it manually: %s", url) + } +} + +// formatBuilderPageURL builds a Forms/Flows builder URL for the given path, +// deriving the region and tenant name from the configured tenant. +func formatBuilderPageURL(tenant string, cfg *config.Config, path string) string { + if len(tenant) == 0 || len(path) == 0 { + return "" + } + + s := strings.Split(tenant, ".") + if len(s) < 3 { + return "" + } + + region := "us" // A PUS1 tenant looks like dev-tti06f6y.auth0.com (3 parts). + if len(s) > 3 { + region = s[len(s)-3] + } + + tenantName := cfg.Tenants[tenant].Name + if len(tenantName) == 0 { + return "" + } + + return fmt.Sprintf("%s/tenants/%s/%s/%s", builderURL, region, tenantName, path) +} diff --git a/internal/display/flows.go b/internal/display/flows.go new file mode 100644 index 000000000..e7846b307 --- /dev/null +++ b/internal/display/flows.go @@ -0,0 +1,416 @@ +package display + +import ( + "encoding/json" + "fmt" + "time" + + managementv3 "github.com/auth0/go-auth0/v3/management" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +// --- Flows ---. + +type flowView struct { + ID string + Name string + ActionCount int + CreatedAt string + UpdatedAt string + ExecutedAt string + + raw interface{} +} + +func (v *flowView) AsTableHeader() []string { + return []string{"ID", "Name", "Executed At", "Updated"} +} + +func (v *flowView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Name, v.ExecutedAt, v.UpdatedAt} +} + +func (v *flowView) KeyValues() [][]string { + return [][]string{ + {"ID", ansi.Faint(v.ID)}, + {"NAME", v.Name}, + {"ACTIONS", fmt.Sprintf("%d actions", v.ActionCount)}, + {"CREATED AT", v.CreatedAt}, + {"UPDATED AT", v.UpdatedAt}, + } +} + +func (v *flowView) Object() interface{} { + return v.raw +} + +type flowSummaryView struct { + ID string + Name string + UpdatedAt string + ExecutedAt string + + raw interface{} +} + +func (v *flowSummaryView) AsTableHeader() []string { + return []string{"ID", "Name", "Executed At", "Updated"} +} + +func (v *flowSummaryView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Name, v.ExecutedAt, v.UpdatedAt} +} + +func (v *flowSummaryView) Object() interface{} { + return v.raw +} + +// FlowsList renders the list of flows. +func (r *Renderer) FlowsList(flows []*managementv3.FlowSummary) error { + resource := "flows" + + r.Heading(resource) + + if len(flows) == 0 { + r.EmptyState(resource, "Use 'auth0 flows create' to add one") + return nil + } + + var res []View + for _, f := range flows { + res = append(res, &flowSummaryView{ + ID: f.GetID(), + Name: f.GetName(), + UpdatedAt: timeAgo(f.GetUpdatedAt()), + ExecutedAt: f.GetExecutedAt(), + raw: mergeExtraProperties(f, f.GetExtraProperties()), + }) + } + + r.Results(res) + + return nil +} + +// FlowShowRaw renders a full-fidelity flow response read through the v1 HTTP +// client, avoiding the v3 SDK's lossy flow-action unions. +func (r *Renderer) FlowShowRaw(flow json.RawMessage) error { + return r.renderRawFlow("flow", flow) +} + +// FlowCreateRaw renders a full-fidelity create response. +func (r *Renderer) FlowCreateRaw(flow json.RawMessage) error { + return r.renderRawFlow("flow created", flow) +} + +// FlowUpdateRaw renders a full-fidelity update response. +func (r *Renderer) FlowUpdateRaw(flow json.RawMessage) error { + return r.renderRawFlow("flow updated", flow) +} + +func (r *Renderer) renderRawFlow(heading string, flow json.RawMessage) error { + view, err := makeFlowViewFromRaw(flow) + if err != nil { + return fmt.Errorf("failed to parse flow response: %w", err) + } + r.Heading(heading) + r.Result(view) + return nil +} + +func makeFlowViewFromRaw(raw json.RawMessage) (*flowView, error) { + var flow struct { + ID string `json:"id"` + Name string `json:"name"` + Actions []json.RawMessage `json:"actions"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ExecutedAt string `json:"executed_at"` + } + if err := json.Unmarshal(raw, &flow); err != nil { + return nil, err + } + + return &flowView{ + ID: flow.ID, + Name: flow.Name, + ActionCount: len(flow.Actions), + CreatedAt: rawTimeAgo(flow.CreatedAt), + UpdatedAt: rawTimeAgo(flow.UpdatedAt), + ExecutedAt: flow.ExecutedAt, + raw: raw, + }, nil +} + +// FlowExport writes a flow body verbatim (uncolored) to the result writer so it +// stays pipe- and import-friendly. +func (r *Renderer) FlowExport(body string) { + fmt.Fprintln(r.ResultWriter, body) +} + +// --- Flow executions ---. + +type flowExecutionView struct { + ID string + Status string + TraceID string + StartedAt string + EndedAt string + CreatedAt string + UpdatedAt string + + raw interface{} +} + +func (v *flowExecutionView) AsTableHeader() []string { + return []string{"ID", "Status", "Started", "Ended"} +} + +func (v *flowExecutionView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Status, v.StartedAt, v.EndedAt} +} + +func (v *flowExecutionView) KeyValues() [][]string { + kvs := [][]string{ + {"ID", ansi.Faint(v.ID)}, + {"STATUS", v.Status}, + {"TRACE ID", v.TraceID}, + } + if v.StartedAt != "" { + kvs = append(kvs, []string{"STARTED AT", v.StartedAt}) + } + if v.EndedAt != "" { + kvs = append(kvs, []string{"ENDED AT", v.EndedAt}) + } + kvs = append(kvs, + []string{"CREATED AT", v.CreatedAt}, + []string{"UPDATED AT", v.UpdatedAt}, + ) + return kvs +} + +func (v *flowExecutionView) Object() interface{} { + return v.raw +} + +// FlowExecutionsList renders the list of flow executions. +func (r *Renderer) FlowExecutionsList(executions []*managementv3.FlowExecutionSummary) error { + resource := "flow executions" + + r.Heading(resource) + + if len(executions) == 0 { + r.EmptyState(resource, "This flow has not been executed yet") + return nil + } + + var res []View + for _, e := range executions { + res = append(res, &flowExecutionView{ + ID: e.GetID(), + Status: e.GetStatus(), + TraceID: e.GetTraceID(), + StartedAt: rawTimeAgo(e.GetStartedAt()), + EndedAt: rawTimeAgo(e.GetEndedAt()), + CreatedAt: rawTimeAgo(e.GetCreatedAt()), + UpdatedAt: rawTimeAgo(e.GetUpdatedAt()), + raw: mergeExtraProperties(e, e.GetExtraProperties()), + }) + } + + r.Results(res) + + return nil +} + +// FlowExecutionShowRaw renders a full-fidelity execution response read through +// the v1 HTTP client. +func (r *Renderer) FlowExecutionShowRaw(execution json.RawMessage) error { + view, err := makeFlowExecutionViewFromRaw(execution) + if err != nil { + return fmt.Errorf("failed to parse flow execution response: %w", err) + } + r.Heading("flow execution") + r.Result(view) + return nil +} + +func makeFlowExecutionViewFromRaw(raw json.RawMessage) (*flowExecutionView, error) { + var execution struct { + ID string `json:"id"` + Status string `json:"status"` + TraceID string `json:"trace_id"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + if err := json.Unmarshal(raw, &execution); err != nil { + return nil, err + } + + return &flowExecutionView{ + ID: execution.ID, + Status: execution.Status, + TraceID: execution.TraceID, + StartedAt: rawTimeAgo(execution.StartedAt), + EndedAt: rawTimeAgo(execution.EndedAt), + CreatedAt: rawTimeAgo(execution.CreatedAt), + UpdatedAt: rawTimeAgo(execution.UpdatedAt), + raw: raw, + }, nil +} + +// --- Flow vault connections ---. + +type flowVaultConnectionView struct { + ID string + Name string + AppID string + Ready bool + AccountName string + CreatedAt string + UpdatedAt string + + raw interface{} +} + +func (v *flowVaultConnectionView) AsTableHeader() []string { + return []string{"ID", "Name", "App", "Ready"} +} + +func (v *flowVaultConnectionView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Name, v.AppID, boolToPresence(v.Ready)} +} + +func (v *flowVaultConnectionView) KeyValues() [][]string { + kvs := [][]string{ + {"ID", ansi.Faint(v.ID)}, + {"NAME", v.Name}, + {"APP ID", v.AppID}, + {"READY", boolToReady(v.Ready)}, + } + if v.AccountName != "" { + kvs = append(kvs, []string{"ACCOUNT NAME", v.AccountName}) + } + kvs = append(kvs, + []string{"CREATED AT", v.CreatedAt}, + []string{"UPDATED AT", v.UpdatedAt}, + ) + return kvs +} + +func (v *flowVaultConnectionView) Object() interface{} { + return v.raw +} + +// FlowVaultConnectionsList renders the list of vault connections. +func (r *Renderer) FlowVaultConnectionsList(connections []*managementv3.FlowsVaultConnectionSummary) error { + resource := "flow vault connections" + + r.Heading(resource) + + if len(connections) == 0 { + r.EmptyState(resource, "Use 'auth0 flows vault connections create' to add one") + return nil + } + + var res []View + for _, c := range connections { + res = append(res, &flowVaultConnectionView{ + ID: c.GetID(), + Name: c.GetName(), + AppID: c.GetAppID(), + Ready: c.GetReady(), + AccountName: c.GetAccountName(), + CreatedAt: rawTimeAgo(c.GetCreatedAt()), + UpdatedAt: rawTimeAgo(c.GetUpdatedAt()), + raw: mergeExtraProperties(c, c.GetExtraProperties()), + }) + } + + r.Results(res) + + return nil +} + +// FlowVaultConnectionShowRaw renders a full-fidelity vault connection response. +// The Management API never returns the write-only `setup` secrets, so nothing is +// masked here; the CLI simply never echoes the create/update body. +func (r *Renderer) FlowVaultConnectionShowRaw(heading string, connection json.RawMessage) error { + view, err := makeFlowVaultConnectionViewFromRaw(connection) + if err != nil { + return fmt.Errorf("failed to parse vault connection response: %w", err) + } + r.Heading(heading) + r.Result(view) + return nil +} + +func makeFlowVaultConnectionViewFromRaw(raw json.RawMessage) (*flowVaultConnectionView, error) { + var connection struct { + ID string `json:"id"` + Name string `json:"name"` + AppID string `json:"app_id"` + Ready bool `json:"ready"` + AccountName string `json:"account_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + if err := json.Unmarshal(raw, &connection); err != nil { + return nil, err + } + + return &flowVaultConnectionView{ + ID: connection.ID, + Name: connection.Name, + AppID: connection.AppID, + Ready: connection.Ready, + AccountName: connection.AccountName, + CreatedAt: rawTimeAgo(connection.CreatedAt), + UpdatedAt: rawTimeAgo(connection.UpdatedAt), + raw: raw, + }, nil +} + +func boolToPresence(present bool) string { + if present { + return "set" + } + return "none" +} + +func boolToReady(ready bool) string { + if ready { + return "yes" + } + return "no" +} + +func rawTimeAgo(value time.Time) string { + if value.IsZero() { + return "" + } + return timeAgo(value) +} + +func mergeExtraProperties(obj interface{}, extra map[string]interface{}) interface{} { + if len(extra) == 0 { + return obj + } + data, err := json.Marshal(obj) + if err != nil { + return obj + } + var merged map[string]interface{} + if err := json.Unmarshal(data, &merged); err != nil { + return obj + } + for key, value := range extra { + if _, ok := merged[key]; !ok { + merged[key] = value + } + } + return merged +} diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 8e17168c9..1cc8e53ab 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -36,14 +36,17 @@ func AskMultiSelect(message string, response interface{}, options ...string) err } func AskBool(message string, value *bool, defaultValue bool) error { + *value = defaultValue prompt := &survey.Confirm{ Message: message, Default: defaultValue, } - err := askOne(prompt, value) + if err := askOne(prompt, value); err != nil { + *value = defaultValue + } - return err + return nil } // Confirm prompts the user with a yes/no question and returns their response. diff --git a/test/integration/fixtures/create-flow.json b/test/integration/fixtures/create-flow.json new file mode 100644 index 000000000..3774df785 --- /dev/null +++ b/test/integration/fixtures/create-flow.json @@ -0,0 +1,16 @@ +{ + "actions": [ + { + "id": "step_http", + "type": "HTTP", + "action": "SEND_REQUEST", + "allow_failure": false, + "mask_output": false, + "params": { + "method": "GET", + "url": "https://api.example.com/enrich", + "content_type": "JSON" + } + } + ] +} diff --git a/test/integration/fixtures/update-flow.json b/test/integration/fixtures/update-flow.json new file mode 100644 index 000000000..a368d682a --- /dev/null +++ b/test/integration/fixtures/update-flow.json @@ -0,0 +1,16 @@ +{ + "actions": [ + { + "id": "step_http", + "type": "HTTP", + "action": "SEND_REQUEST", + "allow_failure": false, + "mask_output": false, + "params": { + "method": "GET", + "url": "https://api.example.com/updated", + "content_type": "JSON" + } + } + ] +} diff --git a/test/integration/fixtures/update-vault-connection.json b/test/integration/fixtures/update-vault-connection.json new file mode 100644 index 000000000..b54830c01 --- /dev/null +++ b/test/integration/fixtures/update-vault-connection.json @@ -0,0 +1,6 @@ +{ + "setup": { + "type": "BEARER", + "token": "integration-test-token-updated" + } +} diff --git a/test/integration/fixtures/vault-connection.json b/test/integration/fixtures/vault-connection.json new file mode 100644 index 000000000..ed97d4080 --- /dev/null +++ b/test/integration/fixtures/vault-connection.json @@ -0,0 +1,6 @@ +{ + "setup": { + "type": "BEARER", + "token": "integration-test-token" + } +} diff --git a/test/integration/flows-test-cases.yaml b/test/integration/flows-test-cases.yaml new file mode 100644 index 000000000..f990c2ccd --- /dev/null +++ b/test/integration/flows-test-cases.yaml @@ -0,0 +1,110 @@ +config: + inherit-env: true + retries: 1 + +tests: + 001 - it successfully lists all flows (json): + command: auth0 flows list --json + exit-code: 0 + + 002 - it successfully creates a flow via --name: + command: auth0 flows create --name integration-test-flow-created --no-input + exit-code: 0 + stdout: + contains: + - ID + - NAME + - integration-test-flow-created + + 003 - it successfully creates a flow and outputs in json: + command: auth0 flows create --name integration-test-flow-json --no-input --json + exit-code: 0 + stdout: + json: + name: "integration-test-flow-json" + + 004 - it successfully creates a flow from a fixture file: + command: auth0 flows create --actions-file ./test/integration/fixtures/create-flow.json --name integration-test-flow-example --no-input --json + exit-code: 0 + stdout: + json: + name: "integration-test-flow-example" + + 005 - it fails to create a flow without a name: + command: auth0 flows create --no-input + exit-code: 1 + stderr: + contains: + - flow name is required + + 006 - it successfully lists all flows with data: + command: auth0 flows list + exit-code: 0 + stdout: + contains: + - ID + - NAME + - UPDATED + + 007 - given a test flow, it successfully shows the flow details: + command: auth0 flows show $(./test/integration/scripts/get-flow-id.sh) + exit-code: 0 + stdout: + contains: + - ID + - NAME + - integration-test-flow + + 008 - given a test flow, it successfully shows the flow details (json): + command: auth0 flows show $(./test/integration/scripts/get-flow-id.sh) --json + exit-code: 0 + stdout: + json: + name: "integration-test-flow" + + 009 - given a test flow, it successfully updates the flow name: + command: auth0 flows update $(./test/integration/scripts/get-flow-id.sh) --name integration-test-flow-updated --json + exit-code: 0 + stdout: + json: + name: "integration-test-flow-updated" + + 010 - given a test flow, it successfully updates the flow actions from a fixture file: + command: auth0 flows update $(./test/integration/scripts/get-flow-id.sh) --actions-file ./test/integration/fixtures/update-flow.json --json + exit-code: 0 + stdout: + json: + name: "integration-test-flow-updated" + + 011 - given a test flow, it prints the builder URL for open: + command: auth0 flows open $(./test/integration/scripts/get-flow-id.sh) --no-input + exit-code: 0 + stderr: + contains: + - forms.auth0.com + - /flows/ + - /edit + + 012 - given a test flow, it successfully lists its executions (json): + command: auth0 flows executions list $(./test/integration/scripts/get-flow-id.sh) --json + exit-code: 0 + + 013 - given a test flow, it successfully lists its executions: + command: auth0 flows executions list $(./test/integration/scripts/get-flow-id.sh) + exit-code: 0 + + 014 - agent mode refuses to delete a flow without force: + command: AUTH0_AGENT_MODE=true auth0 flows delete $(./test/integration/scripts/get-flow-id.sh) + exit-code: 1 + stderr: + contains: + - destructive command + - --force + + 015 - given a test flow, it successfully deletes the flow: + command: auth0 flows delete $(./test/integration/scripts/get-flow-id.sh) --force + exit-code: 0 + + 016 - it cleans up all flows created by this suite: + command: ./test/integration/scripts/cleanup-flows.sh + exit-code: 0 diff --git a/test/integration/flows-vault-test-cases.yaml b/test/integration/flows-vault-test-cases.yaml new file mode 100644 index 000000000..17c3e2b7d --- /dev/null +++ b/test/integration/flows-vault-test-cases.yaml @@ -0,0 +1,100 @@ +config: + inherit-env: true + retries: 1 + +tests: + 001 - it successfully lists all vault connections (json): + command: auth0 flows vault connections list --json + exit-code: 0 + + 002 - it prints the setup template for a vault connection: + command: auth0 flows vault connections create --setup-template --app-id HTTP + exit-code: 0 + stdout: + contains: + - '"setup"' + - '"type"' + + 003 - it prints the vault app builder URL for open: + command: auth0 flows vault open HTTP --no-input + exit-code: 0 + stderr: + contains: + - forms.auth0.com + - /vault/apps/HTTP/edit + + 004 - it fails to create a vault connection without an app id: + command: auth0 flows vault connections create --name integration-test-connection-noapp --no-input + exit-code: 1 + stderr: + contains: + - app id is required + + 005 - it successfully creates a vault connection from a fixture file (json): + command: auth0 flows vault connections create --setup-file ./test/integration/fixtures/vault-connection.json --name integration-test-connection-created --app-id HTTP --no-input --json + exit-code: 0 + stdout: + json: + name: "integration-test-connection-created" + + 006 - it successfully lists all vault connections with data: + command: auth0 flows vault connections list + exit-code: 0 + stdout: + contains: + - ID + - NAME + - APP + + 007 - given a test vault connection, it successfully shows the connection details: + command: auth0 flows vault connections show $(./test/integration/scripts/get-vault-connection-id.sh) + exit-code: 0 + stdout: + contains: + - ID + - NAME + - integration-test-connection + + 008 - given a test vault connection, it does not echo the setup secret on show: + command: auth0 flows vault connections show $(./test/integration/scripts/get-vault-connection-id.sh) + exit-code: 0 + stdout: + not-contains: + - integration-test-token + + 009 - given a test vault connection, it successfully shows the connection details (json): + command: auth0 flows vault connections show $(./test/integration/scripts/get-vault-connection-id.sh) --json + exit-code: 0 + stdout: + json: + name: "integration-test-connection" + + 010 - given a test vault connection, it successfully updates the connection name: + command: auth0 flows vault connections update $(./test/integration/scripts/get-vault-connection-id.sh) --name integration-test-connection-updated --json + exit-code: 0 + stdout: + json: + name: "integration-test-connection-updated" + + 011 - given a test vault connection, it successfully updates the connection setup from a fixture file: + command: auth0 flows vault connections update $(./test/integration/scripts/get-vault-connection-id.sh) -f ./test/integration/fixtures/update-vault-connection.json --json + exit-code: 0 + stdout: + json: + name: "integration-test-connection-updated" + + 012 - agent mode refuses to delete a vault connection without force: + command: AUTH0_AGENT_MODE=true auth0 flows vault connections delete $(./test/integration/scripts/get-vault-connection-id.sh) + exit-code: 1 + stderr: + contains: + - destructive command + - --force + + 013 - given a test vault connection, it successfully deletes the connection: + command: auth0 flows vault connections delete $(./test/integration/scripts/get-vault-connection-id.sh) --force + exit-code: 0 + + 014 - it cleans up all vault connections created by this suite: + command: ./test/integration/scripts/cleanup-vault-connections.sh + exit-code: 0 diff --git a/test/integration/scripts/cleanup-flows.sh b/test/integration/scripts/cleanup-flows.sh new file mode 100755 index 000000000..320b41402 --- /dev/null +++ b/test/integration/scripts/cleanup-flows.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +ids=() +while IFS= read -r id; do + if [[ -n "$id" ]]; then + ids+=("$id") + fi +done < <(auth0 flows list --json --no-input | jq -r '.[] | select(.name | startswith("integration-test-")) | .id') + +if (( ${#ids[@]} > 0 )); then + auth0 flows delete --force "${ids[@]}" +fi + +rm -f ./test/integration/identifiers/flow-id diff --git a/test/integration/scripts/cleanup-vault-connections.sh b/test/integration/scripts/cleanup-vault-connections.sh new file mode 100755 index 000000000..588a6ee9b --- /dev/null +++ b/test/integration/scripts/cleanup-vault-connections.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +ids=() +while IFS= read -r id; do + if [[ -n "$id" ]]; then + ids+=("$id") + fi +done < <(auth0 flows vault connections list --json --no-input | jq -r '.[] | select(.name | startswith("integration-test-")) | .id') + +if (( ${#ids[@]} > 0 )); then + auth0 flows vault connections delete --force "${ids[@]}" +fi + +rm -f ./test/integration/identifiers/vault-connection-id diff --git a/test/integration/scripts/get-flow-id.sh b/test/integration/scripts/get-flow-id.sh new file mode 100755 index 000000000..4877125ce --- /dev/null +++ b/test/integration/scripts/get-flow-id.sh @@ -0,0 +1,13 @@ +#! /bin/bash + +FILE=./test/integration/identifiers/flow-id +if [ -f "$FILE" ]; then + cat $FILE + exit 0 +fi + +flow=$( auth0 flows create --name "integration-test-flow" --json --no-input ) + +mkdir -p ./test/integration/identifiers +echo "$flow" | jq -r '.["id"]' > $FILE +cat $FILE diff --git a/test/integration/scripts/get-vault-connection-id.sh b/test/integration/scripts/get-vault-connection-id.sh new file mode 100755 index 000000000..cf9658401 --- /dev/null +++ b/test/integration/scripts/get-vault-connection-id.sh @@ -0,0 +1,17 @@ +#! /bin/bash + +FILE=./test/integration/identifiers/vault-connection-id +if [ -f "$FILE" ]; then + cat $FILE + exit 0 +fi + +connection=$( auth0 flows vault connections create \ + --setup-file ./test/integration/fixtures/vault-connection.json \ + --name "integration-test-connection" \ + --app-id HTTP \ + --json --no-input ) + +mkdir -p ./test/integration/identifiers +echo "$connection" | jq -r '.["id"]' > $FILE +cat $FILE diff --git a/test/integration/scripts/run-test-suites.sh b/test/integration/scripts/run-test-suites.sh index 4b0e7ea28..f637862d1 100644 --- a/test/integration/scripts/run-test-suites.sh +++ b/test/integration/scripts/run-test-suites.sh @@ -14,21 +14,28 @@ auth0 login \ set +e -# The quickstart integration tests are excluded from the default suite, so run -# each remaining test-cases file individually (in alphabetical order, matching -# --dir) instead of the whole directory. exit_code=0 -for suite in ./test/integration/*.yaml; do - if [[ "$(basename "$suite")" == "quickstarts-test-cases.yaml" ]]; then - echo "Skipping $suite" - continue - fi - - commander test --filter "$FILTER" "$suite" +if [[ -n "${FILE}" ]]; then + commander test --filter "$FILTER" "${FILE}" if [[ $? -ne 0 ]]; then exit_code=1 fi -done +else + # The quickstart integration tests are excluded from the default suite, so run + # each remaining test-cases file individually (in alphabetical order, matching + # --dir) instead of the whole directory. + for suite in ./test/integration/*.yaml; do + if [[ "$(basename "$suite")" == "quickstarts-test-cases.yaml" ]]; then + echo "Skipping $suite" + continue + fi + + commander test --filter "$FILTER" "$suite" + if [[ $? -ne 0 ]]; then + exit_code=1 + fi + done +fi bash ./test/integration/scripts/test-cleanup.sh diff --git a/test/integration/scripts/test-cleanup.sh b/test/integration/scripts/test-cleanup.sh index 61ff1d57f..54400e09c 100755 --- a/test/integration/scripts/test-cleanup.sh +++ b/test/integration/scripts/test-cleanup.sh @@ -32,6 +32,8 @@ delete_resources "actions" "integration-test-" "id" delete_resources "actions modules" "integration-test-module" "id" delete_resources "token-exchange" "integration-test-" "id" delete_resources "event-streams" "integration-test-" "id" +delete_resources "flows vault connections" "integration-test-" "id" +delete_resources "flows" "integration-test-" "id" delete_resources "logs streams" "integration-test-" "id" auth0 domains delete $(./test/integration/scripts/get-custom-domain-id.sh) --no-input