github.com/minio/console@v1.4.1/pkg/logger/audit.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package logger 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "net/http" 25 "strconv" 26 "sync/atomic" 27 "time" 28 29 "github.com/minio/console/pkg/utils" 30 31 "github.com/minio/console/pkg/logger/message/audit" 32 ) 33 34 // ResponseWriter - is a wrapper to trap the http response status code. 35 type ResponseWriter struct { 36 http.ResponseWriter 37 StatusCode int 38 // Log body of 4xx or 5xx responses 39 LogErrBody bool 40 // Log body of all responses 41 LogAllBody bool 42 43 TimeToFirstByte time.Duration 44 StartTime time.Time 45 // number of bytes written 46 bytesWritten int 47 // Internal recording buffer 48 headers bytes.Buffer 49 body bytes.Buffer 50 // Indicate if headers are written in the log 51 headersLogged bool 52 } 53 54 // NewResponseWriter - returns a wrapped response writer to trap 55 // http status codes for auditing purposes. 56 func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { 57 return &ResponseWriter{ 58 ResponseWriter: w, 59 StatusCode: http.StatusOK, 60 StartTime: time.Now().UTC(), 61 } 62 } 63 64 func (lrw *ResponseWriter) Write(p []byte) (int, error) { 65 if !lrw.headersLogged { 66 // We assume the response code to be '200 OK' when WriteHeader() is not called, 67 // that way following Golang HTTP response behavior. 68 lrw.WriteHeader(http.StatusOK) 69 } 70 n, err := lrw.ResponseWriter.Write(p) 71 lrw.bytesWritten += n 72 if lrw.TimeToFirstByte == 0 { 73 lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime) 74 } 75 if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { 76 // Always logging error responses. 77 lrw.body.Write(p) 78 } 79 if err != nil { 80 return n, err 81 } 82 return n, err 83 } 84 85 // Write the headers into the given buffer 86 func (lrw *ResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) { 87 n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode)) 88 lrw.bytesWritten += n 89 for k, v := range headers { 90 n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0]) 91 lrw.bytesWritten += n 92 } 93 } 94 95 // BodyPlaceHolder returns a dummy body placeholder 96 var BodyPlaceHolder = []byte("<BODY>") 97 98 // Body - Return response body. 99 func (lrw *ResponseWriter) Body() []byte { 100 // If there was an error response or body logging is enabled 101 // then we return the body contents 102 if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { 103 return lrw.body.Bytes() 104 } 105 // ... otherwise we return the <BODY> place holder 106 return BodyPlaceHolder 107 } 108 109 // WriteHeader - writes http status code 110 func (lrw *ResponseWriter) WriteHeader(code int) { 111 if !lrw.headersLogged { 112 lrw.StatusCode = code 113 lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header()) 114 lrw.headersLogged = true 115 lrw.ResponseWriter.WriteHeader(code) 116 } 117 } 118 119 // Flush - Calls the underlying Flush. 120 func (lrw *ResponseWriter) Flush() { 121 lrw.ResponseWriter.(http.Flusher).Flush() 122 } 123 124 // Size - reutrns the number of bytes written 125 func (lrw *ResponseWriter) Size() int { 126 return lrw.bytesWritten 127 } 128 129 // SetAuditEntry sets Audit info in the context. 130 func SetAuditEntry(ctx context.Context, audit *audit.Entry) context.Context { 131 if ctx == nil { 132 LogIf(context.Background(), fmt.Errorf("context is nil")) 133 return nil 134 } 135 return context.WithValue(ctx, utils.ContextAuditKey, audit) 136 } 137 138 // GetAuditEntry returns Audit entry if set. 139 func GetAuditEntry(ctx context.Context) *audit.Entry { 140 if ctx != nil { 141 r, ok := ctx.Value(utils.ContextAuditKey).(*audit.Entry) 142 if ok { 143 return r 144 } 145 r = &audit.Entry{ 146 Version: audit.Version, 147 // DeploymentID: globalDeploymentID, 148 Time: time.Now().UTC(), 149 } 150 SetAuditEntry(ctx, r) 151 return r 152 } 153 return nil 154 } 155 156 // AuditLog - logs audit logs to all audit targets. 157 func AuditLog(ctx context.Context, w *ResponseWriter, r *http.Request, reqClaims map[string]interface{}, filterKeys ...string) { 158 // Fast exit if there is not audit target configured 159 if atomic.LoadInt32(&nAuditTargets) == 0 { 160 return 161 } 162 163 var entry audit.Entry 164 165 if w != nil && r != nil { 166 reqInfo := GetReqInfo(ctx) 167 if reqInfo == nil { 168 return 169 } 170 entry = audit.ToEntry(w, r, reqClaims, GetGlobalDeploymentID()) 171 // indicates all requests for this API call are inbound 172 entry.Trigger = "incoming" 173 174 for _, filterKey := range filterKeys { 175 delete(entry.ReqClaims, filterKey) 176 delete(entry.ReqQuery, filterKey) 177 delete(entry.ReqHeader, filterKey) 178 delete(entry.RespHeader, filterKey) 179 } 180 181 var ( 182 statusCode int 183 timeToResponse time.Duration 184 timeToFirstByte time.Duration 185 outputBytes int64 = -1 // -1: unknown output bytes 186 ) 187 188 if w != nil { 189 statusCode = w.StatusCode 190 timeToResponse = time.Now().UTC().Sub(w.StartTime) 191 timeToFirstByte = w.TimeToFirstByte 192 outputBytes = int64(w.Size()) 193 } 194 195 entry.API.Path = r.URL.Path 196 197 entry.API.Status = http.StatusText(statusCode) 198 entry.API.StatusCode = statusCode 199 entry.API.Method = r.Method 200 entry.API.InputBytes = r.ContentLength 201 entry.API.OutputBytes = outputBytes 202 entry.RequestID = reqInfo.RequestID 203 204 entry.API.TimeToResponse = strconv.FormatInt(timeToResponse.Nanoseconds(), 10) + "ns" 205 entry.Tags = reqInfo.GetTagsMap() 206 // ttfb will be recorded only for GET requests, Ignore such cases where ttfb will be empty. 207 if timeToFirstByte != 0 { 208 entry.API.TimeToFirstByte = strconv.FormatInt(timeToFirstByte.Nanoseconds(), 10) + "ns" 209 } 210 } else { 211 auditEntry := GetAuditEntry(ctx) 212 if auditEntry != nil { 213 entry = *auditEntry 214 } 215 } 216 217 if anonFlag { 218 entry.SessionID = hashString(entry.SessionID) 219 entry.RemoteHost = hashString(entry.RemoteHost) 220 } 221 222 // Send audit logs only to http targets. 223 for _, t := range AuditTargets() { 224 if err := t.Send(entry, string(All)); err != nil { 225 LogAlwaysIf(context.Background(), fmt.Errorf("event(%v) was not sent to Audit target (%v): %v", entry, t, err), All) 226 } 227 } 228 }