Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit 6af47f8

Browse files
authored
Merge pull request #136 from JoshVanL/handler-based-chain
Handler based chaining for proxy
2 parents 63ba131 + cf067e8 commit 6af47f8

File tree

9 files changed

+726
-308
lines changed

9 files changed

+726
-308
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ go_vet:
7272
go vet ./cmd
7373

7474
go_lint: $(BINDIR)/golangci-lint ## lint golang code for problems
75-
$(BINDIR)/golangci-lint run
75+
$(BINDIR)/golangci-lint run --timeout 3m
7676

7777
clean: ## clean up created files
7878
rm -rf \

cmd/app/options/options.go

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func (o *Options) Validate(cmd *cobra.Command) error {
8080
errs = append(errs, errors.New("unable to securely serve on port 8080 (used by readiness probe)"))
8181
}
8282

83+
if o.App.DisableImpersonation &&
84+
(o.App.ExtraHeaderOptions.EnableClientIPExtraUserHeader || len(o.App.ExtraHeaderOptions.ExtraUserHeaders) > 0) {
85+
errs = append(errs, errors.New("cannot add extra user headers when impersonation disabled"))
86+
}
87+
8388
if len(errs) > 0 {
8489
return k8sErrors.NewAggregate(errs)
8590
}

cmd/app/run.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Comma
7272
return err
7373
}
7474

