Skip to content

Commit 6b7361d

Browse files
authored
config: openvpn.bypass.common-names accepts regular expressions now (#602)
1 parent 0e227c2 commit 6b7361d

File tree

13 files changed

+215
-53
lines changed

13 files changed

+215
-53
lines changed

docs/Configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Usage of openvpn-auth-oauth2:
117117
--openvpn.auth-token-user
118118
Override the username of a session with the username from the token by using auth-token-user, if the client username is empty (env: CONFIG_OPENVPN_AUTH__TOKEN__USER) (default true)
119119
--openvpn.bypass.common-names value
120-
bypass oauth authentication for CNs. Comma separated list. (env: CONFIG_OPENVPN_BYPASS_COMMON__NAMES)
120+
Skip OAuth authentication for client certificate common names (CNs) matching any of the given regular expressions. Multiple expressions can be provided as a comma-separated list. Regular expressions are automatically anchored (^…$) by default, so "client" matches only "client". To allow partial matches, specify explicitly (e.g. "client.*"). (env: CONFIG_OPENVPN_BYPASS_COMMON__NAMES)
121121
--openvpn.client-config.enabled
122122
If true, openvpn-auth-oauth2 will read the CCD directory for additional configuration. This function mimic the client-config-dir directive in OpenVPN. (env: CONFIG_OPENVPN_CLIENT__CONFIG_ENABLED)
123123
--openvpn.client-config.path value

internal/config/config_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"net/url"
99
"os"
10+
"regexp"
1011
"slices"
1112
"testing"
1213
"time"
@@ -19,6 +20,7 @@ import (
1920
"golang.org/x/oauth2"
2021
)
2122

23+
//goland:noinspection RegExpUnnecessaryNonCapturingGroup
2224
func TestConfig(t *testing.T) {
2325
t.Parallel()
2426

@@ -177,7 +179,7 @@ http:
177179
OmitHost: false,
178180
}},
179181
Bypass: config.OpenVPNBypass{
180-
CommonNames: []string{"test", "test2"},
182+
CommonNames: types.RegexpSlice{regexp.MustCompile(`^(?:test)$`), regexp.MustCompile(`^(?:test2)$`)},
181183
},
182184
ClientConfig: config.OpenVPNConfig{
183185
Enabled: true,
@@ -322,7 +324,8 @@ func TestConfigFlagSet(t *testing.T) {
322324
[]string{"--openvpn.bypass.common-names=a,b"},
323325
func() config.Config {
324326
conf := config.Defaults
325-
conf.OpenVPN.Bypass.CommonNames = []string{"a", "b"}
327+
//goland:noinspection RegExpUnnecessaryNonCapturingGroup
328+
conf.OpenVPN.Bypass.CommonNames = types.RegexpSlice{regexp.MustCompile("^(?:a)$"), regexp.MustCompile("^(?:b)$")}
326329

327330
return conf
328331
}(),

internal/config/defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var Defaults = Config{
5858
},
5959
OverrideUsername: false,
6060
Bypass: OpenVPNBypass{
61-
CommonNames: make([]string, 0),
61+
CommonNames: types.RegexpSlice{},
6262
},
6363
Passthrough: OpenVPNPassthrough{
6464
Enabled: false,

internal/config/flags.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ func (c *Config) flagSetOpenVPN(flagSet *flag.FlagSet) {
138138
&c.OpenVPN.Bypass.CommonNames,
139139
"openvpn.bypass.common-names",
140140
lookupEnvOrDefault("openvpn.bypass.common-names", c.OpenVPN.Bypass.CommonNames),
141-
"bypass oauth authentication for CNs. Comma separated list.",
141+
"Skip OAuth authentication for client certificate common names (CNs) matching any of the given regular expressions. "+
142+
"Multiple expressions can be provided as a comma-separated list. "+
143+
"Regular expressions are automatically anchored (^…$) by default, so \"client\" matches only \"client\". "+
144+
"To allow partial matches, specify explicitly (e.g. \"client.*\").",
142145
)
143146
flagSet.BoolVar(
144147
&c.OpenVPN.ClientConfig.Enabled,

internal/config/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type OpenVPN struct {
6464
}
6565

6666
type OpenVPNBypass struct {
67-
CommonNames types.StringSlice `json:"common-names" yaml:"common-names"`
67+
CommonNames types.RegexpSlice `json:"common-names" yaml:"common-names"`
6868
}
6969
type OpenVPNConfig struct {
7070
Path types.FS `json:"path" yaml:"path"`

internal/config/types/slice.go

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package types
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
7+
"regexp"
68
"strings"
9+
10+
"go.yaml.in/yaml/v3"
711
)
812

913
type StringSlice []string
@@ -35,12 +39,108 @@ func (s *StringSlice) UnmarshalText(text []byte) error {
3539
//
3640
//goland:noinspection GoMixedReceiverTypes
3741
func (s *StringSlice) UnmarshalJSON(jsonBytes []byte) error {
38-
var slice []string
42+
var stringList []string
43+
44+
err := json.NewDecoder(bytes.NewReader(jsonBytes)).Decode(&stringList)
45+
if err != nil {
46+
//nolint:wrapcheck
47+
return err
48+
}
49+
50+
*s = stringList
51+
52+
return nil
53+
}
54+
55+
// UnmarshalYAML implements the [yaml.Unmarshaler] interface.
56+
//
57+
//goland:noinspection GoMixedReceiverTypes
58+
func (s *StringSlice) UnmarshalYAML(data *yaml.Node) error {
59+
var stringList []string
60+
61+
err := data.Decode(&stringList)
62+
if err != nil {
63+
//nolint:wrapcheck
64+
return err
65+
}
66+
67+
*s = stringList
68+
69+
return nil
70+
}
71+
72+
type RegexpSlice []*regexp.Regexp
73+
74+
// String returns the string representation of the [RegexpSlice].
75+
//
76+
//goland:noinspection GoMixedReceiverTypes
77+
func (s RegexpSlice) String() string {
78+
stringList := make([]string, 0, len(s))
79+
for _, r := range s {
80+
stringList = append(stringList, r.String())
81+
}
3982

40-
err := json.NewDecoder(bytes.NewReader(jsonBytes)).Decode(&slice)
83+
return strings.Join(stringList, ",")
84+
}
4185

42-
*s = slice
86+
// MarshalText implements [encoding.TextMarshaler] interface.
87+
//
88+
//goland:noinspection GoMixedReceiverTypes
89+
func (s RegexpSlice) MarshalText() ([]byte, error) {
90+
return []byte(s.String()), nil
91+
}
4392

44-
//nolint:wrapcheck
45-
return err
93+
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
94+
//
95+
//goland:noinspection GoMixedReceiverTypes
96+
func (s *RegexpSlice) UnmarshalText(text []byte) error {
97+
return s.fromSlice(strings.Split(string(text), ","))
98+
}
99+
100+
// UnmarshalJSON implements the [json.Unmarshaler] interface.
101+
//
102+
//goland:noinspection GoMixedReceiverTypes
103+
func (s *RegexpSlice) UnmarshalJSON(jsonBytes []byte) error {
104+
var stringList []string
105+
106+
err := json.NewDecoder(bytes.NewReader(jsonBytes)).Decode(&stringList)
107+
if err != nil {
108+
//nolint:wrapcheck
109+
return err
110+
}
111+
112+
return s.fromSlice(stringList)
113+
}
114+
115+
// UnmarshalYAML implements the [yaml.Unmarshaler] interface.
116+
//
117+
//goland:noinspection GoMixedReceiverTypes
118+
func (s *RegexpSlice) UnmarshalYAML(data *yaml.Node) error {
119+
var stringList []string
120+
121+
err := data.Decode(&stringList)
122+
if err != nil {
123+
//nolint:wrapcheck
124+
return err
125+
}
126+
127+
return s.fromSlice(stringList)
128+
}
129+
130+
//goland:noinspection GoMixedReceiverTypes
131+
func (s *RegexpSlice) fromSlice(stringList []string) error {
132+
regexList := make(RegexpSlice, 0, len(stringList))
133+
for _, str := range stringList {
134+
regexPattern, err := regexp.Compile(fmt.Sprintf("^(?:%s)$", str))
135+
if err != nil {
136+
//nolint:wrapcheck
137+
return err
138+
}
139+
140+
regexList = append(regexList, regexPattern)
141+
}
142+
143+
*s = regexList
144+
145+
return nil
46146
}

internal/config/types/slice_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package types_test
22

33
import (
44
"encoding/json"
5+
"regexp"
56
"strings"
67
"testing"
78

@@ -50,3 +51,48 @@ func TestSliceUnmarshalYAML(t *testing.T) {
5051

5152
assert.Equal(t, types.StringSlice{"a", "b", "c", "d"}, slice)
5253
}
54+
55+
func TestRegexpSliceUnmarshalText(t *testing.T) {
56+
t.Parallel()
57+
58+
slice := types.RegexpSlice{}
59+
60+
require.NoError(t, slice.UnmarshalText([]byte("a,b,c,d")))
61+
62+
//goland:noinspection RegExpUnnecessaryNonCapturingGroup
63+
assert.Equal(t, types.RegexpSlice{regexp.MustCompile("^(?:a)$"), regexp.MustCompile("^(?:b)$"), regexp.MustCompile("^(?:c)$"), regexp.MustCompile("^(?:d)$")}, slice)
64+
}
65+
66+
func TestRegexpSliceUnmarshalTextError(t *testing.T) {
67+
t.Parallel()
68+
69+
slice := types.RegexpSlice{}
70+
71+
require.EqualError(t, slice.UnmarshalText([]byte("^(a,b,c,d")), "error parsing regexp: missing closing ): `^(?:^(a)$`")
72+
}
73+
74+
func TestRegexpSliceMarshalText(t *testing.T) {
75+
t.Parallel()
76+
77+
slice, err := types.RegexpSlice{regexp.MustCompile("a"), regexp.MustCompile("b"), regexp.MustCompile("c"), regexp.MustCompile("d")}.MarshalText()
78+
79+
require.NoError(t, err)
80+
81+
assert.Equal(t, []byte("a,b,c,d"), slice)
82+
}
83+
84+
func TestRegexpSliceUnmarshalJSON(t *testing.T) {
85+
t.Parallel()
86+
87+
slice := types.RegexpSlice{}
88+
89+
require.EqualError(t, json.NewDecoder(strings.NewReader(`["^(a","b","c","d"]`)).Decode(&slice), "error parsing regexp: missing closing ): `^(?:^(a)$`")
90+
}
91+
92+
func TestRegexpSliceUnmarshalYAML(t *testing.T) {
93+
t.Parallel()
94+
95+
slice := types.RegexpSlice{}
96+
97+
require.EqualError(t, yaml.NewDecoder(strings.NewReader("- \"^(a\"\n- b\n- c\n- d\n")).Decode(&slice), "error parsing regexp: missing closing ): `^(?:^(a)$`")
98+
}

internal/oauth2/handler_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestHandler(t *testing.T) {
4444
conf.OAuth2.Validate.Roles = make([]string, 0)
4545
conf.OAuth2.Validate.Issuer = true
4646
conf.OAuth2.Validate.IPAddr = false
47-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
47+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
4848
conf.OpenVPN.AuthTokenUser = true
4949

5050
return conf
@@ -68,7 +68,7 @@ func TestHandler(t *testing.T) {
6868
conf.OAuth2.Validate.Roles = make([]string, 0)
6969
conf.OAuth2.Validate.Issuer = true
7070
conf.OAuth2.Validate.IPAddr = false
71-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
71+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
7272
conf.OpenVPN.AuthTokenUser = true
7373

7474
return conf
@@ -95,7 +95,7 @@ func TestHandler(t *testing.T) {
9595
conf.OAuth2.Validate.IPAddr = false
9696
conf.OAuth2.Nonce = true
9797
conf.OAuth2.PKCE = true
98-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
98+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
9999
conf.OpenVPN.AuthTokenUser = true
100100

101101
return conf
@@ -123,7 +123,7 @@ func TestHandler(t *testing.T) {
123123
conf.OAuth2.Validate.Roles = make([]string, 0)
124124
conf.OAuth2.Validate.Issuer = true
125125
conf.OAuth2.Validate.IPAddr = false
126-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
126+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
127127
conf.OpenVPN.AuthTokenUser = true
128128

129129
return conf
@@ -148,7 +148,7 @@ func TestHandler(t *testing.T) {
148148
conf.OAuth2.Validate.Issuer = true
149149
conf.OAuth2.Validate.IPAddr = false
150150
conf.OAuth2.UserInfo = true
151-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
151+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
152152
conf.OpenVPN.AuthTokenUser = true
153153

154154
return conf
@@ -173,7 +173,7 @@ func TestHandler(t *testing.T) {
173173
conf.OAuth2.Validate.Issuer = true
174174
conf.OAuth2.Validate.IPAddr = false
175175
conf.OAuth2.UserInfo = true
176-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
176+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
177177
conf.OpenVPN.AuthTokenUser = true
178178

179179
return conf
@@ -198,7 +198,7 @@ func TestHandler(t *testing.T) {
198198
conf.OAuth2.Validate.Issuer = true
199199
conf.OAuth2.Validate.IPAddr = false
200200
conf.OAuth2.UserInfo = true
201-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
201+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
202202
conf.OpenVPN.AuthTokenUser = true
203203

204204
return conf
@@ -222,7 +222,7 @@ func TestHandler(t *testing.T) {
222222
conf.OAuth2.Validate.Roles = make([]string, 0)
223223
conf.OAuth2.Validate.Issuer = true
224224
conf.OAuth2.Validate.IPAddr = false
225-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
225+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
226226
conf.OpenVPN.AuthTokenUser = true
227227

228228
return conf
@@ -247,7 +247,7 @@ func TestHandler(t *testing.T) {
247247
conf.OAuth2.Validate.Roles = make([]string, 0)
248248
conf.OAuth2.Validate.Issuer = true
249249
conf.OAuth2.Validate.IPAddr = false
250-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
250+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
251251
conf.OpenVPN.AuthTokenUser = true
252252

253253
return conf
@@ -272,7 +272,7 @@ func TestHandler(t *testing.T) {
272272
conf.OAuth2.Validate.Roles = make([]string, 0)
273273
conf.OAuth2.Validate.Issuer = true
274274
conf.OAuth2.Validate.IPAddr = false
275-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
275+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
276276
conf.OpenVPN.AuthTokenUser = true
277277

278278
return conf
@@ -297,7 +297,7 @@ func TestHandler(t *testing.T) {
297297
conf.OAuth2.Validate.Roles = make([]string, 0)
298298
conf.OAuth2.Validate.Issuer = true
299299
conf.OAuth2.Validate.IPAddr = false
300-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
300+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
301301
conf.OpenVPN.AuthTokenUser = true
302302

303303
return conf
@@ -322,7 +322,7 @@ func TestHandler(t *testing.T) {
322322
conf.OAuth2.Validate.Roles = make([]string, 0)
323323
conf.OAuth2.Validate.Issuer = true
324324
conf.OAuth2.Validate.IPAddr = false
325-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
325+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
326326
conf.OpenVPN.AuthTokenUser = true
327327
conf.OpenVPN.ClientConfig.Enabled = true
328328
conf.OpenVPN.ClientConfig.Path = types.FS{
@@ -355,7 +355,7 @@ func TestHandler(t *testing.T) {
355355
conf.OAuth2.Validate.Roles = make([]string, 0)
356356
conf.OAuth2.Validate.Issuer = true
357357
conf.OAuth2.Validate.IPAddr = false
358-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
358+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
359359
conf.OpenVPN.AuthTokenUser = true
360360
conf.OpenVPN.ClientConfig.Enabled = true
361361
conf.OpenVPN.ClientConfig.Path = types.FS{
@@ -384,7 +384,7 @@ func TestHandler(t *testing.T) {
384384
conf.OAuth2.Validate.Roles = make([]string, 0)
385385
conf.OAuth2.Validate.Issuer = true
386386
conf.OAuth2.Validate.IPAddr = false
387-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
387+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
388388
conf.OpenVPN.AuthTokenUser = true
389389

390390
return conf
@@ -409,7 +409,7 @@ func TestHandler(t *testing.T) {
409409
conf.OAuth2.Validate.Roles = make([]string, 0)
410410
conf.OAuth2.Validate.Issuer = true
411411
conf.OAuth2.Validate.IPAddr = false
412-
conf.OpenVPN.Bypass.CommonNames = make([]string, 0)
412+
conf.OpenVPN.Bypass.CommonNames = make(types.RegexpSlice, 0)
413413
conf.OpenVPN.AuthTokenUser = true
414414

415415
return conf

0 commit comments

Comments
 (0)