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  }