diff --git a/cmd/bucket-lifecycle.go b/cmd/bucket-lifecycle.go index c3e0cb48f..7636b18b4 100644 --- a/cmd/bucket-lifecycle.go +++ b/cmd/bucket-lifecycle.go @@ -342,6 +342,10 @@ func deleteTransitionedObject(ctx context.Context, objectAPI ObjectLayer, bucket if err != nil { return err } + + // Send audit for the lifecycle delete operation + auditLogLifecycle(ctx, bucket, object) + eventName := event.ObjectRemovedDelete if lcOpts.DeleteMarker { eventName = event.ObjectRemovedDeleteMarkerCreated diff --git a/cmd/data-scanner.go b/cmd/data-scanner.go index a476aa6d0..64b2840c2 100644 --- a/cmd/data-scanner.go +++ b/cmd/data-scanner.go @@ -32,6 +32,7 @@ import ( "github.com/minio/minio/cmd/config/heal" "github.com/minio/minio/cmd/logger" + "github.com/minio/minio/cmd/logger/message/audit" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/color" @@ -1058,6 +1059,9 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay return false } + // Send audit for the lifecycle delete operation + auditLogLifecycle(ctx, obj.Bucket, obj.Name) + eventName := event.ObjectRemovedDelete if obj.DeleteMarker { eventName = event.ObjectRemovedDeleteMarkerCreated @@ -1275,3 +1279,13 @@ func (d *dynamicSleeper) Update(factor float64, maxWait time.Duration) error { d.cycle = make(chan struct{}) return nil } + +func auditLogLifecycle(ctx context.Context, bucket, object string) { + entry := audit.NewEntry(globalDeploymentID) + entry.Trigger = "internal-scanner" + entry.API.Name = "DeleteObject" + entry.API.Bucket = bucket + entry.API.Object = object + ctx = logger.SetAuditEntry(ctx, &entry) + logger.AuditLog(ctx, nil, nil, nil) +} diff --git a/cmd/logger/audit.go b/cmd/logger/audit.go index c7b3f06ca..5274bf3ae 100644 --- a/cmd/logger/audit.go +++ b/cmd/logger/audit.go @@ -123,6 +123,31 @@ func (lrw *ResponseWriter) Size() int { return lrw.bytesWritten } +const contextAuditKey = contextKeyType("audit-entry") + +// SetAuditEntry sets Audit info in the context. +func SetAuditEntry(ctx context.Context, audit *audit.Entry) context.Context { + if ctx == nil { + LogIf(context.Background(), fmt.Errorf("context is nil")) + return nil + } + return context.WithValue(ctx, contextAuditKey, audit) +} + +// GetAuditEntry returns Audit entry if set. +func GetAuditEntry(ctx context.Context) *audit.Entry { + if ctx != nil { + r, ok := ctx.Value(contextAuditKey).(*audit.Entry) + if ok { + return r + } + r = &audit.Entry{} + SetAuditEntry(ctx, r) + return r + } + return nil +} + // AuditLog - logs audit logs to all audit targets. func AuditLog(ctx context.Context, w http.ResponseWriter, r *http.Request, reqClaims map[string]interface{}, filterKeys ...string) { // Fast exit if there is not audit target configured @@ -130,41 +155,53 @@ func AuditLog(ctx context.Context, w http.ResponseWriter, r *http.Request, reqCl return } - var ( - statusCode int - timeToResponse time.Duration - timeToFirstByte time.Duration - ) + var entry audit.Entry - st, ok := w.(*ResponseWriter) - if ok { - statusCode = st.StatusCode - timeToResponse = time.Now().UTC().Sub(st.StartTime) - timeToFirstByte = st.TimeToFirstByte - } + if w != nil && r != nil { + reqInfo := GetReqInfo(ctx) + if reqInfo == nil { + return + } - reqInfo := GetReqInfo(ctx) - if reqInfo == nil { - return - } + entry = audit.ToEntry(w, r, reqClaims, globalDeploymentID) + entry.Trigger = "external-request" - entry := audit.ToEntry(w, r, reqClaims, globalDeploymentID) - for _, filterKey := range filterKeys { - delete(entry.ReqClaims, filterKey) - delete(entry.ReqQuery, filterKey) - delete(entry.ReqHeader, filterKey) - delete(entry.RespHeader, filterKey) - } - entry.API.Name = reqInfo.API - entry.API.Bucket = reqInfo.BucketName - entry.API.Object = reqInfo.ObjectName - entry.API.Status = http.StatusText(statusCode) - entry.API.StatusCode = statusCode - entry.API.TimeToResponse = strconv.FormatInt(timeToResponse.Nanoseconds(), 10) + "ns" - entry.Tags = reqInfo.GetTagsMap() - // ttfb will be recorded only for GET requests, Ignore such cases where ttfb will be empty. - if timeToFirstByte != 0 { - entry.API.TimeToFirstByte = strconv.FormatInt(timeToFirstByte.Nanoseconds(), 10) + "ns" + for _, filterKey := range filterKeys { + delete(entry.ReqClaims, filterKey) + delete(entry.ReqQuery, filterKey) + delete(entry.ReqHeader, filterKey) + delete(entry.RespHeader, filterKey) + } + + var ( + statusCode int + timeToResponse time.Duration + timeToFirstByte time.Duration + ) + + st, ok := w.(*ResponseWriter) + if ok { + statusCode = st.StatusCode + timeToResponse = time.Now().UTC().Sub(st.StartTime) + timeToFirstByte = st.TimeToFirstByte + } + + entry.API.Name = reqInfo.API + entry.API.Bucket = reqInfo.BucketName + entry.API.Object = reqInfo.ObjectName + entry.API.Status = http.StatusText(statusCode) + entry.API.StatusCode = statusCode + entry.API.TimeToResponse = strconv.FormatInt(timeToResponse.Nanoseconds(), 10) + "ns" + entry.Tags = reqInfo.GetTagsMap() + // ttfb will be recorded only for GET requests, Ignore such cases where ttfb will be empty. + if timeToFirstByte != 0 { + entry.API.TimeToFirstByte = strconv.FormatInt(timeToFirstByte.Nanoseconds(), 10) + "ns" + } + } else { + auditEntry := GetAuditEntry(ctx) + if auditEntry != nil { + entry = *auditEntry + } } // Send audit logs only to http targets. diff --git a/cmd/logger/message/audit/entry.go b/cmd/logger/message/audit/entry.go index b00214a62..dd7286e97 100644 --- a/cmd/logger/message/audit/entry.go +++ b/cmd/logger/message/audit/entry.go @@ -33,6 +33,7 @@ type Entry struct { Version string `json:"version"` DeploymentID string `json:"deploymentid,omitempty"` Time string `json:"time"` + Trigger string `json:"trigger"` API struct { Name string `json:"name,omitempty"` Bucket string `json:"bucket,omitempty"` @@ -52,38 +53,48 @@ type Entry struct { Tags map[string]interface{} `json:"tags,omitempty"` } -// ToEntry - constructs an audit entry object. +// NewEntry - constructs an audit entry object with some fields filled +func NewEntry(deploymentID string) Entry { + return Entry{ + Version: Version, + DeploymentID: deploymentID, + Time: time.Now().UTC().Format(time.RFC3339Nano), + } +} + +// ToEntry - constructs an audit entry from a http request func ToEntry(w http.ResponseWriter, r *http.Request, reqClaims map[string]interface{}, deploymentID string) Entry { + + entry := NewEntry(deploymentID) + + entry.RemoteHost = handlers.GetSourceIP(r) + entry.UserAgent = r.UserAgent() + entry.ReqClaims = reqClaims + q := r.URL.Query() reqQuery := make(map[string]string, len(q)) for k, v := range q { reqQuery[k] = strings.Join(v, ",") } + entry.ReqQuery = reqQuery + reqHeader := make(map[string]string, len(r.Header)) for k, v := range r.Header { reqHeader[k] = strings.Join(v, ",") } + entry.ReqHeader = reqHeader + wh := w.Header() + entry.RequestID = wh.Get(xhttp.AmzRequestID) respHeader := make(map[string]string, len(wh)) for k, v := range wh { respHeader[k] = strings.Join(v, ",") } + entry.RespHeader = respHeader + if etag := respHeader[xhttp.ETag]; etag != "" { respHeader[xhttp.ETag] = strings.Trim(etag, `"`) } - entry := Entry{ - Version: Version, - DeploymentID: deploymentID, - RemoteHost: handlers.GetSourceIP(r), - RequestID: wh.Get(xhttp.AmzRequestID), - UserAgent: r.UserAgent(), - Time: time.Now().UTC().Format(time.RFC3339Nano), - ReqQuery: reqQuery, - ReqHeader: reqHeader, - ReqClaims: reqClaims, - RespHeader: respHeader, - } - return entry }