|
| 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