75-
proxyOptions := &proxy.Options{
75+
proxyConfig := &proxy.Config{
7676
TokenReview: opts.App.TokenPassthrough.Enabled,
7777
DisableImpersonation: opts.App.DisableImpersonation,
7878

@@ -84,7 +84,7 @@ func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Comma
8484

8585
// Initialise proxy with OIDC token authenticator
8686
p, err := proxy.New(restConfig, opts.OIDCAuthentication,
87-
tokenReviewer, secureServingInfo, proxyOptions)
87+
tokenReviewer, secureServingInfo, proxyConfig)
8888
if err != nil {
8989
return err
9090
}

pkg/proxy/context/context.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package context
3+
4+
import (
5+
"context"
6+
"net/http"
7+
8+
"k8s.io/apiserver/pkg/endpoints/request"
9+
"k8s.io/client-go/transport"
10+
)
11+
12+
type key int
13+
14+
const (
15+
// noImpersonationKey is the context key for whether to use impersonation.
16+
noImpersonationKey key = iota
17+
18+
// impersonationConfigKey is the context key for the impersonation config.
19+
impersonationConfigKey
20+
21+
// bearerTokenKey is the context key for the bearer token.
22+
bearerTokenKey
23+
)
24+
25+
// WithNoImpersonation returns a copy of parent in which the noImpersonation value is set.
26+
func WithNoImpersonation(parent context.Context) context.Context {
27+
return request.WithValue(parent, noImpersonationKey, true)
28+
}
29+
30+
// NoImpersonation returns whether the noImpersonation key has been set
31+
func NoImpersonation(ctx context.Context) bool {
32+
noImp, _ := ctx.Value(noImpersonationKey).(bool)
33+
return noImp
34+
}
35+
36+
// WithImpersonationConfig returns a copy of parent in which contains the impersonation configuration.
37+
func WithImpersonationConfig(parent context.Context, conf *transport.ImpersonationConfig) context.Context {
38+
return request.WithValue(parent, impersonationConfigKey, conf)
39+
}
40+
41+
// ImpersonationConfig returns the impersonation configuration held in the context if existing.
42+
func ImpersonationConfig(ctx context.Context) *transport.ImpersonationConfig {
43+
conf, _ := ctx.Value(impersonationConfigKey).(*transport.ImpersonationConfig)
44+
return conf
45+
}
46+
47+
// WithBearerToken will add the bearer token from an http.Header to the context.
48+
func WithBearerToken(parent context.Context, header http.Header) context.Context {
49+
return request.WithValue(parent, bearerTokenKey, header.Get("Authorization"))
50+
}
51+
52+
// BearerToken will return the bearer token stored in the context.
53+
func BearerToken(ctx context.Context) string {
54+
token, _ := ctx.Value(bearerTokenKey).(string)
55+
return token
56+
}

pkg/proxy/handlers.go

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright Jetstack Ltd. See LICENSE for details.
2+
package proxy
3+
4+
import (
5+
"net/http"
6+
"strings"
7+
8+
authuser "k8s.io/apiserver/pkg/authentication/user"
9+
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
10+
"k8s.io/client-go/transport"
11+
"k8s.io/klog"
12+
13+
"github.com/jetstack/kube-oidc-proxy/pkg/proxy/context"
14+
)
15+
16+
func (p *Proxy) withHandlers(handler http.Handler) http.Handler {
17+
// Set up proxy handlers
18+
handler = p.withImpersonateRequest(handler)
19+
handler = p.withAuthenticateRequest(handler)
20+
return handler
21+
}
22+
23+
// withAuthenticateRequest adds the proxy authentication handler to a chain.
24+
func (p *Proxy) withAuthenticateRequest(handler http.Handler) http.Handler {
25+
tokenReviewHandler := p.withTokenReview(handler)
26+
27+
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
28+
// Auth request and handle unauthed
29+
info, ok, err := p.oidcRequestAuther.AuthenticateRequest(req)
30+
if err != nil {
31+
// Since we have failed OIDC auth, we will try a token review, if enabled.
32+
tokenReviewHandler.ServeHTTP(rw, req)
33+
return
34+
}
35+
36+
// Failed authorization
37+
if !ok {
38+
p.handleError(rw, req, errUnauthorized)
39+
return
40+
}
41+
42+
klog.V(4).Infof("authenticated request: %s", req.RemoteAddr)
43+
44+
// Add the user info to the request context
45+
req = req.WithContext(genericapirequest.WithUser(req.Context(), info.User))
46+
handler.ServeHTTP(rw, req)
47+
})
48+
}
49+
50+
// withTokenReview will attempt a token review on the incoming request, if
51+
// enabled.
52+
func (p *Proxy) withTokenReview(handler http.Handler) http.Handler {
53+
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
54+
// If token review is not enabled then error.
55+
if !p.config.TokenReview {
56+
p.handleError(rw, req, errUnauthorized)
57+
return
58+
}
59+
60+
// Attempt to passthrough request if valid token
61+
if !p.reviewToken(rw, req) {
62+
// Token review failed so error
63+
p.handleError(rw, req, errUnauthorized)
64+
return
65+
}
66+
67+
// Set no impersonation headers and re-add removed headers.
68+
req = req.WithContext(context.WithNoImpersonation(req.Context()))
69+
70+
handler.ServeHTTP(rw, req)
71+
})
72+
}
73+
74+
// withImpersonateRequest adds the impersonation request handler to the chain.
75+
func (p *Proxy) withImpersonateRequest(handler http.Handler) http.Handler {
76+
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
77+
// If no impersonation has already been set, return early
78+
if context.NoImpersonation(req.Context()) {
79+
handler.ServeHTTP(rw, req)
80+
return
81+
}
82+
83+
// If we have disabled impersonation we can forward the request right away
84+
if p.config.DisableImpersonation {
85+
klog.V(2).Infof("passing on request with no impersonation: %s", req.RemoteAddr)
86+
// Indicate we need to not use impersonation.
87+
req = req.WithContext(context.WithNoImpersonation(req.Context()))
88+
handler.ServeHTTP(rw, req)
89+
return
90+
}
91+
92+
if p.hasImpersonation(req.Header) {
93+
p.handleError(rw, req, errImpersonateHeader)
94+
return
95+
}
96+
97+
user, ok := genericapirequest.UserFrom(req.Context())
98+
// No name available so reject request
99+
if !ok || len(user.GetName()) == 0 {
100+
p.handleError(rw, req, errNoName)
101+
return
102+
}
103+
104+
// Ensure group contains allauthenticated builtin
105+
allAuthFound := false
106+
groups := user.GetGroups()
107+
for _, elem := range groups {
108+
if elem == authuser.AllAuthenticated {
109+
allAuthFound = true
110+
break
111+
}
112+
}
113+
if !allAuthFound {
114+
groups = append(groups, authuser.AllAuthenticated)
115+
}
116+
117+
extra := user.GetExtra()
118+
119+
if extra == nil {
120+
extra = make(map[string][]string)
121+
}
122+
123+
// If client IP user extra header option set then append the remote client
124+
// address.
125+
if p.config.ExtraUserHeadersClientIPEnabled {
126+
klog.V(6).Infof("adding impersonate extra user header %s: %s (%s)",
127+
UserHeaderClientIPKey, req.RemoteAddr, req.RemoteAddr)
128+
129+
extra[UserHeaderClientIPKey] = append(extra[UserHeaderClientIPKey], req.RemoteAddr)
130+
}
131+
132+
// Add custom extra user headers to impersonation request.
133+
for k, vs := range p.config.ExtraUserHeaders {
134+
for _, v := range vs {
135+
klog.V(6).Infof("adding impersonate extra user header %s: %s (%s)",
136+
k, v, req.RemoteAddr)
137+
138+
extra[k] = append(extra[k], v)
139+
}
140+
}
141+
142+
conf := &transport.ImpersonationConfig{
143+
UserName: user.GetName(),
144+
Groups: groups,
145+
Extra: extra,
146+
}
147+
148+
// Add the impersonation configuration to the context.
149+
req = req.WithContext(context.WithImpersonationConfig(req.Context(), conf))
150+
handler.ServeHTTP(rw, req)
151+
})
152+
}
153+
154+
// newErrorHandler returns a handler failed requests.
155+
func (p *Proxy) newErrorHandler() func(rw http.ResponseWriter, r *http.Request, err error) {
156+
return func(rw http.ResponseWriter, r *http.Request, err error) {
157+
if err == nil {
158+
klog.Error("error was called with no error")
159+
http.Error(rw, "", http.StatusInternalServerError)
160+
return
161+
}
162+
163+
switch err {
164+
165+
// Failed auth
166+
case errUnauthorized:
167+
klog.V(2).Infof("unauthenticated user request %s", r.RemoteAddr)
168+
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
169+
return
170+
171+
// User request with impersonation
172+
case errImpersonateHeader:
173+
klog.V(2).Infof("impersonation user request %s", r.RemoteAddr)
174+
http.Error(rw, "Impersonation requests are disabled when using kube-oidc-proxy", http.StatusForbidden)
175+
return
176+
177+
// No name given or available in oidc request
178+
case errNoName:
179+
klog.V(2).Infof("no name available in oidc info %s", r.RemoteAddr)
180+
http.Error(rw, "Username claim not available in OIDC Issuer response", http.StatusForbidden)
181+
return
182+
183+
// No impersonation configuration found in context
184+
case errNoImpersonationConfig:
185+
klog.Errorf("if you are seeing this, there is likely a bug in the proxy (%s): %s", r.RemoteAddr, err)
186+
http.Error(rw, "", http.StatusInternalServerError)
187+
return
188+
189+
// Server or unknown error
190+
default:
191+
klog.Errorf("unknown error (%s): %s", r.RemoteAddr, err)
192+
http.Error(rw, "", http.StatusInternalServerError)
193+
}
194+
}
195+
}
196+
197+
func (p *Proxy) hasImpersonation(header http.Header) bool {
198+
for h := range header {
199+
if strings.ToLower(h) == impersonateUserHeader ||
200+
strings.ToLower(h) == impersonateGroupHeader ||
201+
strings.HasPrefix(strings.ToLower(h), impersonateExtraHeader) {
202+
203+
return true
204+
}
205+
}
206+
207+
return false
208+
}

0 commit comments

Comments
 (0)