Skip to content

Commit d45b89c

Browse files
committed
feat: add support for EPCC_CLI_READ_ONLY
1 parent 95accd4 commit d45b89c

File tree

10 files changed

+93
-3
lines changed

10 files changed

+93
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ The following environment variables can be set up to control which environment a
117117
| EPCC_CLI_DISABLE_RESOURCES | A comma seperated list of resources that will not be available with commands or in the resource list |
118118
| EPCC_CLI_RATE_LIMIT | The default rate limit to use |
119119
| EPCC_CLI_DISABLE_HTTP_LOGGING | Disables writing of HTTP logs |
120+
| EPCC_CLI_READ_ONLY | Enables read-only mode, blocking create/update/delete operations. Commands are hidden and return exit code 4 if attempted. |
120121

121122
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.
122123

cmd/create.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ import (
2121
func NewCreateCommand(parentCmd *cobra.Command) func() {
2222

2323
var createCmd = &cobra.Command{
24-
Use: "create",
25-
Short: "Creates a resource",
24+
Use: "create",
25+
Short: "Creates a resource",
26+
Hidden: IsReadOnly(),
27+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
28+
if IsReadOnly() {
29+
return ErrReadOnlyMode
30+
}
31+
return RootCmd.PersistentPreRunE(RootCmd, args)
32+
},
2633
RunE: func(cmd *cobra.Command, args []string) error {
2734
if len(args) == 0 {
2835
return fmt.Errorf("please specify a resource, epcc create [RESOURCE], see epcc create --help")

cmd/delete-all.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ func NewDeleteAllCommand(parentCmd *cobra.Command) func() {
3333
var deleteAll = &cobra.Command{
3434
Use: "delete-all",
3535
Short: "Deletes all of a resource",
36+
Hidden: IsReadOnly(),
3637
SilenceUsage: false,
38+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
39+
if IsReadOnly() {
40+
return ErrReadOnlyMode
41+
}
42+
return RootCmd.PersistentPreRunE(RootCmd, args)
43+
},
3744
RunE: func(cmd *cobra.Command, args []string) error {
3845
if len(args) == 0 {
3946
return fmt.Errorf("please specify a resource, epcc delete-all [RESOURCE], see epcc delete-all --help")

cmd/delete.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() {
2020
var deleteCmd = &cobra.Command{
2121
Use: "delete",
2222
Short: "Deletes a resource",
23+
Hidden: IsReadOnly(),
2324
SilenceUsage: false,
25+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
26+
if IsReadOnly() {
27+
return ErrReadOnlyMode
28+
}
29+
return RootCmd.PersistentPreRunE(RootCmd, args)
30+
},
2431
RunE: func(cmd *cobra.Command, args []string) error {
2532
if len(args) == 0 {
2633
return fmt.Errorf("please specify a resource, epcc delete [RESOURCE], see epcc delete --help")

cmd/reset-store.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ var ResetStore = &cobra.Command{
3030
Short: "Resets a store to it's initial state on a \"best effort\" basis.",
3131
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.",
3232
Args: cobra.MinimumNArgs(1),
33+
PreRunE: func(cmd *cobra.Command, args []string) error {
34+
if IsReadOnly() {
35+
return ErrReadOnlyMode
36+
}
37+
return RootCmd.PersistentPreRunE(RootCmd, args)
38+
},
3339
RunE: func(cmd *cobra.Command, args []string) error {
3440
ctx := clictx.Ctx
3541

cmd/root.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"os/signal"
@@ -63,6 +64,14 @@ var jqCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string
6364

6465
var profileNameFromCommandLine = ""
6566

67+
// ErrReadOnlyMode is returned when a write operation is attempted in read-only mode
68+
var ErrReadOnlyMode = errors.New("operation not permitted: EPCC_CLI_READ_ONLY is enabled")
69+
70+
// IsReadOnly returns true if the CLI is in read-only mode
71+
func IsReadOnly() bool {
72+
return config.GetEnv().EPCC_CLI_READ_ONLY
73+
}
74+
6675
func InitializeCmd() {
6776

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

96+
// Check for read-only mode and hide write commands
97+
readOnlyMode := IsReadOnly()
98+
if readOnlyMode {
99+
log.Debugf("Read-only mode is enabled (EPCC_CLI_READ_ONLY=true)")
100+
ResetStore.Hidden = true
101+
}
102+
87103
resources.PublicInit()
88104
initRunbookCommands()
89105
log.Tracef("Runbooks initialized")
@@ -231,6 +247,7 @@ Environment Variables
231247
- EPCC_CLI_DISABLE_RESOURCES - A comma seperated list of resources that will be hidden in command lists
232248
- EPCC_CLI_RATE_LIMIT - The default rate limit to use.
233249
- EPCC_CLI_DISABLE_HTTP_LOGGING - Disables writing of HTTP logs
250+
- EPCC_CLI_READ_ONLY - Enables read-only mode, blocking create/update/delete operations
234251
`,
235252
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
236253
log.SetLevel(logger.Loglevel)
@@ -311,8 +328,11 @@ func Execute() {
311328
<-shutdownHandlerDone
312329

313330
if err != nil {
331+
if errors.Is(err, ErrReadOnlyMode) {
332+
log.Errorf("Error: %s", err)
333+
os.Exit(4)
334+
}
314335
log.Errorf("Error occurred while processing command: %s", err)
315-
316336
os.Exit(1)
317337
} else {
318338
os.Exit(0)

cmd/update.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() {
7373
var updateCmd = &cobra.Command{
7474
Use: "update",
7575
Short: "Updates a resource",
76+
Hidden: IsReadOnly(),
7677
SilenceUsage: false,
78+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
79+
if IsReadOnly() {
80+
return ErrReadOnlyMode
81+
}
82+
return RootCmd.PersistentPreRunE(RootCmd, args)
83+
},
7784
RunE: func(cmd *cobra.Command, args []string) error {
7885
if len(args) == 0 {
7986
return fmt.Errorf("please specify a resource, epcc update [RESOURCE], see epcc update --help")

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Env struct {
1414
EPCC_CLI_DISABLE_RESOURCES []string `env:"EPCC_CLI_DISABLE_RESOURCES" envSeparator:","`
1515
EPCC_CLI_DISABLE_TEMPLATE_EXECUTION bool `env:"EPCC_CLI_DISABLE_TEMPLATE_EXECUTION"`
1616
EPCC_CLI_DISABLE_HTTP_LOGGING bool `env:"EPCC_CLI_DISABLE_HTTP_LOGGING"`
17+
EPCC_CLI_READ_ONLY bool `env:"EPCC_CLI_READ_ONLY"`
1718
}
1819

1920
var env = atomic.Pointer[Env]{}

external/httpclient/httpclient.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p
205205

206206
env := config.GetEnv()
207207

208+
// Read-only mode: block POST, PUT, DELETE, PATCH requests (except auth endpoints)
209+
if env.EPCC_CLI_READ_ONLY {
210+
if method == "POST" || method == "PUT" || method == "DELETE" || method == "PATCH" {
211+
if !isExemptAuthPath(path) {
212+
return nil, fmt.Errorf("HTTP %s request blocked: EPCC_CLI_READ_ONLY is enabled", method)
213+
}
214+
}
215+
}
216+
208217
reqURL, err := url.Parse(env.EPCC_API_BASE_URL)
209218
if err != nil {
210219
return nil, err
@@ -498,3 +507,17 @@ func AddAdditionalHeadersSpecifiedByFlag(r *http.Request) error {
498507

499508
return nil
500509
}
510+
511+
// isExemptAuthPath returns true if the path is an authentication endpoint
512+
// that should be allowed even in read-only mode.
513+
func isExemptAuthPath(path string) bool {
514+
// Allow customer token creation
515+
if strings.Contains(path, "customer-token") {
516+
return true
517+
}
518+
// Allow account management token creation
519+
if strings.Contains(path, "account-management-authentication-token") {
520+
return true
521+
}
522+
return false
523+
}

external/runbooks/run-all-runbooks.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ set -x
1313
#Let's test that epcc command works after an embarrassing bug that caused it to panic :(
1414
epcc
1515

16+
# Smoke test for EPCC_CLI_READ_ONLY
17+
echo "Starting Read-Only Mode Smoke Test"
18+
epcc reset-store .+
19+
20+
EPCC_CLI_READ_ONLY=true epcc create account --auto-fill && exit 1 || test $? -eq 4
21+
EPCC_CLI_READ_ONLY=true epcc update account 00000000-0000-0000-0000-000000000000 name foo && exit 1 || test $? -eq 4
22+
EPCC_CLI_READ_ONLY=true epcc delete account 00000000-0000-0000-0000-000000000000 && exit 1 || test $? -eq 4
23+
EPCC_CLI_READ_ONLY=true epcc reset-store .+ && exit 1 || test $? -eq 4
24+
EPCC_CLI_READ_ONLY=true epcc delete-all accounts && exit 1 || test $? -eq 4
25+
26+
echo "Read-Only Mode Smoke Test Passed"
1627

1728
echo "Starting Currencies Runbook"
1829
epcc reset-store .+

0 commit comments

Comments
 (0)