Add middleware for audit log (#21376)

Add middleware for audit log ext

Signed-off-by: stonezdj <stone.zhang@broadcom.com>
This commit is contained in:
stonezdj(Daojun Zhang) 2025-01-14 11:54:26 +08:00 committed by GitHub
parent b0545c05fd
commit 67654f26bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 151 additions and 2 deletions

View File

@ -0,0 +1,89 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commonevent
import (
"context"
"regexp"
"sync"
"github.com/goharbor/harbor/src/pkg/notifier/event"
)
// Resolver the interface to resolve Metadata to CommonEvent
type Resolver interface {
Resolve(*Metadata, *event.Event) error
PreCheck(ctx context.Context, url string, method string) (bool, string)
}
var urlResolvers = map[string]Resolver{}
var mu = &sync.Mutex{}
// RegisterResolver register a resolver for a specific URL pattern
func RegisterResolver(urlPattern string, resolver Resolver) {
mu.Lock()
urlResolvers[urlPattern] = resolver
mu.Unlock()
}
// Resolvers get map of resolvers
func Resolvers() map[string]Resolver {
return urlResolvers
}
// Metadata the raw data of event
type Metadata struct {
// Ctx ...
Ctx context.Context
// Username requester username
Username string
// RequestPayload http request payload
RequestPayload string
// RequestMethod
RequestMethod string
// ResponseCode response code
ResponseCode int
// RequestURL request URL
RequestURL string
// IPAddress IP address of the request
IPAddress string
// ResponseLocation response location
ResponseLocation string
// ResourceName
ResourceName string
}
// Resolve parse the audit information from CommonEventMetadata
func (c *Metadata) Resolve(event *event.Event) error {
for url, r := range Resolvers() {
p := regexp.MustCompile(url)
if p.MatchString(c.RequestURL) {
return r.Resolve(c, event)
}
}
return nil
}
// PreCheck check if current event is matched and return the prefetched resource name when it is delete operation
func (c *Metadata) PreCheckMetadata() (bool, string) {
for urlPattern, r := range Resolvers() {
p := regexp.MustCompile(urlPattern)
if p.MatchString(c.RequestURL) {
return r.PreCheck(c.Ctx, c.RequestURL, c.RequestMethod)
}
}
return false, ""
}

View File

@ -90,7 +90,6 @@ func MiddleWares() []web.MiddleWare {
trace.Middleware(),
metric.Middleware(),
requestid.Middleware(),
log.Middleware(),
session.Middleware(),
csrf.Middleware(),
orm.Middleware(pingSkipper),
@ -98,6 +97,7 @@ func MiddleWares() []web.MiddleWare {
transaction.Middleware(dbTxSkippers...),
artifactinfo.Middleware(),
security.Middleware(pingSkipper),
log.Middleware(), // log middleware should be after the security middleware so that the user info can be logged
security.UnauthorizedMiddleware(),
readonly.Middleware(readonlySkippers...),
}

View File

@ -15,10 +15,15 @@
package log
import (
"io"
"net/http"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
tracelib "github.com/goharbor/harbor/src/lib/trace"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/server/middleware"
)
@ -40,6 +45,61 @@ func Middleware() func(http.Handler) http.Handler {
r = r.WithContext(ctx)
}
next.ServeHTTP(w, r)
e := &commonevent.Metadata{
Ctx: r.Context(),
Username: "unknown",
RequestMethod: r.Method,
RequestURL: r.URL.String(),
}
if matched, resName := e.PreCheckMetadata(); matched {
lib.NopCloseRequest(r)
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read request body", http.StatusInternalServerError)
return
}
requestContent := string(body)
if secCtx, ok := security.FromContext(r.Context()); ok {
e.Username = secCtx.GetUsername()
}
rw := &ResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(rw, r)
// Add information in the response
e.ResourceName = resName
e.RequestPayload = requestContent
e.ResponseCode = rw.statusCode
// Need to parse the Location header to get the resource ID on creating resource
if e.RequestMethod == http.MethodPost {
e.ResponseLocation = rw.header.Get("Location")
}
notification.AddEvent(e.Ctx, e, true)
} else {
next.ServeHTTP(w, r)
}
})
}
// ResponseWriter wrapper to HTTP response to get the statusCode and response content
type ResponseWriter struct {
http.ResponseWriter
statusCode int
header http.Header
}
// WriteHeader write header info
func (rw *ResponseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Header get header info
func (rw *ResponseWriter) Header() http.Header {
rw.header = rw.ResponseWriter.Header()
return rw.ResponseWriter.Header()
}