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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Enhancements:
- feat(rust): Allow testing with prerelease Rust versions ([#1604](https://github.com/fastly/cli/pull/1604))
- feat(compute/hashfiles): remove hashsum subcommand ([#1608](https://github.com/fastly/cli/pull/1608))
- feat(commands/ngwaf/rules): add support for CRUD operations for NGWAF rules ([#1578](https://github.com/fastly/cli/pull/1605))

### Bug fixes:

Expand Down
26 changes: 26 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/fastly/cli/pkg/commands/ngwaf/countrylist"
"github.com/fastly/cli/pkg/commands/ngwaf/customsignal"
"github.com/fastly/cli/pkg/commands/ngwaf/iplist"
"github.com/fastly/cli/pkg/commands/ngwaf/rule"
"github.com/fastly/cli/pkg/commands/ngwaf/signallist"
"github.com/fastly/cli/pkg/commands/ngwaf/stringlist"
"github.com/fastly/cli/pkg/commands/ngwaf/wildcardlist"
Expand All @@ -76,6 +77,7 @@ import (
wscustomsignal "github.com/fastly/cli/pkg/commands/ngwaf/workspace/customsignal"
wsiplist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/iplist"
"github.com/fastly/cli/pkg/commands/ngwaf/workspace/redaction"
workspaceRule "github.com/fastly/cli/pkg/commands/ngwaf/workspace/rule"
wssignallistlist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/signallist"
wsstringlistlist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/stringlist"
"github.com/fastly/cli/pkg/commands/ngwaf/workspace/threshold"
Expand Down Expand Up @@ -449,6 +451,12 @@ func Define( // nolint:revive // function-length
ngwafIPListGet := iplist.NewGetCommand(ngwafIPListRoot.CmdClause, data)
ngwafIPListList := iplist.NewListCommand(ngwafIPListRoot.CmdClause, data)
ngwafIPListUpdate := iplist.NewUpdateCommand(ngwafIPListRoot.CmdClause, data)
ngwafRuleRoot := rule.NewRootCommand(ngwafRoot.CmdClause, data)
ngwafRuleCreate := rule.NewCreateCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleDelete := rule.NewDeleteCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleGet := rule.NewGetCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleList := rule.NewListCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleUpdate := rule.NewUpdateCommand(ngwafRuleRoot.CmdClause, data)
ngwafSignalListRoot := signallist.NewRootCommand(ngwafRoot.CmdClause, data)
ngwafSignalListCreate := signallist.NewCreateCommand(ngwafSignalListRoot.CmdClause, data)
ngwafSignalListDelete := signallist.NewDeleteCommand(ngwafSignalListRoot.CmdClause, data)
Expand Down Expand Up @@ -485,6 +493,12 @@ func Define( // nolint:revive // function-length
ngwafWorkspaceIPListGet := wsiplist.NewGetCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceIPListList := wsiplist.NewListCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceIPListUpdate := wsiplist.NewUpdateCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceRuleRoot := workspaceRule.NewRootCommand(ngwafWorkspaceRoot.CmdClause, data)
ngwafWorkspaceRuleCreate := workspaceRule.NewCreateCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleDelete := workspaceRule.NewDeleteCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleGet := workspaceRule.NewGetCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleList := workspaceRule.NewListCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleUpdate := workspaceRule.NewUpdateCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceSignalListRoot := wssignallistlist.NewRootCommand(ngwafWorkspaceRoot.CmdClause, data)
ngwafWorkspaceSignalListCreate := wssignallistlist.NewCreateCommand(ngwafWorkspaceSignalListRoot.CmdClause, data)
ngwafWorkspaceSignalListDelete := wssignallistlist.NewDeleteCommand(ngwafWorkspaceSignalListRoot.CmdClause, data)
Expand Down Expand Up @@ -1007,6 +1021,12 @@ func Define( // nolint:revive // function-length
ngwafIPListGet,
ngwafIPListList,
ngwafIPListUpdate,
ngwafRuleRoot,
ngwafRuleCreate,
ngwafRuleDelete,
ngwafRuleGet,
ngwafRuleList,
ngwafRuleUpdate,
ngwafSignalListRoot,
ngwafSignalListCreate,
ngwafSignalListDelete,
Expand Down Expand Up @@ -1042,6 +1062,12 @@ func Define( // nolint:revive // function-length
ngwafWorkspaceIPListGet,
ngwafWorkspaceIPListList,
ngwafWorkspaceIPListUpdate,
ngwafWorkspaceRuleRoot,
ngwafWorkspaceRuleCreate,
ngwafWorkspaceRuleDelete,
ngwafWorkspaceRuleGet,
ngwafWorkspaceRuleList,
ngwafWorkspaceRuleUpdate,
ngwafWorkspaceSignalListRoot,
ngwafWorkspaceSignalListCreate,
ngwafWorkspaceSignalListDelete,
Expand Down
192 changes: 192 additions & 0 deletions pkg/commands/ngwaf/rule/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package rule

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/fastly/go-fastly/v12/fastly"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/rules"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/scope"

"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"
)

// CreateCommand calls the Fastly API to create account-level rules.
type CreateCommand struct {
argparser.Base
argparser.JSONOutput

// Required.
path string
}

// NewCreateCommand 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 an account-level rule").Alias("add")

// Required.
c.CmdClause.Flag("path", "Path to a json file that contains the rule schema.").Required().StringVar(&c.path)

// 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 {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}
var err error
rule := &rules.Rule{}
if c.path != "" {
path, err := filepath.Abs(c.path)
if err != nil {
return fmt.Errorf("error parsing path '%s': %q", c.path, err)
}

jsonFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("error reading path '%s': %q", c.path, err)
}
defer jsonFile.Close()

byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return fmt.Errorf("failed to read json file: %v", err)
}

if err := json.Unmarshal(byteValue, rule); err != nil {
return fmt.Errorf("failed to unmarshal json data: %v", err)
}
}

input := &rules.CreateInput{
Actions: []*rules.CreateAction{},
Conditions: []*rules.CreateCondition{},
Description: &rule.Description,
GroupConditions: []*rules.CreateGroupCondition{},
MultivalConditions: []*rules.CreateMultivalCondition{},
Enabled: &rule.Enabled,
Type: &rule.Type,
GroupOperator: &rule.GroupOperator,
RequestLogging: &rule.RequestLogging,
Scope: &scope.Scope{
Type: scope.ScopeTypeAccount,
AppliesTo: []string{"*"},
},
}

for _, action := range rule.Actions {
input.Actions = append(input.Actions, &rules.CreateAction{
AllowInteractive: action.AllowInteractive,
DeceptionType: &action.DeceptionType,
RedirectURL: &action.RedirectURL,
ResponseCode: &action.ResponseCode,
Signal: &action.Signal,
Type: &action.Type,
})
}

if rule.RateLimit != nil {
input.RateLimit = &rules.CreateRateLimit{
ClientIdentifiers: []*rules.CreateClientIdentifier{},
Duration: &rule.RateLimit.Duration,
Interval: &rule.RateLimit.Interval,
Signal: &rule.RateLimit.Signal,
Threshold: &rule.RateLimit.Threshold,
}

for _, rateLimit := range rule.RateLimit.ClientIdentifiers {
input.RateLimit.ClientIdentifiers = append(input.RateLimit.ClientIdentifiers, &rules.CreateClientIdentifier{
Key: &rateLimit.Key,
Name: &rateLimit.Name,
Type: &rateLimit.Type,
})
}
}

for _, jsonCondition := range rule.Conditions {
switch jsonCondition.Type {
case "single":
if sc, ok := jsonCondition.Fields.(rules.SingleCondition); ok {
input.Conditions = append(input.Conditions, &rules.CreateCondition{
Field: &sc.Field,
Operator: &sc.Operator,
Value: &sc.Value,
})
} else {
return fmt.Errorf("expected SingleCondition, got %T", jsonCondition.Fields)
}
case "group":
if gc, ok := jsonCondition.Fields.(rules.GroupCondition); ok {
parsedGroupCondition := &rules.CreateGroupCondition{
GroupOperator: &gc.GroupOperator,
Conditions: []*rules.CreateCondition{},
}
for _, groupSingleCondition := range gc.Conditions {
parsedGroupCondition.Conditions = append(parsedGroupCondition.Conditions, &rules.CreateCondition{
Field: &groupSingleCondition.Field,
Operator: &groupSingleCondition.Operator,
Value: &groupSingleCondition.Value,
})
}
input.GroupConditions = append(input.GroupConditions, parsedGroupCondition)
} else {
return fmt.Errorf("expected GroupCondition, got %T", jsonCondition.Fields)
}
case "multival":
if mvc, ok := jsonCondition.Fields.(rules.CreateMultivalCondition); ok {
parsedMultiValCondition := &rules.CreateMultivalCondition{
Field: mvc.Field,
GroupOperator: mvc.GroupOperator,
Operator: mvc.Operator,
Conditions: []*rules.CreateConditionMult{},
}
for _, multiSingleCondition := range mvc.Conditions {
parsedMultiValCondition.Conditions = append(parsedMultiValCondition.Conditions, &rules.CreateConditionMult{
Field: multiSingleCondition.Field,
Operator: multiSingleCondition.Operator,
Value: multiSingleCondition.Value,
})
}
input.MultivalConditions = append(input.MultivalConditions, parsedMultiValCondition)
} else {
return fmt.Errorf("expected MultivalCondition, got %T", jsonCondition.Fields)
}
default:
return fmt.Errorf("unknown condition type: %s", jsonCondition.Type)
}
}

fc, ok := c.Globals.APIClient.(*fastly.Client)
if !ok {
return errors.New("failed to convert interface to a fastly client")
}

data, err := rules.Create(context.TODO(), fc, input)
if err != nil {
return err
}

if ok, err := c.WriteJSON(out, data); ok {
return err
}

text.Success(out, "Created account-level rule with ID %s", data.RuleID)
return nil
}
84 changes: 84 additions & 0 deletions pkg/commands/ngwaf/rule/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package rule

import (
"context"
"errors"
"io"

"github.com/fastly/go-fastly/v12/fastly"

"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/rules"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/scope"

"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 an account-level rule.
type DeleteCommand struct {
argparser.Base
argparser.JSONOutput

// Required.
ruleID 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 an account-level rule")

// Required.
c.CmdClause.Flag("rule-id", "Rule ID").Required().StringVar(&c.ruleID)

// 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 {
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 := rules.Delete(context.TODO(), fc, &rules.DeleteInput{
RuleID: &c.ruleID,
Scope: &scope.Scope{
Type: scope.ScopeTypeAccount,
AppliesTo: []string{"*"},
},
})
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.ruleID,
true,
}
_, err := c.WriteJSON(out, o)
return err
}

text.Success(out, "Deleted account-level rule with id: %s", c.ruleID)
return nil
}
2 changes: 2 additions & 0 deletions pkg/commands/ngwaf/rule/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package rule contains commands to inspect and manipulate NGWAF account-level rules.
package rule
Loading