-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(max-tokens): Implement max-tokens rule, add documentation & test… (
#12) Implement max-tokens rule, add documentation & test… cases, update README.md Co-authored-by: ldebruijn <[email protected]>
- Loading branch information
Showing
8 changed files
with
262 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Max Tokens | ||
|
||
Restricting the maximum number of tokens in an operation helps prevent excessively large operations reaching your landscape. | ||
This can be useful to prevent DDoS attacks, Heap Overflows or Server overload. | ||
|
||
<!-- TOC --> | ||
|
||
## Configuration | ||
|
||
You can configure `go-graphql-armor` to limit the maximum number of tokens allowed on an operation. | ||
|
||
```yaml | ||
max_tokens: | ||
# Enable the feature | ||
enable: "true" | ||
# The maximum number of allowed tokens within a single request. | ||
max: 10000 | ||
# Reject the request when the rule fails. Disable this to allow the request regardless of token count. | ||
reject_on_failure: "true" | ||
``` | ||
## Metrics | ||
This rule produces metrics to help you gain insights into the behavior of the rule. | ||
``` | ||
go_graphql_armor_max_tokens_results{result} | ||
``` | ||
|
||
|
||
| `result` | Description | | ||
|---------|--------------------------------------------------------------------------------------------------------------| | ||
| `allowed` | The rule condition succeeded | | ||
| `rejected` | The rule condition failed and the request was rejected | | ||
| `failed` | The rule condition failed but the request was not rejected. This happens when `reject_on_failure` is `false` | | ||
|
||
No metrics are produced when the rule is disabled. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package tokens | ||
|
||
import ( | ||
"fmt" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/vektah/gqlparser/v2/ast" | ||
"github.com/vektah/gqlparser/v2/lexer" | ||
) | ||
|
||
var resultCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
Namespace: "go_graphql_armor", | ||
Subsystem: "max_tokens", | ||
Name: "results", | ||
Help: "The results of the max tokens rule", | ||
}, | ||
[]string{"result"}, | ||
) | ||
|
||
type Config struct { | ||
Enabled bool `conf:"default:true" yaml:"enabled"` | ||
Max int `conf:"default:10000" yaml:"max"` | ||
RejectOnFailure bool `conf:"default:true" yaml:"reject-on-failure"` | ||
} | ||
|
||
type MaxTokensRule struct { | ||
cfg Config | ||
} | ||
|
||
func MaxTokens(cfg Config) *MaxTokensRule { | ||
return &MaxTokensRule{ | ||
cfg: cfg, | ||
} | ||
} | ||
|
||
func (t *MaxTokensRule) Validate(source *ast.Source) error { | ||
if !t.cfg.Enabled { | ||
return nil | ||
} | ||
|
||
lex := lexer.New(source) | ||
count := 0 | ||
|
||
for { | ||
tok, err := lex.ReadToken() | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
if tok.Kind == lexer.EOF { | ||
break | ||
} | ||
|
||
count++ | ||
} | ||
|
||
if count > t.cfg.Max { | ||
if t.cfg.RejectOnFailure { | ||
resultCounter.WithLabelValues("rejected").Inc() | ||
return fmt.Errorf("operation has exceeded maximum tokens. found [%d], max [%d]", count, t.cfg.Max) | ||
} | ||
resultCounter.WithLabelValues("failed").Inc() | ||
} | ||
resultCounter.WithLabelValues("allowed").Inc() | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package tokens | ||
|
||
import ( | ||
"github.com/stretchr/testify/assert" | ||
"github.com/vektah/gqlparser/v2/ast" | ||
"testing" | ||
) | ||
|
||
func TestMaxTokens(t *testing.T) { | ||
type args struct { | ||
cfg Config | ||
operation string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "rule disabled does nothing", | ||
args: args{ | ||
cfg: Config{ | ||
Enabled: false, | ||
Max: 0, | ||
RejectOnFailure: true, | ||
}, | ||
operation: "query { foo }", | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "yields error when tokens exceed max", | ||
args: args{ | ||
cfg: Config{ | ||
Enabled: true, | ||
Max: 1, | ||
RejectOnFailure: true, | ||
}, | ||
operation: "query { foo }", | ||
}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "yields no error when tokens less than max", | ||
args: args{ | ||
cfg: Config{ | ||
Enabled: true, | ||
Max: 1000000, | ||
RejectOnFailure: true, | ||
}, | ||
operation: "query { foo }", | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "yields no error when tokens exceed max but failure on rejections is false", | ||
args: args{ | ||
cfg: Config{ | ||
Enabled: true, | ||
Max: 1, | ||
RejectOnFailure: false, | ||
}, | ||
operation: "query { foo }", | ||
}, | ||
wantErr: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
rule := MaxTokens(tt.args.cfg) | ||
|
||
source := &ast.Source{ | ||
Input: tt.args.operation, | ||
} | ||
|
||
err := rule.Validate(source) | ||
|
||
if tt.wantErr { | ||
assert.Error(t, err) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} |