Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame] | 1 | /* |
| 2 | Copyright 2016 The Kubernetes Authors. |
| 3 | |
| 4 | Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | you may not use this file except in compliance with the License. |
| 6 | You may obtain a copy of the License at |
| 7 | |
| 8 | http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | |
| 10 | Unless required by applicable law or agreed to in writing, software |
| 11 | distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | See the License for the specific language governing permissions and |
| 14 | limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package filters |
| 18 | |
| 19 | import ( |
| 20 | "bufio" |
| 21 | "errors" |
| 22 | "fmt" |
| 23 | "net" |
| 24 | "net/http" |
| 25 | "sync" |
| 26 | "time" |
| 27 | |
| 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 29 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" |
| 30 | auditinternal "k8s.io/apiserver/pkg/apis/audit" |
| 31 | "k8s.io/apiserver/pkg/audit" |
| 32 | "k8s.io/apiserver/pkg/audit/policy" |
| 33 | "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" |
| 34 | "k8s.io/apiserver/pkg/endpoints/request" |
| 35 | ) |
| 36 | |
| 37 | // WithAudit decorates a http.Handler with audit logging information for all the |
| 38 | // requests coming to the server. Audit level is decided according to requests' |
| 39 | // attributes and audit policy. Logs are emitted to the audit sink to |
| 40 | // process events. If sink or audit policy is nil, no decoration takes place. |
| 41 | func WithAudit(handler http.Handler, sink audit.Sink, policy policy.Checker, longRunningCheck request.LongRunningRequestCheck) http.Handler { |
| 42 | if sink == nil || policy == nil { |
| 43 | return handler |
| 44 | } |
| 45 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| 46 | req, ev, omitStages, err := createAuditEventAndAttachToContext(req, policy) |
| 47 | if err != nil { |
| 48 | utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err)) |
| 49 | responsewriters.InternalError(w, req, errors.New("failed to create audit event")) |
| 50 | return |
| 51 | } |
| 52 | ctx := req.Context() |
| 53 | if ev == nil || ctx == nil { |
| 54 | handler.ServeHTTP(w, req) |
| 55 | return |
| 56 | } |
| 57 | |
| 58 | ev.Stage = auditinternal.StageRequestReceived |
| 59 | processAuditEvent(sink, ev, omitStages) |
| 60 | |
| 61 | // intercept the status code |
| 62 | var longRunningSink audit.Sink |
| 63 | if longRunningCheck != nil { |
| 64 | ri, _ := request.RequestInfoFrom(ctx) |
| 65 | if longRunningCheck(req, ri) { |
| 66 | longRunningSink = sink |
| 67 | } |
| 68 | } |
| 69 | respWriter := decorateResponseWriter(w, ev, longRunningSink, omitStages) |
| 70 | |
| 71 | // send audit event when we leave this func, either via a panic or cleanly. In the case of long |
| 72 | // running requests, this will be the second audit event. |
| 73 | defer func() { |
| 74 | if r := recover(); r != nil { |
| 75 | defer panic(r) |
| 76 | ev.Stage = auditinternal.StagePanic |
| 77 | ev.ResponseStatus = &metav1.Status{ |
| 78 | Code: http.StatusInternalServerError, |
| 79 | Status: metav1.StatusFailure, |
| 80 | Reason: metav1.StatusReasonInternalError, |
| 81 | Message: fmt.Sprintf("APIServer panic'd: %v", r), |
| 82 | } |
| 83 | processAuditEvent(sink, ev, omitStages) |
| 84 | return |
| 85 | } |
| 86 | |
| 87 | // if no StageResponseStarted event was sent b/c neither a status code nor a body was sent, fake it here |
| 88 | // But Audit-Id http header will only be sent when http.ResponseWriter.WriteHeader is called. |
| 89 | fakedSuccessStatus := &metav1.Status{ |
| 90 | Code: http.StatusOK, |
| 91 | Status: metav1.StatusSuccess, |
| 92 | Message: "Connection closed early", |
| 93 | } |
| 94 | if ev.ResponseStatus == nil && longRunningSink != nil { |
| 95 | ev.ResponseStatus = fakedSuccessStatus |
| 96 | ev.Stage = auditinternal.StageResponseStarted |
| 97 | processAuditEvent(longRunningSink, ev, omitStages) |
| 98 | } |
| 99 | |
| 100 | ev.Stage = auditinternal.StageResponseComplete |
| 101 | if ev.ResponseStatus == nil { |
| 102 | ev.ResponseStatus = fakedSuccessStatus |
| 103 | } |
| 104 | processAuditEvent(sink, ev, omitStages) |
| 105 | }() |
| 106 | handler.ServeHTTP(respWriter, req) |
| 107 | }) |
| 108 | } |
| 109 | |
| 110 | // createAuditEventAndAttachToContext is responsible for creating the audit event |
| 111 | // and attaching it to the appropriate request context. It returns: |
| 112 | // - context with audit event attached to it |
| 113 | // - created audit event |
| 114 | // - error if anything bad happened |
| 115 | func createAuditEventAndAttachToContext(req *http.Request, policy policy.Checker) (*http.Request, *auditinternal.Event, []auditinternal.Stage, error) { |
| 116 | ctx := req.Context() |
| 117 | |
| 118 | attribs, err := GetAuthorizerAttributes(ctx) |
| 119 | if err != nil { |
| 120 | return req, nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err) |
| 121 | } |
| 122 | |
| 123 | level, omitStages := policy.LevelAndStages(attribs) |
| 124 | audit.ObservePolicyLevel(level) |
| 125 | if level == auditinternal.LevelNone { |
| 126 | // Don't audit. |
| 127 | return req, nil, nil, nil |
| 128 | } |
| 129 | |
| 130 | ev, err := audit.NewEventFromRequest(req, level, attribs) |
| 131 | if err != nil { |
| 132 | return req, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err) |
| 133 | } |
| 134 | |
| 135 | req = req.WithContext(request.WithAuditEvent(ctx, ev)) |
| 136 | |
| 137 | return req, ev, omitStages, nil |
| 138 | } |
| 139 | |
| 140 | func processAuditEvent(sink audit.Sink, ev *auditinternal.Event, omitStages []auditinternal.Stage) { |
| 141 | for _, stage := range omitStages { |
| 142 | if ev.Stage == stage { |
| 143 | return |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | if ev.Stage == auditinternal.StageRequestReceived { |
| 148 | ev.StageTimestamp = metav1.NewMicroTime(ev.RequestReceivedTimestamp.Time) |
| 149 | } else { |
| 150 | ev.StageTimestamp = metav1.NewMicroTime(time.Now()) |
| 151 | } |
| 152 | audit.ObserveEvent() |
| 153 | sink.ProcessEvents(ev) |
| 154 | } |
| 155 | |
| 156 | func decorateResponseWriter(responseWriter http.ResponseWriter, ev *auditinternal.Event, sink audit.Sink, omitStages []auditinternal.Stage) http.ResponseWriter { |
| 157 | delegate := &auditResponseWriter{ |
| 158 | ResponseWriter: responseWriter, |
| 159 | event: ev, |
| 160 | sink: sink, |
| 161 | omitStages: omitStages, |
| 162 | } |
| 163 | |
| 164 | // check if the ResponseWriter we're wrapping is the fancy one we need |
| 165 | // or if the basic is sufficient |
| 166 | _, cn := responseWriter.(http.CloseNotifier) |
| 167 | _, fl := responseWriter.(http.Flusher) |
| 168 | _, hj := responseWriter.(http.Hijacker) |
| 169 | if cn && fl && hj { |
| 170 | return &fancyResponseWriterDelegator{delegate} |
| 171 | } |
| 172 | return delegate |
| 173 | } |
| 174 | |
| 175 | var _ http.ResponseWriter = &auditResponseWriter{} |
| 176 | |
| 177 | // auditResponseWriter intercepts WriteHeader, sets it in the event. If the sink is set, it will |
| 178 | // create immediately an event (for long running requests). |
| 179 | type auditResponseWriter struct { |
| 180 | http.ResponseWriter |
| 181 | event *auditinternal.Event |
| 182 | once sync.Once |
| 183 | sink audit.Sink |
| 184 | omitStages []auditinternal.Stage |
| 185 | } |
| 186 | |
| 187 | func (a *auditResponseWriter) setHttpHeader() { |
| 188 | a.ResponseWriter.Header().Set(auditinternal.HeaderAuditID, string(a.event.AuditID)) |
| 189 | } |
| 190 | |
| 191 | func (a *auditResponseWriter) processCode(code int) { |
| 192 | a.once.Do(func() { |
| 193 | if a.event.ResponseStatus == nil { |
| 194 | a.event.ResponseStatus = &metav1.Status{} |
| 195 | } |
| 196 | a.event.ResponseStatus.Code = int32(code) |
| 197 | a.event.Stage = auditinternal.StageResponseStarted |
| 198 | |
| 199 | if a.sink != nil { |
| 200 | processAuditEvent(a.sink, a.event, a.omitStages) |
| 201 | } |
| 202 | }) |
| 203 | } |
| 204 | |
| 205 | func (a *auditResponseWriter) Write(bs []byte) (int, error) { |
| 206 | // the Go library calls WriteHeader internally if no code was written yet. But this will go unnoticed for us |
| 207 | a.processCode(http.StatusOK) |
| 208 | a.setHttpHeader() |
| 209 | return a.ResponseWriter.Write(bs) |
| 210 | } |
| 211 | |
| 212 | func (a *auditResponseWriter) WriteHeader(code int) { |
| 213 | a.processCode(code) |
| 214 | a.setHttpHeader() |
| 215 | a.ResponseWriter.WriteHeader(code) |
| 216 | } |
| 217 | |
| 218 | // fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and |
| 219 | // http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc) |
| 220 | // working. |
| 221 | type fancyResponseWriterDelegator struct { |
| 222 | *auditResponseWriter |
| 223 | } |
| 224 | |
| 225 | func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool { |
| 226 | return f.ResponseWriter.(http.CloseNotifier).CloseNotify() |
| 227 | } |
| 228 | |
| 229 | func (f *fancyResponseWriterDelegator) Flush() { |
| 230 | f.ResponseWriter.(http.Flusher).Flush() |
| 231 | } |
| 232 | |
| 233 | func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
| 234 | // fake a response status before protocol switch happens |
| 235 | f.processCode(http.StatusSwitchingProtocols) |
| 236 | |
| 237 | // This will be ignored if WriteHeader() function has already been called. |
| 238 | // It's not guaranteed Audit-ID http header is sent for all requests. |
| 239 | // For example, when user run "kubectl exec", apiserver uses a proxy handler |
| 240 | // to deal with the request, users can only get http headers returned by kubelet node. |
| 241 | f.setHttpHeader() |
| 242 | |
| 243 | return f.ResponseWriter.(http.Hijacker).Hijack() |
| 244 | } |
| 245 | |
| 246 | var _ http.CloseNotifier = &fancyResponseWriterDelegator{} |
| 247 | var _ http.Flusher = &fancyResponseWriterDelegator{} |
| 248 | var _ http.Hijacker = &fancyResponseWriterDelegator{} |