diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab3beae1..5e43db06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - feat(compute/build): Allow usage of Rust 1.91.1 and later patch releases ([#1576](https://github.com/fastly/cli/pull/1576)) - feat(commands/ngwaf/workspaces): add support for CRUD operations for NGWAF workspaces ([#1570](https://github.com/fastly/cli/pull/1570)) - feat(commands/ngwaf/virtualpatch): add support for CRUD operations for NGWAF virtual patches ([#1579](https://github.com/fastly/cli/pull/1579)) +- feat(commands/ngwaf/redaction): add support for CRUD operations for NGWAF redactions ([#1581](https://github.com/fastly/cli/pull/1581)) ### Dependencies: - build(deps): `golangci/golangci-lint-action` from 8 to 9 ([#1575](https://github.com/fastly/cli/pull/1575)) diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index bf2328c21..67e7c3832 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -56,6 +56,7 @@ import ( "github.com/fastly/cli/pkg/commands/logging/syslog" "github.com/fastly/cli/pkg/commands/logtail" "github.com/fastly/cli/pkg/commands/ngwaf" + "github.com/fastly/cli/pkg/commands/ngwaf/redaction" "github.com/fastly/cli/pkg/commands/ngwaf/virtualpatch" "github.com/fastly/cli/pkg/commands/ngwaf/workspace" "github.com/fastly/cli/pkg/commands/objectstorage" @@ -397,6 +398,12 @@ func Define( // nolint:revive // function-length loggingSyslogList := syslog.NewListCommand(loggingSyslogCmdRoot.CmdClause, data) loggingSyslogUpdate := syslog.NewUpdateCommand(loggingSyslogCmdRoot.CmdClause, data) ngwafRoot := ngwaf.NewRootCommand(app, data) + ngwafRedactionRoot := redaction.NewRootCommand(ngwafRoot.CmdClause, data) + ngwafRedactionCreate := redaction.NewCreateCommand(ngwafRedactionRoot.CmdClause, data) + ngwafRedactionDelete := redaction.NewDeleteCommand(ngwafRedactionRoot.CmdClause, data) + ngwafRedactionList := redaction.NewListCommand(ngwafRedactionRoot.CmdClause, data) + ngwafRedactionRetrieve := redaction.NewRetrieveCommand(ngwafRedactionRoot.CmdClause, data) + ngwafRedactionUpdate := redaction.NewUpdateCommand(ngwafRedactionRoot.CmdClause, data) ngwafVirtualpatchRoot := virtualpatch.NewRootCommand(ngwafRoot.CmdClause, data) ngwafVirtualpatchList := virtualpatch.NewListCommand(ngwafVirtualpatchRoot.CmdClause, data) ngwafVirtualpatchUpdate := virtualpatch.NewUpdateCommand(ngwafVirtualpatchRoot.CmdClause, data) @@ -827,6 +834,12 @@ func Define( // nolint:revive // function-length loggingSyslogList, loggingSyslogUpdate, ngwafRoot, + ngwafRedactionCreate, + ngwafRedactionDelete, + ngwafRedactionList, + ngwafRedactionRetrieve, + ngwafRedactionUpdate, + ngwafRedactionRoot, ngwafVirtualpatchList, ngwafVirtualpatchRetrieve, ngwafVirtualpatchRoot, diff --git a/pkg/commands/ngwaf/redaction/create.go b/pkg/commands/ngwaf/redaction/create.go new file mode 100644 index 000000000..c45d9c3b8 --- /dev/null +++ b/pkg/commands/ngwaf/redaction/create.go @@ -0,0 +1,83 @@ +package redaction + +import ( + "context" + "errors" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// CreateCommand calls the Fastly API to create a redaction. +type CreateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + field string + redactionType string + workspaceID argparser.OptionalWorkspaceID +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand { + c := CreateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("create", "Create a redaction") + + // Required. + c.CmdClause.Flag("field", "The name of the field that should be redacted.").Required().StringVar(&c.field) + c.CmdClause.Flag("type", "The type of field that is being redacted.").Required().StringVar(&c.redactionType) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagNGWAFWorkspaceID, + Description: argparser.FlagNGWAFWorkspaceIDDesc, + Dst: &c.workspaceID.Value, + Action: c.workspaceID.Set, + }) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { + // Call Parse() to ensure that we check if workspaceID + // is set or to throw the appropriate error. + if err := c.workspaceID.Parse(); err != nil { + return err + } + + var err error + input := &redactions.CreateInput{ + Field: &c.field, + Type: &c.redactionType, + WorkspaceID: &c.workspaceID.Value, + } + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + data, err := redactions.Create(context.TODO(), fc, input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, data); ok { + return err + } + + text.Success(out, "Created redaction '%s' (field: %s, type: %s)", data.RedactionID, data.Field, data.Type) + return nil +} diff --git a/pkg/commands/ngwaf/redaction/delete.go b/pkg/commands/ngwaf/redaction/delete.go new file mode 100644 index 000000000..18ca6bdc1 --- /dev/null +++ b/pkg/commands/ngwaf/redaction/delete.go @@ -0,0 +1,93 @@ +package redaction + +import ( + "context" + "errors" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// DeleteCommand calls the Fastly API to delete a workspace. +type DeleteCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + workspaceID argparser.OptionalWorkspaceID + redactionID string +} + +// NewDeleteCommand returns a usable command registered under the parent. +func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteCommand { + c := DeleteCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + + c.CmdClause = parent.Command("delete", "Delete a redaction") + + // Required. + c.CmdClause.Flag("redaction-id", "Workspace ID").Required().StringVar(&c.redactionID) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagNGWAFWorkspaceID, + Description: argparser.FlagNGWAFWorkspaceIDDesc, + Dst: &c.workspaceID.Value, + Action: c.workspaceID.Set, + }) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error { + // Call Parse() to ensure that we check if workspaceID + // is set or to throw the appropriate error. + if err := c.workspaceID.Parse(); err != nil { + return err + } + + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + err := redactions.Delete(context.TODO(), fc, &redactions.DeleteInput{ + RedactionID: &c.redactionID, + WorkspaceID: &c.workspaceID.Value, + }) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if c.JSONOutput.Enabled { + o := struct { + ID string `json:"id"` + Deleted bool `json:"deleted"` + }{ + c.redactionID, + true, + } + _, err := c.WriteJSON(out, o) + return err + } + + text.Success(out, "Deleted redaction (id: %s)", c.redactionID) + return nil +} diff --git a/pkg/commands/ngwaf/redaction/list.go b/pkg/commands/ngwaf/redaction/list.go new file mode 100644 index 000000000..20f822055 --- /dev/null +++ b/pkg/commands/ngwaf/redaction/list.go @@ -0,0 +1,91 @@ +package redaction + +import ( + "context" + "errors" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// ListCommand calls the Fastly API to list redactions in a workspace. +type ListCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + workspaceID argparser.OptionalWorkspaceID + + // Optional. + limit argparser.OptionalInt +} + +// NewListCommand returns a usable command registered under the parent. +func NewListCommand(parent argparser.Registerer, g *global.Data) *ListCommand { + c := ListCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + + c.CmdClause = parent.Command("list", "List redactions in a workspace") + + // Required. + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagNGWAFWorkspaceID, + Description: argparser.FlagNGWAFWorkspaceIDDesc, + Dst: &c.workspaceID.Value, + Action: c.workspaceID.Set, + }) + + // Optional. + c.CmdClause.Flag("limit", "Limit how many results are returned").Action(c.limit.Set).IntVar(&c.limit.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *ListCommand) Exec(_ io.Reader, out io.Writer) error { + // Call Parse() to ensure that we check if workspaceID + // is set or to throw the appropriate error. + if err := c.workspaceID.Parse(); err != nil { + return err + } + + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + input := &redactions.ListInput{ + WorkspaceID: &c.workspaceID.Value, + } + + if c.limit.WasSet { + input.Limit = &c.limit.Value + } + + data, err := redactions.List(context.TODO(), fc, input) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, data); ok { + return err + } + + text.PrintRedactionTbl(out, data.Data) + return nil +} diff --git a/pkg/commands/ngwaf/redaction/redaction_test.go b/pkg/commands/ngwaf/redaction/redaction_test.go new file mode 100644 index 000000000..5dd3cfb2c --- /dev/null +++ b/pkg/commands/ngwaf/redaction/redaction_test.go @@ -0,0 +1,375 @@ +package redaction_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strings" + "testing" + + root "github.com/fastly/cli/pkg/commands/ngwaf" + sub "github.com/fastly/cli/pkg/commands/ngwaf/redaction" + fstfmt "github.com/fastly/cli/pkg/fmt" + "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" +) + +const ( + redactionField = "password" + redactionID = "someID" + redactionType = "request" + workspaceID = "workspaceID" +) + +var redaction = redactions.Redaction{ + CreatedAt: testutil.Date, + Field: redactionField, + RedactionID: redactionID, + Type: redactionType, +} + +func TestRedactionCreate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --field flag", + Args: fmt.Sprintf("--type %s --workspace-id %s", redactionType, workspaceID), + WantError: "error parsing arguments: required flag --field not provided", + }, + { + Name: "validate missing --type flag", + Args: fmt.Sprintf("--field %s --workspace-id %s", redactionField, workspaceID), + WantError: "error parsing arguments: required flag --type not provided", + }, + { + Name: "validate missing --workspace-id flag", + Args: fmt.Sprintf("--field %s --type %s", redactionField, redactionType), + WantError: "error reading workspace ID: no workspace ID found", + }, + { + Name: "validate internal server error", + Args: fmt.Sprintf("--field %s --type %s --workspace-id %s", redactionField, redactionType, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusInternalServerError, + Status: http.StatusText(http.StatusInternalServerError), + }, + }, + }, + WantError: "500 - Internal Server Error", + }, + { + Name: "validate API success", + Args: fmt.Sprintf("--field %s --type %s --workspace-id %s", redactionField, redactionType, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader((testutil.GenJSON(redaction)))), + }, + }, + }, + WantOutput: fstfmt.Success("Created redaction '%s' (field: %s, type: %s)", redactionID, redactionField, redactionType), + }, + { + Name: "validate optional --json flag", + Args: fmt.Sprintf("--field %s --type %s --workspace-id %s --json", redactionField, redactionType, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redaction))), + }, + }, + }, + WantOutput: fstfmt.EncodeJSON(redaction), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "create"}, scenarios) +} + +func TestRedactionDelete(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --redaction-id flag", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + WantError: "error parsing arguments: required flag --redaction-id not provided", + }, + { + Name: "validate missing --workspace-id flag", + Args: fmt.Sprintf("--redaction-id %s", redactionID), + WantError: "error reading workspace ID: no workspace ID found", + }, + { + Name: "validate bad request", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` + { + "title": "invalid Redaction ID", + "status": 400 + } + `))), + }, + }, + }, + WantError: "400 - Bad Request", + }, + { + Name: "validate API success", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusNoContent, + Status: http.StatusText(http.StatusNoContent), + }, + }, + }, + WantOutput: fstfmt.Success("Deleted redaction (id: %s)", redactionID), + }, + { + Name: "validate optional --json flag", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s --json", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusNoContent, + Status: http.StatusText(http.StatusNoContent), + }, + }, + }, + WantOutput: fstfmt.JSON(`{"id": %q, "deleted": true}`, redactionID), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "delete"}, scenarios) +} + +func TestRedactionRetrieve(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --redaction-id flag", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + WantError: "error parsing arguments: required flag --redaction-id not provided", + }, + { + Name: "validate missing --workspace-id flag", + Args: fmt.Sprintf("--redaction-id %s", redactionID), + WantError: "error reading workspace ID: no workspace ID found", + }, + { + Name: "validate bad request", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(` + { + "title": "invalid Redaction ID", + "status": 400 + } + `))), + }, + }, + }, + WantError: "400 - Bad Request", + }, + { + Name: "validate API success", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader((testutil.GenJSON(redaction)))), + }, + }, + }, + WantOutput: redactionString, + }, + { + Name: "validate optional --json flag", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s --json", redactionID, workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader((testutil.GenJSON(redaction)))), + }, + }, + }, + WantOutput: fstfmt.EncodeJSON(redaction), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "retrieve"}, scenarios) +} + +func TestRedactionList(t *testing.T) { + redactionsObject := redactions.Redactions{ + Data: []redactions.Redaction{ + { + CreatedAt: testutil.Date, + Field: redactionField, + RedactionID: redactionID, + Type: redactionType, + }, + { + CreatedAt: testutil.Date, + Field: "username", + RedactionID: redactionID + "2", + Type: redactionType, + }, + }, + Meta: redactions.MetaRedactions{}, + } + + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --workspace-id flag", + Args: "", + WantError: "error reading workspace ID: no workspace ID found", + }, + { + Name: "validate internal server error", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusInternalServerError, + Status: http.StatusText(http.StatusInternalServerError), + }, + }, + }, + WantError: "500 - Internal Server Error", + }, + { + Name: "validate API success (zero redactions)", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redactions.Redactions{ + Data: []redactions.Redaction{}, + Meta: redactions.MetaRedactions{}, + }))), + }, + }, + }, + WantOutput: zeroListRedactionString, + }, + { + Name: "validate API success", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redactionsObject))), + }, + }, + }, + WantOutput: listRedactionString, + }, + { + Name: "validate optional --json flag", + Args: fmt.Sprintf("--workspace-id %s --json", workspaceID), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redactionsObject))), + }, + }, + }, + WantOutput: fstfmt.EncodeJSON(redactionsObject), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "list"}, scenarios) +} + +func TestRedactionUpdate(t *testing.T) { + redactionsObject := redactions.Redaction{ + CreatedAt: testutil.Date, + Field: redactionField, + RedactionID: redactionID, + Type: redactionType, + } + + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --redaction-id flag", + Args: fmt.Sprintf("--workspace-id %s", workspaceID), + WantError: "error parsing arguments: required flag --redaction-id not provided", + }, + { + Name: "validate missing --workspace-id flag", + Args: fmt.Sprintf("--redaction-id %s", redactionID), + WantError: "error reading workspace ID: no workspace ID found", + }, + { + Name: "validate API success", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s --field %s --type %s", redactionID, workspaceID, redactionField, redactionType), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redactionsObject))), + }, + }, + }, + WantOutput: fstfmt.Success("Updated redaction '%s' (field: %s, type: %s)", redactionID, redactionField, redactionType), + }, + { + Name: "validate optional --json flag", + Args: fmt.Sprintf("--redaction-id %s --workspace-id %s --field %s --type %s --json", redactionID, workspaceID, redactionField, redactionType), + Client: &http.Client{ + Transport: &testutil.MockRoundTripper{ + Response: &http.Response{ + StatusCode: http.StatusOK, + Status: http.StatusText(http.StatusOK), + Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(redaction))), + }, + }, + }, + WantOutput: fstfmt.EncodeJSON(redaction), + }, + } + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "update"}, scenarios) +} + +var listRedactionString = strings.TrimSpace(` +Field ID Type Created At +password someID request 2021-06-15 23:00:00 +0000 UTC +username someID2 request 2021-06-15 23:00:00 +0000 UTC +`) + "\n" + +var zeroListRedactionString = strings.TrimSpace(` +Field ID Type Created At +`) + "\n" + +var redactionString = strings.TrimSpace(` +Field: password +ID: someID +Type: request +Created At: 2021-06-15 23:00:00 +0000 UTC +`) diff --git a/pkg/commands/ngwaf/redaction/retrieve.go b/pkg/commands/ngwaf/redaction/retrieve.go new file mode 100644 index 000000000..073faed75 --- /dev/null +++ b/pkg/commands/ngwaf/redaction/retrieve.go @@ -0,0 +1,84 @@ +package redaction + +import ( + "context" + "errors" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" + + "github.com/fastly/cli/pkg/argparser" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// GetCommand calls the Fastly API to get a workspace. +type GetCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + redactionID string + workspaceID argparser.OptionalWorkspaceID +} + +// NewGetCommand returns a usable command registered under the parent. +func NewRetrieveCommand(parent argparser.Registerer, g *global.Data) *GetCommand { + c := GetCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + + c.CmdClause = parent.Command("retrieve", "Retrieve a redaction") + + // Required. + c.CmdClause.Flag("redaction-id", "Redaction ID").Required().StringVar(&c.redactionID) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagNGWAFWorkspaceID, + Description: argparser.FlagNGWAFWorkspaceIDDesc, + Dst: &c.workspaceID.Value, + Action: c.workspaceID.Set, + }) + + // Optional. + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *GetCommand) Exec(_ io.Reader, out io.Writer) error { + // Call Parse() to ensure that we check if workspaceID + // is set or to throw the appropriate error. + if err := c.workspaceID.Parse(); err != nil { + return err + } + + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + data, err := redactions.Get(context.TODO(), fc, &redactions.GetInput{ + RedactionID: &c.redactionID, + WorkspaceID: &c.workspaceID.Value, + }) + if err != nil { + c.Globals.ErrLog.Add(err) + return err + } + + if ok, err := c.WriteJSON(out, data); ok { + return err + } + + text.PrintRedaction(out, data) + return nil +} diff --git a/pkg/commands/ngwaf/redaction/root.go b/pkg/commands/ngwaf/redaction/root.go new file mode 100644 index 000000000..a43e02986 --- /dev/null +++ b/pkg/commands/ngwaf/redaction/root.go @@ -0,0 +1,31 @@ +package redaction + +import ( + "io" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" +) + +// RootCommand is the parent command for all subcommands in this package. +// It should be installed under the primary root command. +type RootCommand struct { + argparser.Base + // no flags +} + +// CommandName is the string to be used to invoke this command. +const CommandName = "redaction" + +// NewRootCommand returns a new command registered in the parent. +func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { + var c RootCommand + c.Globals = g + c.CmdClause = parent.Command(CommandName, "Manage NGWAF Redactions") + return &c +} + +// Exec implements the command interface. +func (c *RootCommand) Exec(_ io.Reader, _ io.Writer) error { + panic("unreachable") +} diff --git a/pkg/commands/ngwaf/redaction/update.go b/pkg/commands/ngwaf/redaction/update.go new file mode 100644 index 000000000..9afa54e7b --- /dev/null +++ b/pkg/commands/ngwaf/redaction/update.go @@ -0,0 +1,94 @@ +package redaction + +import ( + "context" + "errors" + "io" + + "github.com/fastly/go-fastly/v12/fastly" + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" +) + +// UpdateCommand calls the Fastly API to update redactions. +type UpdateCommand struct { + argparser.Base + argparser.JSONOutput + + // Required. + redactionID string + workspaceID argparser.OptionalWorkspaceID + + // Optional. + field argparser.OptionalString + redactionType argparser.OptionalString +} + +// NewUpdateCommand returns a usable command registered under the parent. +func NewUpdateCommand(parent argparser.Registerer, g *global.Data) *UpdateCommand { + c := UpdateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.CmdClause = parent.Command("update", "Update a redaction") + + // Required. + c.CmdClause.Flag("redaction-id", "A base62-encoded representation of a UUID used to uniquely identify a redaction.").Required().StringVar(&c.redactionID) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagNGWAFWorkspaceID, + Description: argparser.FlagNGWAFWorkspaceIDDesc, + Dst: &c.workspaceID.Value, + Action: c.workspaceID.Set, + }) + + // Optional. + c.CmdClause.Flag("field", "The name of the field that should be redacted.").Action(c.field.Set).StringVar(&c.field.Value) + c.CmdClause.Flag("type", "The type of field that is being redacted.").Action(c.redactionType.Set).StringVar(&c.redactionType.Value) + c.RegisterFlagBool(c.JSONFlag()) + + return &c +} + +// Exec invokes the application logic for the command. +func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) error { + // Call Parse() to ensure that we check if workspaceID + // is set or to throw the appropriate error. + if err := c.workspaceID.Parse(); err != nil { + return err + } + + var err error + input := &redactions.UpdateInput{ + RedactionID: &c.redactionID, + WorkspaceID: &c.workspaceID.Value, + } + + if c.field.WasSet { + input.Field = &c.field.Value + } + + if c.redactionType.WasSet { + input.Type = &c.redactionType.Value + } + + fc, ok := c.Globals.APIClient.(*fastly.Client) + if !ok { + return errors.New("failed to convert interface to a fastly client") + } + + data, err := redactions.Update(context.TODO(), fc, input) + if err != nil { + return err + } + + if ok, err := c.WriteJSON(out, data); ok { + return err + } + + text.Success(out, "Updated redaction '%s' (field: %s, type: %s)", data.RedactionID, data.Field, data.Type) + return nil +} diff --git a/pkg/text/redaction.go b/pkg/text/redaction.go new file mode 100644 index 000000000..7934b9dcb --- /dev/null +++ b/pkg/text/redaction.go @@ -0,0 +1,32 @@ +package text + +import ( + "fmt" + "io" + + "github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/workspaces/redactions" +) + +// PrintRedaction displays a single redaction. +func PrintRedaction(out io.Writer, redactionToPrint *redactions.Redaction) { + fmt.Fprintf(out, "Field: %s\n", redactionToPrint.Field) + fmt.Fprintf(out, "ID: %s\n", redactionToPrint.RedactionID) + fmt.Fprintf(out, "Type: %s\n", redactionToPrint.Type) + fmt.Fprintf(out, "Created At: %s\n", redactionToPrint.CreatedAt) +} + +// PrintRedactionTbl prints a table of redactions. +func PrintRedactionTbl(out io.Writer, redactionsToPrint []redactions.Redaction) { + tbl := NewTable(out) + tbl.AddHeader("Field", "ID", "Type", "Created At") + + if redactionsToPrint == nil { + tbl.Print() + return + } + + for _, rd := range redactionsToPrint { + tbl.AddLine(rd.Field, rd.RedactionID, rd.Type, rd.CreatedAt) + } + tbl.Print() +}