Skip to content
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

feat: allow for easily matching rules using path prefixes #1073

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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 driver/configuration/config_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
PrometheusServeCollapseRequestPaths Key = "serve.prometheus.collapse_request_paths"
AccessRuleRepositories Key = "access_rules.repositories"
AccessRuleMatchingStrategy Key = "access_rules.matching_strategy"
AcccessRulePrefixMatchingEnabled Key = "access_rules.prefix_matching_enabled"
)

// Authorizers
Expand Down
1 change: 1 addition & 0 deletions driver/configuration/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Provider interface {

AccessRuleRepositories() []url.URL
AccessRuleMatchingStrategy() MatchingStrategy
AcccessRulePrefixMatchingEnabled() bool

ProxyServeAddress() string
APIServeAddress() string
Expand Down
5 changes: 5 additions & 0 deletions driver/configuration/provider_koanf.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ func (v *KoanfProvider) AccessRuleMatchingStrategy() MatchingStrategy {
return MatchingStrategy(v.source.String(AccessRuleMatchingStrategy))
}

// AcccessRulePrefixMatching returns if prefix matching should be used.
func (v *KoanfProvider) AcccessRulePrefixMatchingEnabled() bool {
return v.source.Bool(AcccessRulePrefixMatchingEnabled)
}

func (v *KoanfProvider) CORSEnabled(iface string) bool {
_, enabled := v.CORS(iface)
return enabled
Expand Down
2 changes: 2 additions & 0 deletions internal/config/.oathkeeper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ access_rules:
- https://path-to-my-rules/rules.json
# Optional fields describing matching strategy, defaults to "regexp".
matching_strategy: glob
# Optional fields describing if rules should be matched using path prefixes, defaults to false.
prefix_matching_enabled: false

errors:
fallback:
Expand Down
6 changes: 3 additions & 3 deletions rule/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (
)

type (
Protocol int
Protocol string

Matcher interface {
Match(ctx context.Context, method string, u *url.URL, protocol Protocol) (*Rule, error)
}
)

const (
ProtocolHTTP Protocol = iota
ProtocolGRPC
ProtocolHTTP Protocol = "http"
ProtocolGRPC Protocol = "grpc"
)
95 changes: 95 additions & 0 deletions rule/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,63 @@ var testRulesGlob = []Rule{
},
}

var testRulesPrefix = []Rule{
{
ID: "foo1",
Match: &Match{URL: "https://localhost:1234/<foo|bar>", Methods: []string{"POST"}},
Description: "Create users rule",
Authorizer: Handler{Handler: "allow", Config: []byte(`{"type":"any"}`)},
Authenticators: []Handler{{Handler: "anonymous", Config: []byte(`{"name":"anonymous1"}`)}},
Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
Upstream: Upstream{URL: "http://localhost:1235/", StripPath: "/bar", PreserveHost: true},
},
{
ID: "foo2",
Match: &Match{URL: "https://localhost:1234/foo/<baz|bar>", Methods: []string{"POST"}},
Description: "Create users rule",
Authorizer: Handler{Handler: "allow", Config: []byte(`{"type":"any"}`)},
Authenticators: []Handler{{Handler: "anonymous", Config: []byte(`{"name":"anonymous1"}`)}},
Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
Upstream: Upstream{URL: "http://localhost:1235/", StripPath: "/bar", PreserveHost: true},
},
{
ID: "foo3",
Match: &Match{URL: "https://localhost:1234/foo/something/<baz|bar>", Methods: []string{"POST"}},
Description: "Create users rule",
Authorizer: Handler{Handler: "allow", Config: []byte(`{"type":"any"}`)},
Authenticators: []Handler{{Handler: "anonymous", Config: []byte(`{"name":"anonymous1"}`)}},
Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
Upstream: Upstream{URL: "http://localhost:1235/", StripPath: "/bar", PreserveHost: true},
},
{
ID: "foo4",
Match: &Match{URL: "https://localhost:34/<baz|bar>", Methods: []string{"GET"}},
Description: "Get users rule",
Authorizer: Handler{Handler: "deny", Config: []byte(`{"type":"any"}`)},
Authenticators: []Handler{{Handler: "oauth2_introspection", Config: []byte(`{"name":"anonymous1"}`)}},
Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
Upstream: Upstream{URL: "http://localhost:333/", StripPath: "/foo", PreserveHost: false},
},
{
ID: "foo5",
Match: &Match{URL: "https://localhost:343/<baz|bar>", Methods: []string{"GET"}},
Description: "Get users rule",
Authorizer: Handler{Handler: "deny"},
Authenticators: []Handler{{Handler: "oauth2_introspection"}},
Mutators: []Handler{{Handler: "id_token"}},
Upstream: Upstream{URL: "http://localhost:3333/", StripPath: "/foo", PreserveHost: false},
},
{
ID: "grpc1",
Match: &MatchGRPC{Authority: "bar.example.com", FullMethod: "grpc.api/Call"},
Description: "gRPC Rule",
Authorizer: Handler{Handler: "allow", Config: []byte(`{"type":"any"}`)},
Authenticators: []Handler{{Handler: "anonymous", Config: []byte(`{"name":"anonymous1"}`)}},
Mutators: []Handler{{Handler: "id_token", Config: []byte(`{"issuer":"anything"}`)}},
Upstream: Upstream{URL: "http://bar.example.com/", PreserveHost: false},
},
}

func TestMatcher(t *testing.T) {
type m interface {
Matcher
Expand Down Expand Up @@ -192,5 +249,43 @@ func TestMatcher(t *testing.T) {
testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
})
})
t.Run(fmt.Sprintf("prefix matcher=%s", name), func(t *testing.T) {
require.NoError(t, matcher.SetMatchingStrategy(context.Background(), configuration.Regexp))
require.NoError(t, matcher.SetPrefixMatching(context.Background(), true))
require.NoError(t, matcher.Set(context.Background(), []Rule{}))
t.Run("case=empty", func(t *testing.T) {
testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, true, nil)
testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
})

require.NoError(t, matcher.Set(context.Background(), testRulesPrefix))

t.Run("case=created", func(t *testing.T) {
testMatcher(t, matcher, "POST", "https://localhost:1234/", ProtocolHTTP, false, &testRulesPrefix[0])
testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, false, &testRulesPrefix[1])
testMatcher(t, matcher, "POST", "https://localhost:1234/foo/something/very/long", ProtocolHTTP, false, &testRulesPrefix[2])
testMatcher(t, matcher, "POST", "https://localhost:1234/foo/baz/something/very/long", ProtocolHTTP, false, &testRulesPrefix[1])
testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRulesPrefix[3])
testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
testMatcher(t, matcher, "POST", "grpc://bar.example.com/grpc.api/Call", ProtocolGRPC, false, &testRulesPrefix[5])
})

