Skip to content

Commit 951b53f

Browse files
feat(controller): add impersonation group filter options (#375)
* feat(controller): add impersonation group filter options Signed-off-by: Oliver Bähler <[email protected]> * test(controller): add impersonation group filter options Signed-off-by: Oliver Bähler <[email protected]> --------- Signed-off-by: Oliver Bähler <[email protected]>
1 parent 958472a commit 951b53f

File tree

8 files changed

+175
-54
lines changed

8 files changed

+175
-54
lines changed

internal/options/kube.go

+32-11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net"
1010
"net/http"
1111
"net/url"
12+
"regexp"
1213
"time"
1314

1415
"github.com/pkg/errors"
@@ -19,25 +20,37 @@ import (
1920
)
2021

2122
type kubeOpts struct {
22-
authTypes []request.AuthType
23-
url url.URL
24-
ignoredGroups []string
25-
claimName string
26-
config *rest.Config
23+
authTypes []request.AuthType
24+
url url.URL
25+
ignoredGroups []string
26+
ignoredImpersonationGroups []string
27+
claimName string
28+
impersonationGroupsRegexp *regexp.Regexp
29+
config *rest.Config
2730
}
2831

29-
func NewKube(authTypes []request.AuthType, ignoredGroups []string, claimName string, config *rest.Config) (ListenerOpts, error) {
32+
func NewKube(authTypes []request.AuthType, ignoredGroups []string, claimName string, config *rest.Config, ignoredImpersonationGroups []string, impersonationGroupsString string) (ListenerOpts, error) {
3033
u, err := url.Parse(config.Host)
3134
if err != nil {
3235
return nil, fmt.Errorf("cannot create Kubernetes Options due to failed URL parsing: %w", err)
3336
}
3437

38+
var impersonationGroupsRegexp *regexp.Regexp
39+
if impersonationGroupsString != "" {
40+
impersonationGroupsRegexp, err = regexp.Compile(impersonationGroupsString)
41+
if err != nil {
42+
return nil, fmt.Errorf("cannot create Kubernetes Options due to failed regexp compilation: %w", err)
43+
}
44+
}
45+
3546
return &kubeOpts{
36-
authTypes: authTypes,
37-
url: *u,
38-
ignoredGroups: ignoredGroups,
39-
claimName: claimName,
40-
config: config,
47+
authTypes: authTypes,
48+
url: *u,
49+
ignoredGroups: ignoredGroups,
50+
ignoredImpersonationGroups: ignoredImpersonationGroups,
51+
impersonationGroupsRegexp: impersonationGroupsRegexp,
52+
claimName: claimName,
53+
config: config,
4154
}, nil
4255
}
4356

@@ -57,6 +70,14 @@ func (k kubeOpts) IgnoredGroupNames() []string {
5770
return k.ignoredGroups
5871
}
5972

73+
func (k kubeOpts) IgnoredImpersonationsGroups() []string {
74+
return k.ignoredImpersonationGroups
75+
}
76+
77+
func (k kubeOpts) ImpersonationGroupsRegexp() *regexp.Regexp {
78+
return k.impersonationGroupsRegexp
79+
}
80+
6081
func (k kubeOpts) PreferredUsernameClaim() string {
6182
return k.claimName
6283
}

internal/options/listener.go

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package options
66
import (
77
"net/http"
88
"net/url"
9+
"regexp"
910

1011
"github.com/projectcapsule/capsule-proxy/internal/request"
1112
)
@@ -14,6 +15,8 @@ type ListenerOpts interface {
1415
AuthTypes() []request.AuthType
1516
KubernetesControlPlaneURL() *url.URL
1617
IgnoredGroupNames() []string
18+
IgnoredImpersonationsGroups() []string
19+
ImpersonationGroupsRegexp() *regexp.Regexp
1720
PreferredUsernameClaim() string
1821
ReverseProxyTransport() (*http.Transport, error)
1922
BearerToken() string

internal/request/http.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package request
66
import (
77
"fmt"
88
h "net/http"
9+
"regexp"
910
"strings"
1011

1112
authenticationv1 "k8s.io/api/authentication/v1"
@@ -16,13 +17,15 @@ import (
1617

1718
type http struct {
1819
*h.Request
19-
authTypes []AuthType
20-
usernameClaimField string
21-
client client.Writer
20+
authTypes []AuthType
21+
usernameClaimField string
22+
ignoredImpersonationGroups []string
23+
impersonationGroupsRegexp *regexp.Regexp
24+
client client.Writer
2225
}
2326

24-
func NewHTTP(request *h.Request, authTypes []AuthType, usernameClaimField string, client client.Writer) Request {
25-
return &http{Request: request, authTypes: authTypes, usernameClaimField: usernameClaimField, client: client}
27+
func NewHTTP(request *h.Request, authTypes []AuthType, usernameClaimField string, client client.Writer, ignoredImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp) Request {
28+
return &http{Request: request, authTypes: authTypes, usernameClaimField: usernameClaimField, client: client, ignoredImpersonationGroups: ignoredImpersonationGroups, impersonationGroupsRegexp: impersonationGroupsRegexp}
2629
}
2730

2831
func (h http) GetHTTPRequest() *h.Request {
@@ -45,7 +48,7 @@ func (h http) GetUserAndGroups() (username string, groups []string, err error) {
4548

4649
// In case the requester is asking for impersonation, we have to be sure that's allowed by creating a
4750
// SubjectAccessReview with the requested data, before proceeding.
48-
if impersonateGroups := GetImpersonatingGroups(h.Request); len(impersonateGroups) > 0 {
51+
if impersonateGroups := GetImpersonatingGroups(h.Request, h.ignoredImpersonationGroups, h.impersonationGroupsRegexp); len(impersonateGroups) > 0 {
4952
for _, impersonateGroup := range impersonateGroups {
5053
ac := &authorizationv1.SubjectAccessReview{
5154
Spec: authorizationv1.SubjectAccessReviewSpec{

internal/request/http_test.go

+47-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net/http"
1010
"reflect"
11+
"regexp"
1112
"sort"
1213
"testing"
1314

@@ -45,10 +46,12 @@ func Test_http_GetUserAndGroups(t *testing.T) {
4546
t.Parallel()
4647

4748
type fields struct {
48-
Request *http.Request
49-
authTypes []request.AuthType
50-
usernameClaimField string
51-
client client.Writer
49+
Request *http.Request
50+
authTypes []request.AuthType
51+
ignoreGroups []string
52+
ignoreImpersonationRegexp *regexp.Regexp
53+
usernameClaimField string
54+
client client.Writer
5255
}
5356

5457
tests := []struct {
@@ -95,7 +98,7 @@ func Test_http_GetUserAndGroups(t *testing.T) {
9598
}),
9699
},
97100
wantUsername: "ImpersonatedUser",
98-
wantGroups: []string{"group", "ImpersonatedGroup"},
101+
wantGroups: []string{"ImpersonatedGroup"},
99102
wantErr: false,
100103
},
101104
{
@@ -117,6 +120,44 @@ func Test_http_GetUserAndGroups(t *testing.T) {
117120
},
118121
wantUsername: "",
119122
wantGroups: nil,
123+
wantErr: true,
124+
},
125+
{
126+
name: "Certificate-Impersonation",
127+
fields: fields{
128+
Request: &http.Request{
129+
Header: map[string][]string{
130+
authenticationv1.ImpersonateGroupHeader: {"ImpersonatedGroup", "Regex:Group1", "Regex:Group2", "Regex:DropGroup1", "Regex:DropGroup2"},
131+
authenticationv1.ImpersonateUserHeader: {"ImpersonatedUser"},
132+
},
133+
TLS: &tls.ConnectionState{
134+
PeerCertificates: []*x509.Certificate{
135+
{
136+
Subject: pkix.Name{
137+
CommonName: "nobody",
138+
Organization: []string{
139+
"group",
140+
},
141+
},
142+
},
143+
},
144+
},
145+
},
146+
ignoreImpersonationRegexp: regexp.MustCompile("Regex:.*"),
147+
ignoreGroups: []string{"Regex:DropGroup1", "Regex:DropGroup2"},
148+
authTypes: []request.AuthType{
149+
request.BearerToken,
150+
request.TLSCertificate,
151+
},
152+
client: testClient(func(ctx context.Context, obj client.Object) error {
153+
ac := obj.(*authorizationv1.SubjectAccessReview)
154+
ac.Status.Allowed = true
155+
156+
return nil
157+
}),
158+
},
159+
wantUsername: "ImpersonatedUser",
160+
wantGroups: []string{"Regex:Group1", "Regex:Group2"},
120161
wantErr: false,
121162
},
122163
}
@@ -126,7 +167,7 @@ func Test_http_GetUserAndGroups(t *testing.T) {
126167
t.Run(tc.name, func(t *testing.T) {
127168
t.Parallel()
128169

129-
req := request.NewHTTP(tc.fields.Request, tc.fields.authTypes, tc.fields.usernameClaimField, tc.fields.client)
170+
req := request.NewHTTP(tc.fields.Request, tc.fields.authTypes, tc.fields.usernameClaimField, tc.fields.client, tc.fields.ignoreGroups, tc.fields.ignoreImpersonationRegexp)
130171
gotUsername, gotGroups, err := req.GetUserAndGroups()
131172
if (err != nil) != tc.wantErr {
132173
t.Errorf("GetUserAndGroups() error = %v, wantErr %v", err, tc.wantErr)

internal/request/impersonation.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package request
55

66
import (
77
nethttp "net/http"
8+
"regexp"
89
"strings"
910

1011
authenticationv1 "k8s.io/api/authentication/v1"
@@ -25,6 +26,42 @@ func GetImpersonatingUser(request *nethttp.Request) string {
2526
return request.Header.Get(authenticationv1.ImpersonateUserHeader)
2627
}
2728

28-
func GetImpersonatingGroups(request *nethttp.Request) []string {
29-
return request.Header.Values(authenticationv1.ImpersonateGroupHeader)
29+
func GetImpersonatingGroups(request *nethttp.Request, ignoreImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp) []string {
30+
groups := request.Header.Values(authenticationv1.ImpersonateGroupHeader)
31+
if len(groups) > 0 {
32+
if impersonationGroupsRegexp != nil {
33+
groups = filterGroups(groups, impersonationGroupsRegexp)
34+
}
35+
36+
if len(ignoreImpersonationGroups) > 0 {
37+
groups = ignoreGroups(groups, regexp.MustCompile(strings.Join(ignoreImpersonationGroups, "|")))
38+
}
39+
}
40+
41+
return groups
42+
}
43+
44+
func filterGroups(groups []string, impersonationGroupsRegexp *regexp.Regexp) []string {
45+
filteredGroups := []string{}
46+
47+
for _, group := range groups {
48+
if impersonationGroupsRegexp.MatchString(group) {
49+
filteredGroups = append(filteredGroups, group)
50+
}
51+
}
52+
53+
return filteredGroups
54+
}
55+
56+
func ignoreGroups(groups []string, ignoredGroupsRegexp *regexp.Regexp) []string {
57+
ignoredGroups := []string{}
58+
59+
for _, group := range groups {
60+
if !ignoredGroupsRegexp.MatchString(group) {
61+
// If the group does NOT match the regex, include it in the filtered list
62+
ignoredGroups = append(ignoredGroups, group)
63+
}
64+
}
65+
66+
return ignoredGroups
3067
}

internal/webserver/middleware/user_in_group.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func CheckUserInIgnoredGroupMiddleware(client client.Writer, log logr.Logger, cl
1919
return func(next http.Handler) http.Handler {
2020
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
2121
if ignoredUserGroups.Len() > 0 {
22-
user, groups, err := req.NewHTTP(request, authTypes, claim, client).GetUserAndGroups()
22+
user, groups, err := req.NewHTTP(request, authTypes, claim, client, nil, nil).GetUserAndGroups()
2323
if err != nil {
2424
log.Error(err, "Cannot retrieve username and group from request")
2525
}
@@ -42,7 +42,7 @@ func CheckUserInIgnoredGroupMiddleware(client client.Writer, log logr.Logger, cl
4242
func CheckUserInCapsuleGroupMiddleware(client client.Writer, log logr.Logger, claim string, authTypes []req.AuthType, impersonate func(http.ResponseWriter, *http.Request)) mux.MiddlewareFunc {
4343
return func(next http.Handler) http.Handler {
4444
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
45-
_, groups, err := req.NewHTTP(request, authTypes, claim, client).GetUserAndGroups()
45+
_, groups, err := req.NewHTTP(request, authTypes, claim, client, nil, nil).GetUserAndGroups()
4646
if err != nil {
4747
log.Error(err, "Cannot retrieve username and group from request")
4848
}

internal/webserver/webserver.go

+28-23
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"net/http/httputil"
1313
"net/textproto"
14+
"regexp"
1415
"strings"
1516
"time"
1617

@@ -62,31 +63,35 @@ func NewKubeFilter(opts options.ListenerOpts, srv options.ServerOptions, rbRefle
6263
reverseProxy.Transport = reverseProxyTransport
6364

6465
return &kubeFilter{
65-
reader: clientOverride,
66-
writer: client,
67-
managerReader: client,
68-
allowedPaths: sets.New("/api", "/apis", "/version"),
69-
authTypes: opts.AuthTypes(),
70-
ignoredUserGroups: sets.New(opts.IgnoredGroupNames()...),
71-
reverseProxy: reverseProxy,
72-
bearerToken: opts.BearerToken(),
73-
usernameClaimField: opts.PreferredUsernameClaim(),
74-
serverOptions: srv,
75-
log: ctrl.Log.WithName("proxy"),
76-
roleBindingsReflector: rbReflector,
66+
reader: clientOverride,
67+
writer: client,
68+
managerReader: client,
69+
allowedPaths: sets.New("/api", "/apis", "/version"),
70+
authTypes: opts.AuthTypes(),
71+
ignoredUserGroups: sets.New(opts.IgnoredGroupNames()...),
72+
ignoredImpersonationGroups: opts.IgnoredImpersonationsGroups(),
73+
impersonationGroupsRegexp: opts.ImpersonationGroupsRegexp(),
74+
reverseProxy: reverseProxy,
75+
bearerToken: opts.BearerToken(),
76+
usernameClaimField: opts.PreferredUsernameClaim(),
77+
serverOptions: srv,
78+
log: ctrl.Log.WithName("proxy"),
79+
roleBindingsReflector: rbReflector,
7780
}, nil
7881
}
7982

8083
type kubeFilter struct {
81-
allowedPaths sets.Set[string]
82-
authTypes []req.AuthType
83-
ignoredUserGroups sets.Set[string]
84-
reverseProxy *httputil.ReverseProxy
85-
bearerToken string
86-
usernameClaimField string
87-
serverOptions options.ServerOptions
88-
log logr.Logger
89-
roleBindingsReflector *controllers.RoleBindingReflector
84+
allowedPaths sets.Set[string]
85+
authTypes []req.AuthType
86+
ignoredUserGroups sets.Set[string]
87+
ignoredImpersonationGroups []string
88+
impersonationGroupsRegexp *regexp.Regexp
89+
reverseProxy *httputil.ReverseProxy
90+
bearerToken string
91+
usernameClaimField string
92+
serverOptions options.ServerOptions
93+
log logr.Logger
94+
roleBindingsReflector *controllers.RoleBindingReflector
9095

9196
managerReader, reader client.Reader
9297
writer client.Writer
@@ -172,7 +177,7 @@ func (n *kubeFilter) handleRequest(request *http.Request, selector labels.Select
172177
}
173178

174179
func (n *kubeFilter) impersonateHandler(writer http.ResponseWriter, request *http.Request) {
175-
hr := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer)
180+
hr := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp)
176181

177182
username, groups, err := hr.GetUserAndGroups()
178183
if err != nil {
@@ -243,7 +248,7 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) {
243248
middleware.CheckUserInCapsuleGroupMiddleware(n.writer, n.log, n.usernameClaimField, n.authTypes, n.impersonateHandler),
244249
)
245250
sr.HandleFunc("", func(writer http.ResponseWriter, request *http.Request) {
246-
proxyRequest := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer)
251+
proxyRequest := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, nil, nil)
247252
username, groups, err := proxyRequest.GetUserAndGroups()
248253
if err != nil {
249254
server.HandleError(writer, err, "cannot retrieve user and group from the request")

0 commit comments

Comments
 (0)