Skip to content

Commit

Permalink
add audit/admin trace support for browser requests (minio#10947)
Browse files Browse the repository at this point in the history
To support this functionality we had to fork
the gorilla/rpc package with relevant changes
  • Loading branch information
harshavardhana authored Nov 21, 2020
1 parent 7bc47a1 commit 86409fa
Show file tree
Hide file tree
Showing 21 changed files with 2,033 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ issues:
exclude:
- should have a package comment
- error strings should not be capitalized or end with punctuation or a newline

run:
skip-dirs:
- pkg/rpc

service:
golangci-lint-version: 1.20.0 # use the fixed version to not introduce new linters unexpectedly
2 changes: 1 addition & 1 deletion browser/app/js/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import storage from 'local-storage-fallback'

class Web {
constructor(endpoint) {
const namespace = 'Web'
const namespace = 'web'
this.JSONrpc = new JSONrpc({
endpoint,
namespace
Expand Down
65 changes: 27 additions & 38 deletions browser/ui-assets.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions cmd/admin-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 66 additions & 1 deletion cmd/http-tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down
19 changes: 19 additions & 0 deletions cmd/web-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"net/url"
"os"
"path"
"reflect"
"runtime"
"strconv"
"strings"
Expand Down Expand Up @@ -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{}
Expand Down
42 changes: 21 additions & 21 deletions cmd/web-handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 18 additions & 3 deletions cmd/web-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
Loading

0 comments on commit 86409fa

Please sign in to comment.