Skip to content

Commit 06979c6

Browse files
committed
Support like and not like operators on path regex to simplify path matching
1 parent 1e32b0c commit 06979c6

4 files changed

Lines changed: 304 additions & 258 deletions

File tree

authentication/mtls.go

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ func init() {
2929
}
3030

3131
type mTLSConfig struct {
32-
RawCA []byte `json:"ca"`
33-
CAPath string `json:"caPath"`
34-
PathPatterns []string `json:"pathPatterns"`
32+
RawCA []byte `json:"ca"`
33+
CAPath string `json:"caPath"`
34+
Paths []PathPattern `json:"paths,omitempty"`
3535
CAs []*x509.Certificate
36-
pathMatchers []*regexp.Regexp
36+
pathMatchers []PathMatcher
3737
}
3838

3939
type MTLSAuthenticator struct {
@@ -86,13 +86,27 @@ func newMTLSAuthenticator(c map[string]interface{}, tenant string, registrationR
8686
config.CAs = cas
8787
}
8888

89-
// Compile path patterns
90-
for _, pattern := range config.PathPatterns {
91-
matcher, err := regexp.Compile(pattern)
89+
// Compile path patterns with operators
90+
for _, pathPattern := range config.Paths {
91+
operator := pathPattern.Operator
92+
if operator == "" {
93+
operator = "=~" // default operator
94+
}
95+
96+
// Validate operator
97+
if operator != "=~" && operator != "!~" {
98+
return nil, fmt.Errorf("invalid mTLS path operator %q, must be '=~' or '!~'", operator)
99+
}
100+
101+
matcher, err := regexp.Compile(pathPattern.Pattern)
92102
if err != nil {
93-
return nil, fmt.Errorf("failed to compile mTLS path pattern %q: %v", pattern, err)
103+
return nil, fmt.Errorf("failed to compile mTLS path pattern %q: %v", pathPattern.Pattern, err)
94104
}
95-
config.pathMatchers = append(config.pathMatchers, matcher)
105+
106+
config.pathMatchers = append(config.pathMatchers, PathMatcher{
107+
Operator: operator,
108+
Regex: matcher,
109+
})
96110
}
97111

98112
return MTLSAuthenticator{
@@ -107,16 +121,24 @@ func (a MTLSAuthenticator) Middleware() Middleware {
107121
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108122
// Check if mTLS is required for this path
109123
if len(a.config.pathMatchers) > 0 {
110-
pathMatches := false
124+
shouldEnforceMTLS := false
125+
111126
for _, matcher := range a.config.pathMatchers {
112-
if matcher.MatchString(r.URL.Path) {
113-
pathMatches = true
127+
regexMatches := matcher.Regex.MatchString(r.URL.Path)
128+
129+
if matcher.Operator == "=~" && regexMatches {
130+
// Positive match - enforce mTLS
131+
shouldEnforceMTLS = true
132+
break
133+
} else if matcher.Operator == "!~" && !regexMatches {
134+
// Negative match - enforce mTLS (path does NOT match pattern)
135+
shouldEnforceMTLS = true
114136
break
115137
}
116138
}
117139

118-
// If path doesn't match, skip mTLS enforcement
119-
if !pathMatches {
140+
// If no patterns matched requirements, skip mTLS enforcement
141+
if !shouldEnforceMTLS {
120142
next.ServeHTTP(w, r)
121143
return
122144
}

authentication/mtls_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http/httptest"
88
"os"
99
"path/filepath"
10+
"regexp"
1011
"testing"
1112

1213
"github.com/go-kit/log"
@@ -466,3 +467,137 @@ func TestMTLSAuthenticator_CAConfiguration(t *testing.T) {
466467
})
467468
}
468469

470+
func TestMTLSPathPatternsWithOperators(t *testing.T) {
471+
tests := []struct {
472+
name string
473+
paths []PathPattern
474+
requestPath string
475+
expectSkip bool
476+
expectError bool
477+
description string
478+
}{
479+
{
480+
name: "positive_match_operator",
481+
paths: []PathPattern{{Operator: "=~", Pattern: "/api/.*/receive"}},
482+
requestPath: "/api/metrics/v1/receive",
483+
expectSkip: false,
484+
description: "Positive match with =~ operator should enforce mTLS",
485+
},
486+
{
487+
name: "positive_no_match_operator",
488+
paths: []PathPattern{{Operator: "=~", Pattern: "/api/.*/receive"}},
489+
requestPath: "/api/metrics/v1/query",
490+
expectSkip: true,
491+
description: "No match with =~ operator should skip mTLS",
492+
},
493+
{
494+
name: "negative_match_operator",
495+
paths: []PathPattern{{Operator: "!~", Pattern: "^/api/(logs|metrics)/v1/auth-tenant/.*(query|labels|series)"}},
496+
requestPath: "/api/metrics/v1/auth-tenant/api/v1/receive",
497+
expectSkip: false,
498+
description: "Path not matching negative pattern should enforce mTLS",
499+
},
500+
{
501+
name: "negative_no_match_operator",
502+
paths: []PathPattern{{Operator: "!~", Pattern: "^/api/(logs|metrics)/v1/auth-tenant/.*(query|labels|series)"}},
503+
requestPath: "/api/metrics/v1/auth-tenant/api/v1/query",
504+
expectSkip: true,
505+
description: "Path matching negative pattern should skip mTLS",
506+
},
507+
{
508+
name: "default_operator",
509+
paths: []PathPattern{{Pattern: "/api/.*/receive"}}, // no operator specified
510+
requestPath: "/api/metrics/v1/receive",
511+
expectSkip: false,
512+
description: "Default operator should be =~",
513+
},
514+
{
515+
name: "multiple_patterns_one_match",
516+
paths: []PathPattern{
517+
{Operator: "=~", Pattern: "/api/.*/receive"},
518+
{Operator: "=~", Pattern: "/api/.*/push"},
519+
},
520+
requestPath: "/api/logs/v1/push",
521+
expectSkip: false,
522+
description: "One matching pattern should enforce mTLS",
523+
},
524+
{
525+
name: "multiple_patterns_none_match",
526+
paths: []PathPattern{
527+
{Operator: "=~", Pattern: "/api/.*/receive"},
528+
{Operator: "=~", Pattern: "/api/.*/push"},
529+
},
530+
requestPath: "/api/metrics/v1/query",
531+
expectSkip: true,
532+
description: "No matching patterns should skip mTLS",
533+
},
534+
{
535+
name: "invalid_operator",
536+
paths: []PathPattern{{Operator: "invalid", Pattern: "/api/.*/receive"}},
537+
expectError: true,
538+
description: "Invalid operator should cause error",
539+
},
540+
}
541+
542+
for _, tt := range tests {
543+
t.Run(tt.name, func(t *testing.T) {
544+
// Test compilation (similar to newMTLSAuthenticator)
545+
var pathMatchers []PathMatcher
546+
547+
for _, pathPattern := range tt.paths {
548+
operator := pathPattern.Operator
549+
if operator == "" {
550+
operator = "=~" // default operator
551+
}
552+
553+
// Validate operator
554+
if operator != "=~" && operator != "!~" {
555+
if tt.expectError {
556+
return // Expected error
557+
}
558+
t.Errorf("Invalid operator %q should have caused error", operator)
559+
return
560+
}
561+
562+
matcher, err := regexp.Compile(pathPattern.Pattern)
563+
if err != nil {
564+
if tt.expectError {
565+
return // Expected error
566+
}
567+
t.Fatalf("Failed to compile pattern: %v", err)
568+
}
569+
570+
pathMatchers = append(pathMatchers, PathMatcher{
571+
Operator: operator,
572+
Regex: matcher,
573+
})
574+
}
575+
576+
if tt.expectError {
577+
t.Error("Expected error but none occurred")
578+
return
579+
}
580+
581+
// Test the matching logic (from middleware)
582+
shouldEnforceMTLS := false
583+
584+
for _, matcher := range pathMatchers {
585+
regexMatches := matcher.Regex.MatchString(tt.requestPath)
586+
587+
if matcher.Operator == "=~" && regexMatches {
588+
shouldEnforceMTLS = true
589+
break
590+
} else if matcher.Operator == "!~" && !regexMatches {
591+
shouldEnforceMTLS = true
592+
break
593+
}
594+
}
595+
596+
shouldSkip := !shouldEnforceMTLS
597+
598+
if shouldSkip != tt.expectSkip {
599+
t.Errorf("Expected skip=%v, got skip=%v for path %s", tt.expectSkip, shouldSkip, tt.requestPath)
600+
}
601+
})
602+
}
603+
}

authentication/oidc.go

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,34 @@ import (
3636
// OIDCAuthenticatorType represents the oidc authentication provider type.
3737
const OIDCAuthenticatorType = "oidc"
3838

39+
// PathPattern represents a path pattern with an operator for matching.
40+
type PathPattern struct {
41+
Operator string `json:"operator,omitempty"` // "=~" (default) or "!~"
42+
Pattern string `json:"pattern"` // regex pattern
43+
}
44+
45+
// PathMatcher represents a compiled path pattern with operator.
46+
type PathMatcher struct {
47+
Operator string
48+
Regex *regexp.Regexp
49+
}
50+
3951
func init() {
4052
onboardNewProvider(OIDCAuthenticatorType, newOIDCAuthenticator)
4153
}
4254

4355
// oidcConfig represents the oidc authenticator config.
4456
type oidcConfig struct {
45-
ClientID string `json:"clientID"`
46-
ClientSecret string `json:"clientSecret"`
47-
GroupClaim string `json:"groupClaim"`
48-
IssuerRawCA []byte `json:"issuerCA"`
49-
IssuerCAPath string `json:"issuerCAPath"`
57+
ClientID string `json:"clientID"`
58+
ClientSecret string `json:"clientSecret"`
59+
GroupClaim string `json:"groupClaim"`
60+
IssuerRawCA []byte `json:"issuerCA"`
61+
IssuerCAPath string `json:"issuerCAPath"`
5062
issuerCA *x509.Certificate
51-
IssuerURL string `json:"issuerURL"`
52-
RedirectURL string `json:"redirectURL"`
53-
UsernameClaim string `json:"usernameClaim"`
54-
PathPatterns []string `json:"pathPatterns"`
63+
IssuerURL string `json:"issuerURL"`
64+
RedirectURL string `json:"redirectURL"`
65+
UsernameClaim string `json:"usernameClaim"`
66+
Paths []PathPattern `json:"paths,omitempty"`
5567
}
5668

5769
type oidcAuthenticator struct {
@@ -65,7 +77,7 @@ type oidcAuthenticator struct {
6577
redirectURL string
6678
oauth2Config oauth2.Config
6779
handler http.Handler
68-
pathMatchers []*regexp.Regexp
80+
pathMatchers []PathMatcher
6981
}
7082

7183
func newOIDCAuthenticator(c map[string]interface{}, tenant string,
@@ -160,14 +172,28 @@ func newOIDCAuthenticator(c map[string]interface{}, tenant string,
160172
pathMatchers = append(pathMatchers, matcher)
161173
}
162174

163-
// Compile path patterns
164-
var pathMatchers []*regexp.Regexp
165-
for _, pattern := range config.PathPatterns {
166-
matcher, err := regexp.Compile(pattern)
175+
// Compile path patterns with operators
176+
var pathMatchers []PathMatcher
177+
for _, pathPattern := range config.Paths {
178+
operator := pathPattern.Operator
179+
if operator == "" {
180+
operator = "=~" // default operator
181+
}
182+
183+
// Validate operator
184+
if operator != "=~" && operator != "!~" {
185+
return nil, fmt.Errorf("invalid OIDC path operator %q, must be '=~' or '!~'", operator)
186+
}
187+
188+
matcher, err := regexp.Compile(pathPattern.Pattern)
167189
if err != nil {
168-
return nil, fmt.Errorf("failed to compile OIDC path pattern %q: %v", pattern, err)
190+
return nil, fmt.Errorf("failed to compile OIDC path pattern %q: %v", pathPattern.Pattern, err)
169191
}
170-
pathMatchers = append(pathMatchers, matcher)
192+
193+
pathMatchers = append(pathMatchers, PathMatcher{
194+
Operator: operator,
195+
Regex: matcher,
196+
})
171197
}
172198

173199
oidcProvider := &oidcAuthenticator{
@@ -302,16 +328,24 @@ func (a oidcAuthenticator) Middleware() Middleware {
302328
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
303329
// Check if OIDC is required for this path
304330
if len(a.pathMatchers) > 0 {
305-
pathMatches := false
331+
shouldEnforceOIDC := false
332+
306333
for _, matcher := range a.pathMatchers {
307-
if matcher.MatchString(r.URL.Path) {
308-
pathMatches = true
334+
regexMatches := matcher.Regex.MatchString(r.URL.Path)
335+
336+
if matcher.Operator == "=~" && regexMatches {
337+
// Positive match - enforce OIDC
338+
shouldEnforceOIDC = true
339+
break
340+
} else if matcher.Operator == "!~" && !regexMatches {
341+
// Negative match - enforce OIDC (path does NOT match pattern)
342+
shouldEnforceOIDC = true
309343
break
310344
}
311345
}
312346

313-
// If path doesn't match, skip OIDC enforcement
314-
if !pathMatches {
347+
// If no patterns matched requirements, skip OIDC enforcement
348+
if !shouldEnforceOIDC {
315349
next.ServeHTTP(w, r)
316350
return
317351
}

0 commit comments

Comments
 (0)