mirror of
https://github.com/minio/minio.git
synced 2025-11-07 04:42:56 -05:00
add audit/admin trace support for browser requests (#10947)
To support this functionality we had to fork the gorilla/rpc package with relevant changes
This commit is contained in:
@@ -1006,6 +1006,15 @@ func mustTrace(entry interface{}, trcAll, errOnly bool) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle browser requests separately filter them and return.
|
||||
if HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath+"/upload") {
|
||||
if errOnly {
|
||||
return trcInfo.RespInfo.StatusCode >= http.StatusBadRequest
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
trace := trcAll || !HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath+SlashSeparator)
|
||||
if errOnly {
|
||||
return trace && trcInfo.RespInfo.StatusCode >= http.StatusBadRequest
|
||||
|
||||
@@ -28,8 +28,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
jsonrpc "github.com/minio/minio/pkg/rpc"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
)
|
||||
|
||||
@@ -83,8 +85,8 @@ func getOpName(name string) (op string) {
|
||||
op = strings.TrimPrefix(name, "github.com/minio/minio/cmd.")
|
||||
op = strings.TrimSuffix(op, "Handler-fm")
|
||||
op = strings.Replace(op, "objectAPIHandlers", "s3", 1)
|
||||
op = strings.Replace(op, "webAPIHandlers", "webui", 1)
|
||||
op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
|
||||
op = strings.Replace(op, "(*webAPIHandlers)", "web", 1)
|
||||
op = strings.Replace(op, "(*storageRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*peerRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*lockRESTServer)", "internal", 1)
|
||||
@@ -95,6 +97,69 @@ func getOpName(name string) (op string) {
|
||||
return op
|
||||
}
|
||||
|
||||
// WebTrace gets trace of web request
|
||||
func WebTrace(ri *jsonrpc.RequestInfo) trace.Info {
|
||||
r := ri.Request
|
||||
w := ri.ResponseWriter
|
||||
|
||||
name := ri.Method
|
||||
// Setup a http request body recorder
|
||||
reqHeaders := r.Header.Clone()
|
||||
reqHeaders.Set("Host", r.Host)
|
||||
if len(r.TransferEncoding) == 0 {
|
||||
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
|
||||
} else {
|
||||
reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
|
||||
}
|
||||
|
||||
t := trace.Info{FuncName: name}
|
||||
t.NodeName = r.Host
|
||||
if globalIsDistErasure {
|
||||
t.NodeName = GetLocalPeer(globalEndpoints)
|
||||
}
|
||||
|
||||
// strip port from the host address
|
||||
if host, _, err := net.SplitHostPort(t.NodeName); err == nil {
|
||||
t.NodeName = host
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
rq := trace.RequestInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Proto: r.Proto,
|
||||
Method: r.Method,
|
||||
Path: SlashSeparator + pathJoin(vars["bucket"], vars["object"]),
|
||||
RawQuery: r.URL.RawQuery,
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
}
|
||||
|
||||
rw, ok := w.(*logger.ResponseWriter)
|
||||
if ok {
|
||||
rs := trace.ResponseInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Headers: rw.Header().Clone(),
|
||||
StatusCode: rw.StatusCode,
|
||||
Body: logger.BodyPlaceHolder,
|
||||
}
|
||||
|
||||
if rs.StatusCode == 0 {
|
||||
rs.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
t.RespInfo = rs
|
||||
t.CallStats = trace.CallStats{
|
||||
Latency: rs.Time.Sub(rw.StartTime),
|
||||
InputBytes: int(r.ContentLength),
|
||||
OutputBytes: rw.Size(),
|
||||
TimeToFirstByte: rw.TimeToFirstByte,
|
||||
}
|
||||
}
|
||||
|
||||
t.ReqInfo = rq
|
||||
return t
|
||||
}
|
||||
|
||||
// Trace gets trace of http request
|
||||
func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) trace.Info {
|
||||
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -56,6 +57,24 @@ import (
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
)
|
||||
|
||||
func extractBucketObject(args reflect.Value) (bucketName, objectName string) {
|
||||
switch args.Kind() {
|
||||
case reflect.Ptr:
|
||||
a := args.Elem()
|
||||
for i := 0; i < a.NumField(); i++ {
|
||||
switch a.Type().Field(i).Name {
|
||||
case "BucketName":
|
||||
bucketName = a.Field(i).String()
|
||||
case "Prefix":
|
||||
objectName = a.Field(i).String()
|
||||
case "ObjectName":
|
||||
objectName = a.Field(i).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return bucketName, objectName
|
||||
}
|
||||
|
||||
// WebGenericArgs - empty struct for calls that don't accept arguments
|
||||
// for ex. ServerInfo, GenerateAuth
|
||||
type WebGenericArgs struct{}
|
||||
|
||||
@@ -189,7 +189,7 @@ func testStorageInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func testServerInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
|
||||
serverInfoRequest := &WebGenericArgs{}
|
||||
serverInfoReply := &ServerInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.ServerInfo", authorization, serverInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.ServerInfo", authorization, serverInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -279,7 +279,7 @@ func testMakeBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
for i, testCase := range testCases {
|
||||
makeBucketRequest := MakeBucketArgs{BucketName: testCase.bucketName}
|
||||
makeBucketReply := &WebGenericRep{}
|
||||
req, err := newTestWebRPCRequest("Web.MakeBucket", authorization, makeBucketRequest)
|
||||
req, err := newTestWebRPCRequest("web.MakeBucket", authorization, makeBucketRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request: <ERROR> %v", i+1, err)
|
||||
}
|
||||
@@ -366,7 +366,7 @@ func testDeleteBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
makeBucketRequest := MakeBucketArgs{BucketName: test.bucketName}
|
||||
makeBucketReply := &WebGenericRep{}
|
||||
|
||||
req, err := newTestWebRPCRequest("Web.DeleteBucket", test.token, makeBucketRequest)
|
||||
req, err := newTestWebRPCRequest("web.DeleteBucket", test.token, makeBucketRequest)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func testListBucketsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
|
||||
listBucketsRequest := WebGenericArgs{}
|
||||
listBucketsReply := &ListBucketsRep{}
|
||||
req, err := newTestWebRPCRequest("Web.ListBuckets", authorization, listBucketsRequest)
|
||||
req, err := newTestWebRPCRequest("web.ListBuckets", authorization, listBucketsRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -500,7 +500,7 @@ func testListObjectsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
listObjectsRequest := ListObjectsArgs{BucketName: bucketName, Prefix: ""}
|
||||
listObjectsReply := &ListObjectsRep{}
|
||||
var req *http.Request
|
||||
req, err = newTestWebRPCRequest("Web.ListObjects", token, listObjectsRequest)
|
||||
req, err = newTestWebRPCRequest("web.ListObjects", token, listObjectsRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -584,7 +584,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest := RemoveObjectArgs{BucketName: bucketName, Objects: []string{"a/", "object"}}
|
||||
removeReply := &WebGenericRep{}
|
||||
req, err := newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err := newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -599,7 +599,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest = RemoveObjectArgs{BucketName: bucketName, Objects: []string{"a/", "object"}}
|
||||
removeReply = &WebGenericRep{}
|
||||
req, err = newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err = newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -614,7 +614,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest = RemoveObjectArgs{BucketName: bucketName}
|
||||
removeReply = &WebGenericRep{}
|
||||
req, err = newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err = newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ func testGenerateAuthWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
generateAuthRequest := WebGenericArgs{}
|
||||
generateAuthReply := &GenerateAuthReply{}
|
||||
req, err := newTestWebRPCRequest("Web.GenerateAuth", authorization, generateAuthRequest)
|
||||
req, err := newTestWebRPCRequest("web.GenerateAuth", authorization, generateAuthRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -692,7 +692,7 @@ func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler)
|
||||
args := WebGenericArgs{}
|
||||
tokenReply := &URLTokenReply{}
|
||||
|
||||
req, err := newTestWebRPCRequest("Web.CreateURLToken", authorization, args)
|
||||
req, err := newTestWebRPCRequest("web.CreateURLToken", authorization, args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1043,7 +1043,7 @@ func testWebPresignedGetHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
Expiry: 1000,
|
||||
}
|
||||
presignGetRep := &PresignedGetRep{}
|
||||
req, err := newTestWebRPCRequest("Web.PresignedGet", authorization, presignGetReq)
|
||||
req, err := newTestWebRPCRequest("web.PresignedGet", authorization, presignGetReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -1088,7 +1088,7 @@ func testWebPresignedGetHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
ObjectName: "",
|
||||
}
|
||||
presignGetRep = &PresignedGetRep{}
|
||||
req, err = newTestWebRPCRequest("Web.PresignedGet", authorization, presignGetReq)
|
||||
req, err = newTestWebRPCRequest("web.PresignedGet", authorization, presignGetReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -1140,7 +1140,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
for _, rpcCall := range webRPCs {
|
||||
reply := &WebGenericRep{}
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall, "Bearer fooauthorization", &WebGenericArgs{})
|
||||
req, nerr := newTestWebRPCRequest("web."+rpcCall, "Bearer fooauthorization", &WebGenericArgs{})
|
||||
if nerr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request: <ERROR> %v", rpcCall, nerr)
|
||||
}
|
||||
@@ -1159,7 +1159,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
// Test authorization of Web.Download
|
||||
// Test authorization of web.Download
|
||||
req, err := http.NewRequest(http.MethodGet, "/minio/download/bucket/object?token=wrongauth", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create upload request, %v", err)
|
||||
@@ -1175,7 +1175,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
// Test authorization of Web.Upload
|
||||
// Test authorization of web.Upload
|
||||
content := []byte("temporary file's content")
|
||||
req, err = http.NewRequest(http.MethodPut, "/minio/upload/bucket/object", nil)
|
||||
req.Header.Set("Authorization", "Bearer foo-authorization")
|
||||
@@ -1264,7 +1264,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
for _, rpcCall := range webRPCs {
|
||||
args := &rpcCall.ReqArgs
|
||||
reply := &rpcCall.RepArgs
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall.webRPCName, authorization, args)
|
||||
req, nerr := newTestWebRPCRequest("web."+rpcCall.webRPCName, authorization, args)
|
||||
if nerr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request: <ERROR> %v", rpcCall, nerr)
|
||||
}
|
||||
@@ -1278,10 +1278,10 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test Web.StorageInfo
|
||||
// Test web.StorageInfo
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@@ -1294,7 +1294,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
t.Fatalf("Failed %v", err)
|
||||
}
|
||||
|
||||
// Test authorization of Web.Download
|
||||
// Test authorization of web.Download
|
||||
req, err = http.NewRequest(http.MethodGet, "/minio/download/bucket/object?token="+authorization, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create upload request, %v", err)
|
||||
@@ -1304,7 +1304,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code)
|
||||
}
|
||||
|
||||
// Test authorization of Web.Upload
|
||||
// Test authorization of web.Upload
|
||||
content := []byte("temporary file's content")
|
||||
req, err = http.NewRequest(http.MethodPut, "/minio/upload/bucket/object", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+authorization)
|
||||
|
||||
@@ -23,9 +23,10 @@ import (
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
jsonrpc "github.com/gorilla/rpc/v2"
|
||||
"github.com/gorilla/rpc/v2/json2"
|
||||
"github.com/minio/minio/browser"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
jsonrpc "github.com/minio/minio/pkg/rpc"
|
||||
"github.com/minio/minio/pkg/rpc/json2"
|
||||
)
|
||||
|
||||
// webAPI container for Web API.
|
||||
@@ -76,9 +77,23 @@ func registerWebRouter(router *mux.Router) error {
|
||||
webRPC := jsonrpc.NewServer()
|
||||
webRPC.RegisterCodec(codec, "application/json")
|
||||
webRPC.RegisterCodec(codec, "application/json; charset=UTF-8")
|
||||
webRPC.RegisterAfterFunc(func(ri *jsonrpc.RequestInfo) {
|
||||
if ri != nil {
|
||||
claims, _, _ := webRequestAuthenticate(ri.Request)
|
||||
bucketName, objectName := extractBucketObject(ri.Args)
|
||||
ri.Request = mux.SetURLVars(ri.Request, map[string]string{
|
||||
"bucket": bucketName,
|
||||
"object": objectName,
|
||||
})
|
||||
if globalHTTPTrace.HasSubscribers() {
|
||||
globalHTTPTrace.Publish(WebTrace(ri))
|
||||
}
|
||||
logger.AuditLog(ri.ResponseWriter, ri.Request, ri.Method, claims.Map())
|
||||
}
|
||||
})
|
||||
|
||||
// Register RPC handlers with server
|
||||
if err := webRPC.RegisterService(web, "Web"); err != nil {
|
||||
if err := webRPC.RegisterService(web, "web"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user