t.Run("case=cache", func(t *testing.T) {
r, err := matcher.Match(context.Background(), "GET", mustParseURL(t, "https://localhost:34/baz"), ProtocolHTTP)
require.NoError(t, err)
_, err = matcher.Get(context.Background(), r.ID)
require.NoError(t, err)
// assert.NotEmpty(t, got.matchingEngine.Checksum())
})

require.NoError(t, matcher.Set(context.Background(), testRulesPrefix[3:]))

t.Run("case=updated", func(t *testing.T) {
testMatcher(t, matcher, "GET", "https://localhost:34/baz", ProtocolHTTP, false, &testRulesPrefix[3])
testMatcher(t, matcher, "POST", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
testMatcher(t, matcher, "DELETE", "https://localhost:1234/foo", ProtocolHTTP, true, nil)
})
})
}
}
2 changes: 2 additions & 0 deletions rule/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ type Repository interface {
Count(context.Context) (int, error)
MatchingStrategy(context.Context) (configuration.MatchingStrategy, error)
SetMatchingStrategy(context.Context, configuration.MatchingStrategy) error
PrefixMatching(context.Context) (bool, error)
SetPrefixMatching(context.Context, bool) error
ReadyChecker(*http.Request) error
}
79 changes: 66 additions & 13 deletions rule/repository_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ type RepositoryMemory struct {
rules []Rule
invalidRules []Rule
matchingStrategy configuration.MatchingStrategy
prefixMatching bool
r repositoryMemoryRegistry
trie *Trie
}

