-
-
Notifications
You must be signed in to change notification settings - Fork 234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert library entrypoints to immutable interfaces #397
Conversation
) | ||
|
||
type WAFConfig interface { | ||
WithRule(rule *corazawaf.Rule) WAFConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about accepting a variadic set of rules? This will save clone
calls and will allow a more fluent API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably also renaming it to WithRules
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we take it post merge? @anuraaga
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry yeah forgot about this one will follow up
|
||
func (c *wafConfig) WithRequestBodyAccess(config RequestBodyConfig) WAFConfig { | ||
ret := c.clone() | ||
ret.requestBody = config.(*requestBodyConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since RequestBodyConfig
is an interface this casting can of course fail because anyone can implement the interface and attempt to do dynamic stuff. I think for the sake of correctness this interface can have a private method so you make sure you are the only one which can implement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @anuraaga
|
||
func (c *auditLogConfig) LogRelevantOnly() AuditLogConfig { | ||
ret := c.clone() | ||
c.relevantOnly = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c.relevantOnly = true | |
ret.relevantOnly = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @anuraaga
internal/io/file.go
Outdated
// detail, we get the abstraction we need while being able to handle paths as | ||
// the os package otherwise would. | ||
// More context in: https://github.com/golang/go/issues/44279 | ||
type OSFS struct{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest with list the interfaces this implements.
config.go
Outdated
WithDebugLogger(logger corazawaf.DebugLogger) WAFConfig | ||
WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig | ||
|
||
WithFSRoot(fs fs.FS) WAFConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect something like WithRootFS
or WithRootDir
as it is more clear although the parameter is a Fs and not a Dir cc @jptosso
Thanks for all the good comments. I'd like a high-level "keep at it" from @jptosso before proceeding too far as it will be a lot more to do. |
Another big addition in 911e0ad I think this shows where things are going in general. It defines a public API for It means the actions plugin API is rather anemic, but maybe it's still useful enough given it has access to important parts of the transaction. I think the notable addition needed which I didn't do quite yet is a mechanism to propagate state from The main API is One thing I'm particularly happy about is actions not having to check rule engine before interruption. We can focus on making this API easy to use without being coupled to internal details. https://github.com/corazawaf/coraza/pull/397/files#diff-081faffa20610c1510154a66fcff0bbf617e176cf3a420ba5f5f1838ceb72503R124 Next I'll make the operators a public API and then programatic rules, and then scrub through remaining parts. @jptosso Feel free to leave any thoughts on where this is going, though I'll keep on going anyways :-) |
We must keep enough exported fields for metadata, for example, a rule action can update a transaction and a rule (during bootstrap), for that reason, we should have TransactionMetadata and RuleMetadata (which already exist). Maybe actions should do something like |
I'm going to give this another read-through by Monday but think it's mostly ready for a first step in a few. The API can't be fully cleaned up in this one PR but I think it'll be a big step without losing current functionality like custom operators/actions. |
@anuraaga is this ready for review? |
Sorry got distracted by backslashes of all things 😭 Will definitely open for review tomorrow |
@jptosso Sorry for the delay, I think for a first version this is OK, there is still more cleanup to follow up with, in particular removing API added here that we feel is overkill, etc. |
testing/engine_test.go
Outdated
"strings" | ||
"testing" | ||
|
||
"github.com/corazawaf/coraza/v3" | ||
) | ||
|
||
func TestRawRequests(t *testing.T) { | ||
waf := coraza.NewWAF() | ||
waf, _ := coraza.NewWAFWithConfig(coraza.NewWAFConfig()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could keep both: NewWAF
and NewWAFWithConfig
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I was wondering if this could be just NewWAF
. I thought it could be cool to have a default experience with embedded rules but if we would never have that we don't need two functions (and can add NewDefaultWAF
or something in the future if that changes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NewDefaultWAF seems more suitable, because it does implement default settings.
actions/actions.go
Outdated
@@ -64,7 +64,7 @@ func init() { | |||
|
|||
// GetAction returns an unwrapped RuleAction from the actionmap based on the name | |||
// If the action does not exist it returns an error | |||
func GetAction(name string) (coraza.RuleAction, error) { | |||
func GetAction(name string) (corazawaf.RuleAction, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should rename this to Get?
.gitignore
Outdated
@@ -16,6 +16,6 @@ test/crs | |||
vendor/ | |||
coraza-waf | |||
__debug_bin | |||
seclang/crs_test.go | |||
internal/seclang/crs_test.go |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can delete this, it was for internal tests
README.md
Outdated
"context" | ||
"fmt" | ||
"github.com/corazawaf/coraza/v3" | ||
"github.com/corazawaf/coraza/v3/seclang" | ||
"github.com/corazawaf/coraza/v3/internal/seclang" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
External packages cannot import internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes forgot to update readme / example
actions/actions.go
Outdated
@@ -7,20 +7,20 @@ import ( | |||
"fmt" | |||
"strings" | |||
|
|||
"github.com/corazawaf/coraza/v3" | |||
"github.com/corazawaf/coraza/v3/internal/corazawaf" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename internal/corazawaf to waf?
examples/http-server/interceptor.go
Outdated
@@ -3,12 +3,12 @@ package main | |||
import ( | |||
"net/http" | |||
|
|||
"github.com/corazawaf/coraza/v3" | |||
"github.com/corazawaf/coraza/v3/internal/corazawaf" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internal cannot be called by external packages
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also implement it using immutability
actions/allow.go
Outdated
) | ||
|
||
// 0 nothing, 1 phase, 2 request | ||
type allowFn struct { | ||
allow int | ||
} | ||
|
||
func (a *allowFn) Init(r *coraza.Rule, b1 string) error { | ||
func (a *allowFn) Init(r rules.RuleInfo, b1 string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should rename ruleinfo to RuleMetadata? its more consistent with the rule metadata concept
config.go
Outdated
WithRule(rule *corazawaf.Rule) WAFConfig | ||
|
||
// WithDirectives parses the directives from the given string and adds them to the WAF. | ||
WithDirectives(rules string) WAFConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithDirectives(rules string) WAFConfig | |
WithDirectives(directives string) WAFConfig |
config.go
Outdated
WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig | ||
|
||
// WithFSRoot configures the root file system. | ||
WithFSRoot(fs fs.FS) WAFConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithFSRoot(fs fs.FS) WAFConfig | |
WithRootFS(fs fs.FS) WAFConfig |
tx.WAF.Logger.Debug("[%s] Capturing field %d with value %q", tx.ID, index, value) | ||
i := strconv.Itoa(index) | ||
tx.Variables.TX.SetIndex(i, 0, value) | ||
if tx.Capture { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually enforce the tx.Capture validation in the operator, that way we can read only the amount of results we expect. For example, RX will load only the first match if !tx.Capture, otherwise it will read up to 10.
Maybe there is no overhead in having it here though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think if the operator checks it it's just a doublecheck here but that isn't worthwhile overhead, but when the operator doesn't have a fast-path for non-capturing (Capturing() is still exposed to allow this for when it is the case) it makes it safer/easier to use so think this is good.
testing/engine_test.go
Outdated
"strings" | ||
"testing" | ||
|
||
"github.com/corazawaf/coraza/v3" | ||
) | ||
|
||
func TestRawRequests(t *testing.T) { | ||
waf := coraza.NewWAF() | ||
waf, _ := coraza.NewWAFWithConfig(coraza.NewWAFConfig()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NewDefaultWAF seems more suitable, because it does implement default settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR might break some functionalities, but we must keep forward with this immutability pattern. LGTM
|
||
func (*geoLookup) Init(coraza.RuleOperatorOptions) error { return nil } | ||
func (*geoLookup) Init(rules.OperatorOptions) error { return nil } | ||
|
||
// kept for compatibility, it requires a plugin. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is v3 a good opportunity to drop this? cc @jptosso
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's kept for compatibility, not used by crs by default
This is a very early look into exploring #371
While writing it, I noticed another objective, where it could be easier to use the API if
Transaction
is split into the interface for WAF users and those writing custom rules / operators. From my cursory understanding, methods likeAddRequestHeader
,Process*
only make sense in former andCapture
only make sense in latter. In this early iteration, we can see theTransaction
interface for WAF users.Rule
also probably needs to have a separate interface for defining rules programatically and implementing operators, etc.The approach for doing this is to first stuff what we have into
internal/corazawaf
,internal/seclang
and start creating public API from scratch delegating to these. After completing migration, there is probably cleanup that could be done which may result in the removal of delegation, but that could happen after locking in a public API.Any methods suffixed
Next()
are placeholders to replace existing methods in future iterations of this PR.The important files at the moment are (these links load slow! wait a few seconds, don't scroll)
Public API:
config.go
waf.go
Public API Users:
coraza_test.go
http.go
Let me know what you think of this very general direction and whether we should pursue it to completion. It will take time to reflect these patterns across all the APIs, but IMO it would be worth it to improve the ease-of-use of the API by making sure they are built on the actual use cases / user personas rather than having too many large interfaces with sort of jumbling of concerns, and chance of safety issues because of mutability. As we move towards using Coraza more in production, v3 is hopefully the right time to settle on the API that avoids a v4 for as long as possible.
Happy to hear thoughts!