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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ The following environment variables can be set up to control which environment a
| EPCC_CLI_DISABLE_RESOURCES | A comma seperated list of resources that will not be available with commands or in the resource list |
| EPCC_CLI_RATE_LIMIT | The default rate limit to use |
| EPCC_CLI_DISABLE_HTTP_LOGGING | Disables writing of HTTP logs |
| EPCC_CLI_READ_ONLY | Enables read-only mode, blocking create/update/delete operations. Commands are hidden and return exit code 4 if attempted. |

It is recommended to set EPCC_API_BASE_URL, EPCC_CLIENT_ID, and EPCC_CLIENT_SECRET to be able to interact with most things in the CLI.

Expand Down
11 changes: 9 additions & 2 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ import (
func NewCreateCommand(parentCmd *cobra.Command) func() {

var createCmd = &cobra.Command{
Use: "create",
Short: "Creates a resource",
Use: "create",
Short: "Creates a resource",
Hidden: IsReadOnly(),
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if IsReadOnly() {
return ErrReadOnlyMode
}
return RootCmd.PersistentPreRunE(RootCmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please specify a resource, epcc create [RESOURCE], see epcc create --help")
Expand Down
7 changes: 7 additions & 0 deletions cmd/delete-all.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ func NewDeleteAllCommand(parentCmd *cobra.Command) func() {
var deleteAll = &cobra.Command{
Use: "delete-all",
Short: "Deletes all of a resource",
Hidden: IsReadOnly(),
SilenceUsage: false,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if IsReadOnly() {
return ErrReadOnlyMode
}
return RootCmd.PersistentPreRunE(RootCmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please specify a resource, epcc delete-all [RESOURCE], see epcc delete-all --help")
Expand Down
7 changes: 7 additions & 0 deletions cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() {
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Deletes a resource",
Hidden: IsReadOnly(),
SilenceUsage: false,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if IsReadOnly() {
return ErrReadOnlyMode
}
return RootCmd.PersistentPreRunE(RootCmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please specify a resource, epcc delete [RESOURCE], see epcc delete --help")
Expand Down
6 changes: 6 additions & 0 deletions cmd/reset-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ var ResetStore = &cobra.Command{
Short: "Resets a store to it's initial state on a \"best effort\" basis.",
Long: "This command resets a store to it's initial state. There are some limitations to this as for instance orders cannot be deleted, nor can audit entries.",
Args: cobra.MinimumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if IsReadOnly() {
return ErrReadOnlyMode
}
return RootCmd.PersistentPreRunE(RootCmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := clictx.Ctx

Expand Down
22 changes: 21 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"errors"
"fmt"
"os"
"os/signal"
Expand Down Expand Up @@ -63,6 +64,14 @@ var jqCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string

var profileNameFromCommandLine = ""

// ErrReadOnlyMode is returned when a write operation is attempted in read-only mode
var ErrReadOnlyMode = errors.New("operation not permitted: EPCC_CLI_READ_ONLY is enabled")

// IsReadOnly returns true if the CLI is in read-only mode
func IsReadOnly() bool {
return config.GetEnv().EPCC_CLI_READ_ONLY
}

func InitializeCmd() {

DumpTraces()
Expand All @@ -84,6 +93,13 @@ func InitializeCmd() {
applyLogLevelEarlyDetectionHack()
log.Tracef("Root Command Building In Progress")

// Check for read-only mode and hide write commands
readOnlyMode := IsReadOnly()
if readOnlyMode {
log.Debugf("Read-only mode is enabled (EPCC_CLI_READ_ONLY=true)")
ResetStore.Hidden = true
}

resources.PublicInit()
initRunbookCommands()
log.Tracef("Runbooks initialized")
Expand Down Expand Up @@ -231,6 +247,7 @@ Environment Variables
- EPCC_CLI_DISABLE_RESOURCES - A comma seperated list of resources that will be hidden in command lists
- EPCC_CLI_RATE_LIMIT - The default rate limit to use.
- EPCC_CLI_DISABLE_HTTP_LOGGING - Disables writing of HTTP logs
- EPCC_CLI_READ_ONLY - Enables read-only mode, blocking create/update/delete operations
`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
log.SetLevel(logger.Loglevel)
Expand Down Expand Up @@ -311,8 +328,11 @@ func Execute() {
<-shutdownHandlerDone

if err != nil {
if errors.Is(err, ErrReadOnlyMode) {
log.Errorf("Error: %s", err)
os.Exit(4)
}
log.Errorf("Error occurred while processing command: %s", err)

os.Exit(1)
} else {
os.Exit(0)
Expand Down
7 changes: 7 additions & 0 deletions cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() {
var updateCmd = &cobra.Command{
Use: "update",
Short: "Updates a resource",
Hidden: IsReadOnly(),
SilenceUsage: false,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if IsReadOnly() {
return ErrReadOnlyMode
}
return RootCmd.PersistentPreRunE(RootCmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please specify a resource, epcc update [RESOURCE], see epcc update --help")
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Env struct {
EPCC_CLI_DISABLE_RESOURCES []string `env:"EPCC_CLI_DISABLE_RESOURCES" envSeparator:","`
EPCC_CLI_DISABLE_TEMPLATE_EXECUTION bool `env:"EPCC_CLI_DISABLE_TEMPLATE_EXECUTION"`
EPCC_CLI_DISABLE_HTTP_LOGGING bool `env:"EPCC_CLI_DISABLE_HTTP_LOGGING"`
EPCC_CLI_READ_ONLY bool `env:"EPCC_CLI_READ_ONLY"`
}

var env = atomic.Pointer[Env]{}
Expand Down
23 changes: 23 additions & 0 deletions external/httpclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p

env := config.GetEnv()

// Read-only mode: block POST, PUT, DELETE, PATCH requests (except auth endpoints)
if env.EPCC_CLI_READ_ONLY {
if method == "POST" || method == "PUT" || method == "DELETE" || method == "PATCH" {
if !isExemptAuthPath(path) {
return nil, fmt.Errorf("HTTP %s request blocked: EPCC_CLI_READ_ONLY is enabled", method)
}
}
}

reqURL, err := url.Parse(env.EPCC_API_BASE_URL)
if err != nil {
return nil, err
Expand Down Expand Up @@ -498,3 +507,17 @@ func AddAdditionalHeadersSpecifiedByFlag(r *http.Request) error {

return nil
}

// isExemptAuthPath returns true if the path is an authentication endpoint
// that should be allowed even in read-only mode.
func isExemptAuthPath(path string) bool {
// Allow customer token creation
if strings.Contains(path, "customer-token") {
return true
}
// Allow account management token creation
if strings.Contains(path, "account-management-authentication-token") {
return true
}
return false
}
11 changes: 11 additions & 0 deletions external/runbooks/run-all-runbooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ set -x
#Let's test that epcc command works after an embarrassing bug that caused it to panic :(
epcc

# Smoke test for EPCC_CLI_READ_ONLY
echo "Starting Read-Only Mode Smoke Test"
epcc reset-store .+

EPCC_CLI_READ_ONLY=true epcc create account --auto-fill && exit 1 || test $? -eq 4
EPCC_CLI_READ_ONLY=true epcc update account 00000000-0000-0000-0000-000000000000 name foo && exit 1 || test $? -eq 4
EPCC_CLI_READ_ONLY=true epcc delete account 00000000-0000-0000-0000-000000000000 && exit 1 || test $? -eq 4
EPCC_CLI_READ_ONLY=true epcc reset-store .+ && exit 1 || test $? -eq 4
EPCC_CLI_READ_ONLY=true epcc delete-all accounts && exit 1 || test $? -eq 4

echo "Read-Only Mode Smoke Test Passed"

echo "Starting Currencies Runbook"
epcc reset-store .+
Expand Down
Loading