// MatchingStrategy returns current MatchingStrategy.
Expand All @@ -48,17 +50,36 @@ func (m *RepositoryMemory) SetMatchingStrategy(_ context.Context, ms configurati
return nil
}

// PrefixMatching returns current PrefixMatching.
func (m *RepositoryMemory) PrefixMatching(_ context.Context) (bool, error) {
m.RLock()
defer m.RUnlock()
return m.prefixMatching, nil
}

// SetPrefixMatching updates PrefixMatching.
func (m *RepositoryMemory) SetPrefixMatching(_ context.Context, enabled bool) error {
m.Lock()
defer m.Unlock()
m.prefixMatching = enabled
return nil
}

func NewRepositoryMemory(r repositoryMemoryRegistry) *RepositoryMemory {
return &RepositoryMemory{
r: r,
rules: make([]Rule, 0),
trie: NewTrie(),
}
}

// WithRules sets rules without validation. For testing only.
func (m *RepositoryMemory) WithRules(rules []Rule) {
m.Lock()
m.rules = rules
for _, rule := range rules {
m.trie.InsertRule(rule)
}
m.Unlock()
}

Expand Down Expand Up @@ -97,13 +118,24 @@ func (m *RepositoryMemory) Set(ctx context.Context, rules []Rule) error {
m.rules = make([]Rule, 0, len(rules))
m.invalidRules = make([]Rule, 0)

// Reset the trie if we are using prefix matching and the rules have changed.
if m.prefixMatching {
m.trie = NewTrie()
}

for _, check := range rules {
if err := m.r.RuleValidator().Validate(&check); err != nil {
m.r.Logger().WithError(err).WithField("rule_id", check.ID).
Errorf("A Rule uses a malformed configuration and all URLs matching this rule will not work. You should resolve this issue now.")
m.invalidRules = append(m.invalidRules, check)
} else {
m.rules = append(m.rules, check)
if m.prefixMatching {
if err := m.trie.InsertRule(check); err != nil {
m.r.Logger().WithError(err).WithField("rule_id", check.ID).
Errorf("A Prefix Rule could not be loaded into the trie so all requests will be sent to the closest matching prefix. You should resolve this issue now.")
}
}
}
}

Expand All @@ -119,20 +151,41 @@ func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL,
defer m.Unlock()

var rules []*Rule
for k := range m.rules {
r := &m.rules[k]
if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil {
return nil, errors.WithStack(err)
} else if matched {
rules = append(rules, r)

if m.prefixMatching {
if m.trie.root == nil {
return nil, errors.WithStack(errors.New("prefix trie is nil"))
} else {
matchedRules := m.trie.Match(method, u, protocol)
for _, r := range matchedRules {
// if there are multiple rules that match, we will procede to filter them using the matching strategy
if len(matchedRules) > 1 {
if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil {
return nil, errors.WithStack(err)
} else if matched {
rules = append(rules, &r)
}
} else {
rules = append(rules, &r)
}
}
}
}
for k := range m.invalidRules {
r := &m.invalidRules[k]
if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil {
return nil, errors.WithStack(err)
} else if matched {
rules = append(rules, r)
} else {
for k := range m.rules {
r := &m.rules[k]
if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil {
return nil, errors.WithStack(err)
} else if matched {
rules = append(rules, r)
}
}
for k := range m.invalidRules {
r := &m.invalidRules[k]
if matched, err := r.IsMatching(m.matchingStrategy, method, u, protocol); err != nil {
return nil, errors.WithStack(err)
} else if matched {
rules = append(rules, r)
}
}
}

Expand Down
Loading
Loading