diff --git a/.gitignore b/.gitignore index cbe51798e..c144586a6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ vendor/ # Ignore executables target/ +!pkg/logger/target/ console !console/ diff --git a/cluster/config.go b/cluster/config.go index 96ac280ff..fb7e10799 100644 --- a/cluster/config.go +++ b/cluster/config.go @@ -23,6 +23,8 @@ import ( "strings" "time" + xhttp "github.com/minio/console/pkg/http" + "github.com/minio/console/pkg/utils" "github.com/minio/pkg/env" @@ -68,7 +70,7 @@ func GetMinioImage() (*string, error) { return &image, nil } latestMinIOImage, errLatestMinIOImage := utils.GetLatestMinIOImage( - &utils.HTTPClient{ + &xhttp.Client{ Client: &http.Client{ Timeout: 5 * time.Second, }, diff --git a/cmd/console/app_commands.go b/cmd/console/app_commands.go index fa355e65c..517900034 100644 --- a/cmd/console/app_commands.go +++ b/cmd/console/app_commands.go @@ -20,10 +20,14 @@ package main import ( + "context" + "fmt" "os" "strconv" "time" + "github.com/minio/console/pkg/logger" + "github.com/minio/cli" "github.com/minio/console/restapi" ) @@ -36,15 +40,28 @@ var appCmds = []cli.Command{ // StartServer starts the console service func StartServer(ctx *cli.Context) error { - if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" { - return startOperatorServer(ctx) - } + // Load all certificates if err := loadAllCerts(ctx); err != nil { // Log this as a warning and continue running console without TLS certificates restapi.LogError("Unable to load certs: %v", err) } + xctx := context.Background() + transport := restapi.PrepareSTSClientTransport(false) + if err := logger.InitializeLogger(xctx, transport); err != nil { + fmt.Println("error InitializeLogger", err) + logger.CriticalIf(xctx, err) + } + // custom error configuration + restapi.LogInfo = logger.Info + restapi.LogError = logger.Error + restapi.LogIf = logger.LogIf + + if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" { + return startOperatorServer(ctx) + } + var rctx restapi.Context if err := rctx.Load(ctx); err != nil { restapi.LogError("argument validation failed: %v", err) diff --git a/cmd/console/app_commands_noop.go b/cmd/console/app_commands_noop.go index 576dea507..6d9161143 100644 --- a/cmd/console/app_commands_noop.go +++ b/cmd/console/app_commands_noop.go @@ -20,9 +20,13 @@ package main import ( + "context" + "fmt" "strconv" "time" + "github.com/minio/console/pkg/logger" + "github.com/minio/cli" "github.com/minio/console/restapi" ) @@ -39,6 +43,17 @@ func StartServer(ctx *cli.Context) error { restapi.LogError("Unable to load certs: %v", err) } + xctx := context.Background() + transport := restapi.PrepareSTSClientTransport(false) + if err := logger.InitializeLogger(xctx, transport); err != nil { + fmt.Println("error InitializeLogger", err) + logger.CriticalIf(xctx, err) + } + // custom error configuration + restapi.LogInfo = logger.Info + restapi.LogError = logger.Error + restapi.LogIf = logger.LogIf + var rctx restapi.Context if err := rctx.Load(ctx); err != nil { restapi.LogError("argument validation failed: %v", err) diff --git a/cmd/console/operator.go b/cmd/console/operator.go index 138eb6d00..36a16590b 100644 --- a/cmd/console/operator.go +++ b/cmd/console/operator.go @@ -2,7 +2,7 @@ // +build operator // This file is part of MinIO Console Server -// Copyright (c) 2021 MinIO, Inc. +// Copyright (c) 2022 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -20,6 +20,7 @@ package main import ( + "context" "fmt" "io/ioutil" "path/filepath" @@ -27,6 +28,8 @@ import ( "syscall" "time" + "github.com/minio/console/pkg/logger" + "github.com/minio/console/restapi" "github.com/go-openapi/loads" @@ -106,7 +109,7 @@ func buildOperatorServer() (*operatorapi.Server, error) { } api := operations.NewOperatorAPI(swaggerSpec) - api.Logger = operatorapi.LogInfo + api.Logger = restapi.LogInfo server := operatorapi.NewServer(api) parser := flags.NewParser(server, flags.Default) @@ -147,7 +150,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error { } // load the certificates and the CAs - operatorapi.GlobalRootCAs, operatorapi.GlobalPublicCerts, operatorapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs() + restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs() if err != nil { return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err) } @@ -159,12 +162,12 @@ func loadOperatorAllCerts(ctx *cli.Context) error { swaggerServerCACertificate := ctx.String("tls-ca") // load tls cert and key from swagger server tls-certificate and tls-key flags if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" { - if err = operatorapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil { + if err = restapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil { return err } x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate) if err == nil { - operatorapi.GlobalPublicCerts = append(operatorapi.GlobalPublicCerts, x509Certs...) + restapi.GlobalPublicCerts = append(restapi.GlobalPublicCerts, x509Certs...) } } @@ -172,7 +175,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error { if swaggerServerCACertificate != "" { caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate) if caCertErr == nil { - operatorapi.GlobalRootCAs.AppendCertsFromPEM(caCert) + restapi.GlobalRootCAs.AppendCertsFromPEM(caCert) } } } @@ -186,20 +189,32 @@ func loadOperatorAllCerts(ctx *cli.Context) error { // StartServer starts the console service func startOperatorServer(ctx *cli.Context) error { - if err := loadOperatorAllCerts(ctx); err != nil { + + if err := loadAllCerts(ctx); err != nil { // Log this as a warning and continue running console without TLS certificates - operatorapi.LogError("Unable to load certs: %v", err) + restapi.LogError("Unable to load certs: %v", err) } + xctx := context.Background() + transport := restapi.PrepareSTSClientTransport(false) + if err := logger.InitializeLogger(xctx, transport); err != nil { + fmt.Println("error InitializeLogger", err) + logger.CriticalIf(xctx, err) + } + // custom error configuration + restapi.LogInfo = logger.Info + restapi.LogError = logger.Error + restapi.LogIf = logger.LogIf + var rctx operatorapi.Context if err := rctx.Load(ctx); err != nil { - operatorapi.LogError("argument validation failed: %v", err) + restapi.LogError("argument validation failed: %v", err) return err } server, err := buildOperatorServer() if err != nil { - operatorapi.LogError("Unable to initialize console server: %v", err) + restapi.LogError("Unable to initialize console server: %v", err) return err } @@ -212,7 +227,7 @@ func startOperatorServer(ctx *cli.Context) error { operatorapi.Port = strconv.Itoa(server.Port) operatorapi.Hostname = server.Host - if len(operatorapi.GlobalPublicCerts) > 0 { + if len(restapi.GlobalPublicCerts) > 0 { // If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect // plain HTTP connections to HTTPS server server.EnabledListeners = []string{"http", "https"} diff --git a/go.mod b/go.mod index e3225b314..8740ca367 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cheggaaa/pb/v3 v3.0.8 github.com/dustin/go-humanize v1.0.0 + github.com/fatih/color v1.13.0 github.com/go-openapi/errors v0.20.2 github.com/go-openapi/loads v0.21.1 github.com/go-openapi/runtime v0.23.3 @@ -14,10 +15,12 @@ require ( github.com/go-openapi/swag v0.21.1 github.com/go-openapi/validate v0.21.0 github.com/golang-jwt/jwt/v4 v4.4.1 + github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/klauspost/compress v1.15.1 github.com/minio/cli v1.22.0 + github.com/minio/highwayhash v1.0.2 github.com/minio/kes v0.19.2 github.com/minio/madmin-go v1.3.12 github.com/minio/mc v0.0.0-20220419155441-cc4ff3a0cc82 @@ -57,7 +60,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/fatih/color v1.13.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect @@ -74,7 +76,6 @@ require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -109,7 +110,7 @@ require ( github.com/navidys/tvxwidgets v0.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/philhofer/fwd v1.1.1 // indirect + github.com/philhofer/fwd v1.1.2-0.20210722190033-5c56ac6d0bb9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/xattr v0.4.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -126,7 +127,7 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/tinylib/msgp v1.1.6 // indirect + github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.4.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect diff --git a/go.sum b/go.sum index 0cd336daa..e8ebfc420 100644 --- a/go.sum +++ b/go.sum @@ -470,6 +470,8 @@ github.com/minio/colorjson v1.0.2 h1:Em3IM68MTm3h+Oxa0nxrV9VQqDgbxvC5iq5A+pqzDeI github.com/minio/colorjson v1.0.2/go.mod h1:JWxcL2n8T8JVf+NY6awl6kn5nK49aAzHOeQEM33dL0k= github.com/minio/filepath v1.0.0 h1:fvkJu1+6X+ECRA6G3+JJETj4QeAYO9sV43I79H8ubDY= github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEXx9T/Bw= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/kes v0.19.2 h1:0kdMAgLMSkiDA33k8pMHC7d6erDuseuLrZF+N3017SM= github.com/minio/kes v0.19.2/go.mod h1:X2fMkDbAkjbSKDGOQZvyPkHxoG7nuzP6R78Jw+TzXtM= github.com/minio/madmin-go v1.3.5/go.mod h1:vGKGboQgGIWx4DuDUaXixjlIEZOCIp6ivJkQoiVaACc= @@ -556,8 +558,9 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/philhofer/fwd v1.1.2-0.20210722190033-5c56ac6d0bb9 h1:6ob53CVz+ja2i7easAStApZJlh7sxyq3Cm7g1Di6iqA= +github.com/philhofer/fwd v1.1.2-0.20210722190033-5c56ac6d0bb9/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -663,8 +666,8 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.3/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= -github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e h1:P5tyWbssToKowBPTA1/EzqPXwrZNc8ZeNPdjgpcDEoI= +github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e/go.mod h1:g7jEyb18KPe65d9RRhGw+ThaJr5duyBH8eaFgBUor7Y= github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= @@ -858,6 +861,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/operatorapi/auth/operator.go b/operatorapi/auth/operator.go index fbd66466b..3fa74053c 100644 --- a/operatorapi/auth/operator.go +++ b/operatorapi/auth/operator.go @@ -18,15 +18,14 @@ package auth import ( "context" - "errors" + + errors "github.com/minio/console/restapi" "github.com/minio/console/cluster" "github.com/minio/minio-go/v7/pkg/credentials" operatorClientset "github.com/minio/operator/pkg/client/clientset/versioned" ) -var errInvalidCredentials = errors.New("invalid Login") - // operatorCredentialsProvider is an struct to hold the JWT (service account token) type operatorCredentialsProvider struct { serviceAccountJWT string @@ -86,7 +85,7 @@ func GetConsoleCredentialsForOperator(jwt string) (*credentials.Credentials, err client: opClientClientSet, } if err = checkServiceAccountTokenValid(ctx, opClient); err != nil { - return nil, errInvalidCredentials + return nil, errors.ErrInvalidLogin } return credentials.New(operatorCredentialsProvider{serviceAccountJWT: jwt}), nil } diff --git a/operatorapi/configure_operator.go b/operatorapi/configure_operator.go index b78d904c6..191774c6a 100644 --- a/operatorapi/configure_operator.go +++ b/operatorapi/configure_operator.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/klauspost/compress/gzhttp" - "github.com/minio/console/restapi" "github.com/unrolled/secure" @@ -58,7 +57,7 @@ func configureAPI(api *operations.OperatorAPI) http.Handler { api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) { // we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt // was generated and signed by us in the first place - claims, err := auth.SessionTokenAuthenticate(token) + claims, err := auth.ParseClaimsFromToken(token) if err != nil { api.Logger("Unable to validate the session token %s: %v", token, err) return nil, errors.New(401, "incorrect api key auth") @@ -101,8 +100,8 @@ func configureAPI(api *operations.OperatorAPI) http.Handler { // The TLS configuration before HTTPS server starts. func configureTLS(tlsConfig *tls.Config) { - tlsConfig.RootCAs = GlobalRootCAs - tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate + tlsConfig.RootCAs = restapi.GlobalRootCAs + tlsConfig.GetCertificate = restapi.GlobalTLSCertsManager.GetCertificate } // As soon as server is initialized but not run yet, this function will be called. @@ -118,24 +117,6 @@ func setupMiddlewares(handler http.Handler) http.Handler { return handler } -func AuthenticationMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token, err := auth.GetTokenFromRequest(r) - if err != nil && err != auth.ErrNoAuthToken { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - // All handlers handle appropriately to return errors - // based on their swagger rules, we do not need to - // additionally return error here, let the next ServeHTTPs - // handle it appropriately. - if token != "" { - r.Header.Add("Authorization", "Bearer "+token) - } - next.ServeHTTP(w, r) - }) -} - // proxyMiddleware adds the proxy capability func proxyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -150,19 +131,23 @@ func proxyMiddleware(next http.Handler) http.Handler { // The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document. // So this is a good place to plug in a panic handling middleware, logging and metrics. func setupGlobalMiddleware(handler http.Handler) http.Handler { - // handle cookie or authorization header for session - next := AuthenticationMiddleware(handler) // proxy requests - next = proxyMiddleware(next) + next := proxyMiddleware(handler) + // if audit-log is enabled console will log all incoming request + next = restapi.AuditLogMiddleware(next) // serve static files next = restapi.FileServerMiddleware(next) + // add information to request context + next = restapi.ContextMiddleware(next) + // handle cookie or authorization header for session + next = restapi.AuthenticationMiddleware(next) // Secure middleware, this middleware wrap all the previous handlers and add // HTTP security headers secureOptions := secure.Options{ AllowedHosts: restapi.GetSecureAllowedHosts(), AllowedHostsAreRegex: restapi.GetSecureAllowedHostsAreRegex(), HostsProxyHeaders: restapi.GetSecureHostsProxyHeaders(), - SSLRedirect: restapi.GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0, + SSLRedirect: restapi.GetTLSRedirect() == "on" && len(restapi.GlobalPublicCerts) > 0, SSLHost: restapi.GetSecureTLSHost(), STSSeconds: restapi.GetSecureSTSSeconds(), STSIncludeSubdomains: restapi.GetSecureSTSIncludeSubdomains(), diff --git a/operatorapi/error.go b/operatorapi/error.go deleted file mode 100644 index c3031cc60..000000000 --- a/operatorapi/error.go +++ /dev/null @@ -1,219 +0,0 @@ -// This file is part of MinIO Console Server -// Copyright (c) 2021 MinIO, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package operatorapi - -import ( - "errors" - "runtime" - "strings" - - "github.com/go-openapi/swag" - "github.com/minio/console/models" - "github.com/minio/madmin-go" - k8sErrors "k8s.io/apimachinery/pkg/api/errors" -) - -var ( - // Generic error messages - errorGeneric = errors.New("an error occurred, please try again") - errInvalidCredentials = errors.New("invalid Login") - errorGenericInvalidSession = errors.New("invalid session") - errorGenericUnauthorized = errors.New("unauthorized") - errorGenericForbidden = errors.New("forbidden") - // ErrorGenericNotFound Generic error for not found - ErrorGenericNotFound = errors.New("not found") - // Explicit error messages - errorInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value") - errorUnableToGetTenantUsage = errors.New("unable to get tenant usage") - errorUnableToGetTenantLogs = errors.New("unable to get tenant logs") - errorUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates") - errorUpdatingEncryptionConfig = errors.New("unable to update encryption configuration") - errorDeletingEncryptionConfig = errors.New("error disabling tenant encryption") - errorEncryptionConfigNotFound = errors.New("encryption configuration not found") - errBucketBodyNotInRequest = errors.New("error bucket body not in request") - errBucketNameNotInRequest = errors.New("error bucket name not in request") - errGroupBodyNotInRequest = errors.New("error group body not in request") - errGroupNameNotInRequest = errors.New("error group name not in request") - errPolicyNameNotInRequest = errors.New("error policy name not in request") - errPolicyBodyNotInRequest = errors.New("error policy body not in request") - errSSENotConfigured = errors.New("error server side encryption configuration not found") - errBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found") - errChangePassword = errors.New("error please check your current password") - errInvalidLicense = errors.New("invalid license key") - errLicenseNotFound = errors.New("license not found") - errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself") - errAccessDenied = errors.New("access denied") - errTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster") - errTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant") - errTooFewSchedulableNodes = errors.New("there is not enough schedulable nodes to satisfy this requirement") - errFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant") -) - -// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message -func prepareError(err ...error) *models.Error { - errorCode := int32(500) - errorMessage := errorGeneric.Error() - if len(err) > 0 { - frame := getFrame(2) - fileParts := strings.Split(frame.File, "/") - LogError("original error -> (%s:%d: %v)", fileParts[len(fileParts)-1], frame.Line, err[0]) - if k8sErrors.IsUnauthorized(err[0]) { - errorCode = 401 - errorMessage = errorGenericUnauthorized.Error() - } - if k8sErrors.IsForbidden(err[0]) { - errorCode = 403 - errorMessage = errorGenericForbidden.Error() - } - if k8sErrors.IsNotFound(err[0]) { - errorCode = 404 - errorMessage = ErrorGenericNotFound.Error() - } - if err[0] == ErrorGenericNotFound { - errorCode = 404 - errorMessage = ErrorGenericNotFound.Error() - } - if errors.Is(err[0], errInvalidCredentials) { - errorCode = 401 - errorMessage = errInvalidCredentials.Error() - } - // console invalid erasure coding value - if errors.Is(err[0], errorInvalidErasureCodingValue) { - errorCode = 400 - errorMessage = errorInvalidErasureCodingValue.Error() - } - if errors.Is(err[0], errBucketBodyNotInRequest) { - errorCode = 400 - errorMessage = errBucketBodyNotInRequest.Error() - } - if errors.Is(err[0], errBucketNameNotInRequest) { - errorCode = 400 - errorMessage = errBucketNameNotInRequest.Error() - } - if errors.Is(err[0], errGroupBodyNotInRequest) { - errorCode = 400 - errorMessage = errGroupBodyNotInRequest.Error() - } - if errors.Is(err[0], errGroupNameNotInRequest) { - errorCode = 400 - errorMessage = errGroupNameNotInRequest.Error() - } - if errors.Is(err[0], errPolicyNameNotInRequest) { - errorCode = 400 - errorMessage = errPolicyNameNotInRequest.Error() - } - if errors.Is(err[0], errPolicyBodyNotInRequest) { - errorCode = 400 - errorMessage = errPolicyBodyNotInRequest.Error() - } - // console invalid session error - if errors.Is(err[0], errorGenericInvalidSession) { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // Bucket life cycle not configured - if errors.Is(err[0], errBucketLifeCycleNotConfigured) { - errorCode = 404 - errorMessage = errBucketLifeCycleNotConfigured.Error() - } - // Encryption not configured - if errors.Is(err[0], errSSENotConfigured) { - errorCode = 404 - errorMessage = errSSENotConfigured.Error() - } - // account change password - if madmin.ToErrorResponse(err[0]).Code == "SignatureDoesNotMatch" { - errorCode = 403 - errorMessage = errChangePassword.Error() - } - if errors.Is(err[0], errLicenseNotFound) { - errorCode = 404 - errorMessage = errLicenseNotFound.Error() - } - if errors.Is(err[0], errInvalidLicense) { - errorCode = 404 - errorMessage = errInvalidLicense.Error() - } - if errors.Is(err[0], errAvoidSelfAccountDelete) { - errorCode = 403 - errorMessage = errAvoidSelfAccountDelete.Error() - } - if madmin.ToErrorResponse(err[0]).Code == "AccessDenied" { - errorCode = 403 - errorMessage = errAccessDenied.Error() - } - if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // console invalid session error - if madmin.ToErrorResponse(err[0]).Code == "XMinioAdminNoSuchUser" { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // if we received a second error take that as friendly message but dont override the code - if len(err) > 1 && err[1] != nil { - LogError("friendly error: %v", err[1].Error()) - errorMessage = err[1].Error() - } - // if we receive third error we just print that as debugging - if len(err) > 2 && err[2] != nil { - LogError("debugging error: %v", err[2].Error()) - } - - errRemoteTierExists := errors.New("Specified remote tier already exists") //nolint - if errors.Is(err[0], errRemoteTierExists) { - errorMessage = err[0].Error() - } - if errors.Is(err[0], errTooFewNodes) { - errorCode = 507 - errorMessage = errTooFewNodes.Error() - } - if errors.Is(err[0], errTooFewSchedulableNodes) { - errorCode = 507 - errorMessage = errTooFewSchedulableNodes.Error() - } - if errors.Is(err[0], errFewerThanFourNodes) { - errorCode = 507 - errorMessage = errFewerThanFourNodes.Error() - } - } - return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err[0].Error())} -} - -func getFrame(skipFrames int) runtime.Frame { - // We need the frame at index skipFrames+2, since we never want runtime.Callers and getFrame - targetFrameIndex := skipFrames + 2 - - // Set size to targetFrameIndex+2 to ensure we have room for one more caller than we need - programCounters := make([]uintptr, targetFrameIndex+2) - n := runtime.Callers(0, programCounters) - - frame := runtime.Frame{Function: "unknown"} - if n > 0 { - frames := runtime.CallersFrames(programCounters[:n]) - for more, frameIndex := true, 0; more && frameIndex <= targetFrameIndex; frameIndex++ { - var frameCandidate runtime.Frame - frameCandidate, more = frames.Next() - if frameIndex == targetFrameIndex { - frame = frameCandidate - } - } - } - - return frame -} diff --git a/operatorapi/login.go b/operatorapi/login.go index 8693dc7e5..49c18b5ea 100644 --- a/operatorapi/login.go +++ b/operatorapi/login.go @@ -42,7 +42,7 @@ import ( func registerLoginHandlers(api *operations.OperatorAPI) { // GET login strategy api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder { - loginDetails, err := getLoginDetailsResponse(params.HTTPRequest) + loginDetails, err := getLoginDetailsResponse(params) if err != nil { return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err) } @@ -50,7 +50,7 @@ func registerLoginHandlers(api *operations.OperatorAPI) { }) // POST login using k8s service account token api.AuthLoginOperatorHandler = authApi.LoginOperatorHandlerFunc(func(params authApi.LoginOperatorParams) middleware.Responder { - loginResponse, err := getLoginOperatorResponse(params.Body) + loginResponse, err := getLoginOperatorResponse(params) if err != nil { return authApi.NewLoginOperatorDefault(int(err.Code)).WithPayload(err) } @@ -63,7 +63,7 @@ func registerLoginHandlers(api *operations.OperatorAPI) { }) // POST login using external IDP api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder { - loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body) + loginResponse, err := getLoginOauth2AuthResponse(params) if err != nil { return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err) } @@ -87,14 +87,19 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) { // if we made it here, the consoleCredentials work, generate a jwt with claims token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), nil) if err != nil { - LogError("error authenticating user: %v", err) - return nil, errInvalidCredentials + restapi.LogError("error authenticating user: %v", err) + return nil, restapi.ErrInvalidLogin } return &token, nil } // getLoginDetailsResponse returns information regarding the Console authentication mechanism. -func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) { +func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + + r := params.HTTPRequest + loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount redirectURL := "" @@ -103,7 +108,7 @@ func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Err // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient()) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // Validate user against IDP identityProvider := &auth.IdentityProvider{Client: oauth2Client} @@ -126,31 +131,35 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, return oauth2Token, nil } -func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.LoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + + r := params.HTTPRequest + lr := params.Body + if oauth2.IsIDPEnabled() { // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient()) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // initialize new identity provider identityProvider := auth.IdentityProvider{Client: oauth2Client} // Validate user against IDP _, err = verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // If we pass here that means the IDP correctly authenticate the user with the operator resource // we proceed to use the service account token configured in the operator-console pod creds, err := newConsoleCredentials(getK8sSAToken()) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } token, err := login(restapi.ConsoleCredentials{ConsoleCredentials: creds}) if err != nil { - return nil, prepareError(errInvalidCredentials, nil, err) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err) } // serialize output loginResponse := &models.LoginResponse{ @@ -158,7 +167,7 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque } return loginResponse, nil } - return nil, prepareError(errorGeneric) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault) } func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) { @@ -170,17 +179,22 @@ func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) { } // getLoginOperatorResponse validate the provided service account token against k8s api -func getLoginOperatorResponse(lmr *models.LoginOperatorRequest) (*models.LoginResponse, *models.Error) { +func getLoginOperatorResponse(params authApi.LoginOperatorParams) (*models.LoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + + lmr := params.Body + creds, err := newConsoleCredentials(*lmr.Jwt) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } consoleCreds := restapi.ConsoleCredentials{ConsoleCredentials: creds} // Set a random as access key as session identifier consoleCreds.AccountAccessKey = fmt.Sprintf("%d", rand.Intn(100000-10000)+10000) token, err := login(consoleCreds) if err != nil { - return nil, prepareError(errInvalidCredentials, nil, err) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err) } // serialize output loginResponse := &models.LoginResponse{ diff --git a/operatorapi/namespaces.go b/operatorapi/namespaces.go index 3c91ef10e..3a37fa2b8 100644 --- a/operatorapi/namespaces.go +++ b/operatorapi/namespaces.go @@ -20,12 +20,13 @@ import ( "context" "errors" - "github.com/minio/console/operatorapi/operations/operator_api" + xerrors "github.com/minio/console/restapi" "github.com/go-openapi/runtime/middleware" "github.com/minio/console/cluster" "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations" + "github.com/minio/console/operatorapi/operations/operator_api" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1" @@ -44,12 +45,12 @@ func registerNamespaceHandlers(api *operations.OperatorAPI) { } func getNamespaceCreatedResponse(session *models.Principal, params operator_api.CreateNamespaceParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return xerrors.ErrorWithContext(ctx, err) } namespace := *params.Body.Name @@ -57,7 +58,7 @@ func getNamespaceCreatedResponse(session *models.Principal, params operator_api. errCreation := getNamespaceCreated(ctx, clientset.CoreV1(), namespace) if errCreation != nil { - return prepareError(errCreation) + return xerrors.ErrorWithContext(ctx, errCreation) } return nil diff --git a/operatorapi/nodes.go b/operatorapi/nodes.go index 86b56a13c..ef7402bba 100644 --- a/operatorapi/nodes.go +++ b/operatorapi/nodes.go @@ -21,6 +21,8 @@ import ( "errors" "sort" + xerrors "github.com/minio/console/restapi" + "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/console/operatorapi/operations/operator_api" @@ -52,8 +54,8 @@ func registerNodesHandlers(api *operations.OperatorAPI) { return operator_api.NewListNodeLabelsOK().WithPayload(*resp) }) - api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder { - resp, err := getAllocatableResourcesResponse(params.NumNodes, principal) + api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, session *models.Principal) middleware.Responder { + resp, err := getAllocatableResourcesResponse(session, params) if err != nil { return operator_api.NewGetAllocatableResourcesDefault(int(err.Code)).WithPayload(err) } @@ -71,7 +73,7 @@ type NodeResourceInfo struct { func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface, numNodes int32) (*models.MaxAllocatableMemResponse, error) { // can't request less than 4 nodes if numNodes < 4 { - return nil, errFewerThanFourNodes + return nil, xerrors.ErrFewerThanFourNodes } // get all nodes from cluster @@ -97,15 +99,15 @@ func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface, } // requesting more nodes than schedulable and less than total number of workers if int(numNodes) > schedulableNodes && int(numNodes) < nonMasterNodes { - return nil, errTooManyNodes + return nil, xerrors.ErrTooManyNodes } if nonMasterNodes < int(numNodes) { - return nil, errTooFewNodes + return nil, xerrors.ErrTooFewNodes } // not enough schedulable nodes if schedulableNodes < int(numNodes) { - return nil, errTooFewSchedulableNodes + return nil, xerrors.ErrTooFewAvailableNodes } availableMemSizes := []int64{} @@ -177,12 +179,12 @@ func min(x, y int64) int64 { func getMaxAllocatableMemoryResponse(ctx context.Context, session *models.Principal, numNodes int32) (*models.MaxAllocatableMemResponse, *models.Error) { client, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } clusterResources, err := getMaxAllocatableMemory(ctx, client.CoreV1(), numNodes) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } return clusterResources, nil } @@ -217,12 +219,12 @@ func getNodeLabels(ctx context.Context, clientset v1.CoreV1Interface) (*models.N func getNodeLabelsResponse(ctx context.Context, session *models.Principal) (*models.NodeLabels, *models.Error) { client, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } clusterResources, err := getNodeLabels(ctx, client.CoreV1()) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } return clusterResources, nil } @@ -357,17 +359,16 @@ OUTER: // Get allocatable resources response -func getAllocatableResourcesResponse(numNodes int32, session *models.Principal) (*models.AllocatableResourcesResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getAllocatableResourcesResponse(session *models.Principal, params operator_api.GetAllocatableResourcesParams) (*models.AllocatableResourcesResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() client, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } - - clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), numNodes) + clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), params.NumNodes) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } return clusterResources, nil } diff --git a/operatorapi/parity.go b/operatorapi/parity.go index b3450d25a..73069eaeb 100644 --- a/operatorapi/parity.go +++ b/operatorapi/parity.go @@ -17,9 +17,10 @@ package operatorapi import ( + "context" "fmt" - "github.com/minio/console/restapi" + errors "github.com/minio/console/restapi" "github.com/minio/console/pkg/utils" @@ -50,14 +51,14 @@ func GetParityInfo(nodes int64, disksPerNode int64) (models.ParityResponse, erro } func getParityResponse(params operator_api.GetParityParams) (models.ParityResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() nodes := params.Nodes disksPerNode := params.DisksPerNode - parityValues, err := GetParityInfo(nodes, disksPerNode) if err != nil { - restapi.LogError("error getting parity info: %v", err) - return nil, prepareError(err) + errors.LogError("error getting parity info: %v", err) + return nil, errors.ErrorWithContext(ctx, err) } - return parityValues, nil } diff --git a/operatorapi/resource_quota.go b/operatorapi/resource_quota.go index a6c0729fc..1f947b9f4 100644 --- a/operatorapi/resource_quota.go +++ b/operatorapi/resource_quota.go @@ -20,6 +20,8 @@ import ( "context" "fmt" + xerrors "github.com/minio/console/restapi" + "k8s.io/apimachinery/pkg/api/errors" "github.com/minio/console/cluster" @@ -94,18 +96,18 @@ func getResourceQuota(ctx context.Context, client K8sClientI, namespace, resourc } func getResourceQuotaResponse(session *models.Principal, params operator_api.GetResourceQuotaParams) (*models.ResourceQuota, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() client, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } k8sClient := &k8sClient{ client: client, } resourceQuota, err := getResourceQuota(ctx, k8sClient, params.Namespace, params.ResourceQuotaName) if err != nil { - return nil, prepareError(err) + return nil, xerrors.ErrorWithContext(ctx, err) } return resourceQuota, nil } diff --git a/operatorapi/session.go b/operatorapi/session.go index f9a6a7c14..1c6374590 100644 --- a/operatorapi/session.go +++ b/operatorapi/session.go @@ -17,8 +17,11 @@ package operatorapi import ( + "context" "fmt" + errors "github.com/minio/console/restapi" + "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations" @@ -28,7 +31,7 @@ import ( func registerSessionHandlers(api *operations.OperatorAPI) { // session check api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder { - sessionResp, err := getSessionResponse(session) + sessionResp, err := getSessionResponse(session, params) if err != nil { return authApi.NewSessionCheckDefault(int(err.Code)).WithPayload(err) } @@ -37,10 +40,12 @@ func registerSessionHandlers(api *operations.OperatorAPI) { } // getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI -func getSessionResponse(session *models.Principal) (*models.OperatorSessionResponse, *models.Error) { +func getSessionResponse(session *models.Principal, params authApi.SessionCheckParams) (*models.OperatorSessionResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() // serialize output if session == nil { - return nil, prepareError(errorGenericInvalidSession) + return nil, errors.ErrorWithContext(ctx, errors.ErrInvalidSession) } sessionResp := &models.OperatorSessionResponse{ Status: models.OperatorSessionResponseStatusOk, diff --git a/operatorapi/tenant_add.go b/operatorapi/tenant_add.go index 5ba5dbdd3..9d6bc75df 100644 --- a/operatorapi/tenant_add.go +++ b/operatorapi/tenant_add.go @@ -41,7 +41,7 @@ import ( func getTenantCreatedResponse(session *models.Principal, params operator_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) { tenantReq := params.Body minioImage := tenantReq.Image - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if minioImage == "" { minImg, err := cluster.GetMinioImage() @@ -56,7 +56,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre client: clientSet, } if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } ns := *tenantReq.Namespace @@ -98,7 +98,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre _, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // Enable/Disable console object browser for MinIO tenant (default is on) @@ -110,7 +110,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre tenantConfigurationENV["MINIO_ROOT_USER"] = accessKey tenantConfigurationENV["MINIO_ROOT_PASSWORD"] = secretKey - // delete secrets created if an error occurred during tenant creation, + // delete secrets created if an errors occurred during tenant creation, defer func() { if mError != nil { restapi.LogError("deleting secrets created for failed tenant: %s if any: %v", tenantName, mError) @@ -127,7 +127,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre // Check the Erasure Coding Parity for validity and pass it to Tenant if tenantReq.ErasureCodingParity > 0 { if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 { - return nil, prepareError(errorInvalidErasureCodingValue) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidErasureCodingValue) } tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity) } @@ -204,7 +204,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre } _, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } } // attach the users to the tenant @@ -248,7 +248,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre } _, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } } // attach the users to the tenant @@ -274,7 +274,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre externalCertSecretName := fmt.Sprintf("%s-instance-external-certificates", secretName) externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, tenantReq.TLS.Minio, externalCertSecretName, tenantName) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } minInst.Spec.ExternalCertSecret = externalCertSecret } @@ -286,7 +286,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre certificates := []*models.KeyPairConfiguration{tenantReq.Encryption.Client} certificateSecrets, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, tenantExternalClientCertSecretName, tenantName) if err != nil { - return nil, prepareError(restapi.ErrorGeneric) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault) } if len(certificateSecrets) > 0 { minInst.Spec.ExternalClientCertSecret = certificateSecrets[0] @@ -296,7 +296,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre // KES configuration for Tenant instance minInst.Spec.KES, err = getKESConfiguration(ctx, &k8sClient, ns, tenantReq.Encryption, secretName, tenantName) if err != nil { - return nil, prepareError(restapi.ErrorGeneric) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault) } // Set Labels, Annotations and Node Selector for KES minInst.Spec.KES.Labels = tenantReq.Encryption.Labels @@ -306,7 +306,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if tenantReq.Encryption.SecurityContext != nil { sc, err := convertModelSCToK8sSC(tenantReq.Encryption.SecurityContext) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } minInst.Spec.KES.SecurityContext = sc } @@ -317,7 +317,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre for i, caCertificate := range tenantReq.TLS.CaCertificates { certificateContent, err := base64.StdEncoding.DecodeString(caCertificate) if err != nil { - return nil, prepareError(restapi.ErrorGeneric, nil, err) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err) } caCertificates = append(caCertificates, tenantSecret{ Name: fmt.Sprintf("ca-certificate-%d", i), @@ -329,7 +329,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if len(caCertificates) > 0 { certificateSecrets, err := createOrReplaceSecrets(ctx, &k8sClient, ns, caCertificates, tenantName) if err != nil { - return nil, prepareError(restapi.ErrorGeneric, nil, err) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err) } minInst.Spec.ExternalCaCertSecret = certificateSecrets } @@ -347,7 +347,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre pool, err := parseTenantPoolRequest(pool) if err != nil { restapi.LogError("parseTenantPoolRequest failed: %v", err) - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } minInst.Spec.Pools = append(minInst.Spec.Pools, *pool) } @@ -363,7 +363,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if tenantReq.ImagePullSecret != "" { imagePullSecret = tenantReq.ImagePullSecret } else if imagePullSecret, err = setImageRegistry(ctx, tenantReq.ImageRegistry, clientSet.CoreV1(), ns, tenantName); err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // pass the image pull secret to the Tenant if imagePullSecret != "" { @@ -410,7 +410,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if tenantReq.LogSearchConfiguration.SecurityContext != nil { sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.SecurityContext) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } logSearchSecurityContext = sc } @@ -418,7 +418,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if tenantReq.LogSearchConfiguration.PostgresSecurityContext != nil { sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.PostgresSecurityContext) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } logSearchPgSecurityContext = sc } @@ -514,7 +514,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre if tenantReq.PrometheusConfiguration != nil && tenantReq.PrometheusConfiguration.SecurityContext != nil { sc, err := convertModelSCToK8sSC(tenantReq.PrometheusConfiguration.SecurityContext) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } minInst.Spec.Prometheus.SecurityContext = sc } @@ -538,7 +538,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre }, }, tenantName) if err != nil { - return nil, prepareError(restapi.ErrorGeneric, nil, err) + return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err) } minInst.Spec.Configuration = &corev1.LocalObjectReference{Name: tenantConfigurationName} @@ -562,20 +562,20 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre opClient, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } _, err = opClient.MinioV2().Tenants(ns).Create(context.Background(), &minInst, metav1.CreateOptions{}) if err != nil { restapi.LogError("Creating new tenant failed with: %v", err) - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // Integrations if os.Getenv("GKE_INTEGRATION") != "" { err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } } response = &models.CreateTenantResponse{ diff --git a/operatorapi/tenant_get.go b/operatorapi/tenant_get.go index e12857d3b..e45dcecf4 100644 --- a/operatorapi/tenant_get.go +++ b/operatorapi/tenant_get.go @@ -20,20 +20,21 @@ import ( "context" "fmt" + errors "github.com/minio/console/restapi" + "github.com/minio/console/cluster" "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations/operator_api" - "github.com/minio/console/restapi" miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func getTenantDetailsResponse(session *models.Principal, params operator_api.TenantDetailsParams) (*models.Tenant, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } opClient := &operatorClient{ @@ -42,7 +43,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } info := getTenantInfo(minTenant) @@ -50,7 +51,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } k8sClient := k8sClient{ @@ -59,7 +60,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten tenantConfiguration, err := GetTenantConfiguration(ctx, &k8sClient, minTenant) if err != nil { - restapi.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err) + errors.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err) } // detect if AD/LDAP is enabled @@ -105,14 +106,14 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten //minio service minSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.MinIOCIServiceName(), metav1.GetOptions{}) if err != nil { - // we can tolerate this error - restapi.LogError("Unable to get MinIO service name: %v, continuing", err) + // we can tolerate this errors + errors.LogError("Unable to get MinIO service name: %v, continuing", err) } //console service conSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.ConsoleCIServiceName(), metav1.GetOptions{}) if err != nil { - // we can tolerate this error - restapi.LogError("Unable to get MinIO console service name: %v, continuing", err) + // we can tolerate this errors + errors.LogError("Unable to get MinIO console service name: %v, continuing", err) } schema := "http" diff --git a/operatorapi/tenant_update.go b/operatorapi/tenant_update.go index daba71228..f08b0d401 100644 --- a/operatorapi/tenant_update.go +++ b/operatorapi/tenant_update.go @@ -22,9 +22,12 @@ import ( "fmt" "strings" + "github.com/minio/console/restapi" + + "github.com/minio/console/pkg/http" + "github.com/minio/console/operatorapi/operations/operator_api" utils2 "github.com/minio/console/pkg/utils" - "github.com/minio/console/restapi" miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -32,7 +35,7 @@ import ( ) // updateTenantAction does an update on the minioTenant by patching the desired changes -func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl utils2.HTTPClientI, namespace string, params operator_api.UpdateTenantParams) error { +func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl http.ClientI, namespace string, params operator_api.UpdateTenantParams) error { imageToUpdate := params.Body.Image imageRegistryReq := params.Body.ImageRegistry diff --git a/operatorapi/tenants.go b/operatorapi/tenants.go index e99c6ae58..0fe9332ac 100644 --- a/operatorapi/tenants.go +++ b/operatorapi/tenants.go @@ -33,11 +33,11 @@ import ( "strings" "time" + utils2 "github.com/minio/console/pkg/http" + "github.com/dustin/go-humanize" "github.com/minio/madmin-go" - utils2 "github.com/minio/console/pkg/utils" - "github.com/minio/console/restapi" "github.com/minio/console/operatorapi/operations/operator_api" @@ -354,14 +354,16 @@ func registerTenantHandlers(api *operations.OperatorAPI) { // getDeleteTenantResponse gets the output of deleting a minio instance func getDeleteTenantResponse(session *models.Principal, params operator_api.DeleteTenantParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, @@ -373,12 +375,12 @@ func getDeleteTenantResponse(session *models.Principal, params operator_api.Dele tenant, err := opClient.TenantGet(params.HTTPRequest.Context(), params.Namespace, params.Tenant, metav1.GetOptions{}) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } tenant.EnsureDefaults() if err = deleteTenantAction(params.HTTPRequest.Context(), opClient, clientset.CoreV1(), tenant, deleteTenantPVCs); err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } return nil } @@ -396,7 +398,7 @@ func deleteTenantAction( err := operatorClient.TenantDelete(ctx, tenant.Namespace, tenant.Name, metav1.DeleteOptions{}) if err != nil { // try to delete pvc even if the tenant doesn't exist anymore but only if deletePvcs is set to true, - // else, we return the error + // else, we return the errors if (deletePvcs && !k8sErrors.IsNotFound(err)) || !deletePvcs { return err } @@ -440,19 +442,19 @@ func deleteTenantAction( // getDeleteTenantResponse gets the output of deleting a minio instance func getDeletePodResponse(session *models.Principal, params operator_api.DeletePodParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get Kubernetes Client clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } listOpts := metav1.ListOptions{ LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant), FieldSelector: fmt.Sprintf("metadata.name=%s%s", params.Tenant, params.PodName[len(params.Tenant):]), } if err = clientset.CoreV1().Pods(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } return nil } @@ -494,12 +496,12 @@ func getTenantCreds(ctx context.Context, client K8sClientI, tenant *miniov2.Tena tenantAccessKey, ok := tenantConfiguration["accesskey"] if !ok { restapi.LogError("tenant's secret doesn't contain accesskey") - return nil, restapi.ErrorGeneric + return nil, restapi.ErrDefault } tenantSecretKey, ok := tenantConfiguration["secretkey"] if !ok { restapi.LogError("tenant's secret doesn't contain secretkey") - return nil, restapi.ErrorGeneric + return nil, restapi.ErrDefault } return &tenantKeys{accessKey: tenantAccessKey, secretKey: tenantSecretKey}, nil } @@ -842,19 +844,19 @@ func updateTenantIdentityProvider(ctx context.Context, operatorClient OperatorCl func getTenantIdentityProviderResponse(session *models.Principal, params operator_api.TenantIdentityProviderParams) (*models.IdpConfiguration, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) @@ -862,27 +864,27 @@ func getTenantIdentityProviderResponse(session *models.Principal, params operato client: clientSet, } if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } info, err := getTenantIdentityProvider(ctx, &k8sClient, minTenant) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } return info, nil } func getUpdateTenantIdentityProviderResponse(session *models.Principal, params operator_api.UpdateTenantIdentityProviderParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } k8sClient := k8sClient{ client: clientSet, @@ -891,25 +893,25 @@ func getUpdateTenantIdentityProviderResponse(session *models.Principal, params o client: opClientClientSet, } if err := updateTenantIdentityProvider(ctx, opClient, &k8sClient, params.Namespace, params); err != nil { - return prepareError(err, errors.New("unable to update tenant")) + return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant")) } return nil } func getTenantSecurityResponse(session *models.Principal, params operator_api.TenantSecurityParams) (*models.TenantSecurityResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) @@ -917,27 +919,27 @@ func getTenantSecurityResponse(session *models.Principal, params operator_api.Te client: clientSet, } if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } info, err := getTenantSecurity(ctx, &k8sClient, minTenant) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } return info, nil } func getUpdateTenantSecurityResponse(session *models.Principal, params operator_api.UpdateTenantSecurityParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } k8sClient := k8sClient{ client: clientSet, @@ -946,7 +948,7 @@ func getUpdateTenantSecurityResponse(session *models.Principal, params operator_ client: opClientClientSet, } if err := updateTenantSecurity(ctx, opClient, &k8sClient, params.Namespace, params); err != nil { - return prepareError(err, errors.New("unable to update tenant")) + return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant")) } return nil } @@ -1117,36 +1119,36 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace } func getListAllTenantsResponse(session *models.Principal, params operator_api.ListAllTenantsParams) (*models.ListTenantsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } listT, err := listTenants(ctx, opClient, "", params.Limit) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } return listT, nil } // getListTenantsResponse list tenants by namespace func getListTenantsResponse(session *models.Principal, params operator_api.ListTenantsParams) (*models.ListTenantsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } listT, err := listTenants(ctx, opClient, params.Namespace, params.Limit) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } return listT, nil } @@ -1233,27 +1235,27 @@ func removeAnnotations(annotationsOne, annotationsTwo map[string]string) map[str } func getUpdateTenantResponse(session *models.Principal, params operator_api.UpdateTenantParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } - httpC := &utils2.HTTPClient{ + httpC := &utils2.Client{ Client: &http.Client{ Timeout: 4 * time.Second, }, } if err := updateTenantAction(ctx, opClient, clientSet.CoreV1(), httpC, params.Namespace, params); err != nil { - return prepareError(err, errors.New("unable to update tenant")) + return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant")) } return nil } @@ -1284,17 +1286,17 @@ func addTenantPool(ctx context.Context, operatorClient OperatorClientI, params o } func getTenantAddPoolResponse(session *models.Principal, params operator_api.TenantAddPoolParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ client: opClientClientSet, } if err := addTenantPool(ctx, opClient, params); err != nil { - return prepareError(err, errors.New("unable to add pool")) + return restapi.ErrorWithContext(ctx, err, errors.New("unable to add pool")) } return nil } @@ -1302,16 +1304,16 @@ func getTenantAddPoolResponse(session *models.Principal, params operator_api.Ten // getTenantUsageResponse returns the usage of a tenant func getTenantUsageResponse(session *models.Principal, params operator_api.GetTenantUsageParams) (*models.TenantUsage, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantUsage) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantUsage) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } opClient := &operatorClient{ @@ -1323,7 +1325,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantUsage) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } minTenant.EnsureDefaults() @@ -1336,7 +1338,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe svcURL, ) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantUsage) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } // create a minioClient interface implementation // defining the client to be used @@ -1344,7 +1346,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe // serialize output adminInfo, err := restapi.GetAdminInfo(ctx, adminClient) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantUsage) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } info := &models.TenantUsage{Used: adminInfo.Usage, DiskUsed: adminInfo.DisksUsage} return info, nil @@ -1353,12 +1355,12 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe // getTenantLogsResponse returns the logs of a tenant func getTenantLogsResponse(session *models.Principal, params operator_api.GetTenantLogsParams) (*models.TenantLogs, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantLogs) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs) } opClient := &operatorClient{ @@ -1367,7 +1369,7 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return nil, prepareError(err, errorUnableToGetTenantLogs) + return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs) } if minTenant.Spec.Log == nil { retval := &models.TenantLogs{ @@ -1450,12 +1452,12 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen // setTenantLogsResponse returns the logs of a tenant func setTenantLogsResponse(session *models.Principal, params operator_api.SetTenantLogsParams) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } opClient := &operatorClient{ @@ -1464,7 +1466,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } var labels = make(map[string]string) @@ -1493,7 +1495,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen if reflect.TypeOf(params.Data.LogCPURequest).Kind() == reflect.String && params.Data.LogCPURequest != "0Gi" && params.Data.LogCPURequest != "" { cpuQuantity, err := resource.ParseQuantity(params.Data.LogCPURequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } logResourceRequest["cpu"] = cpuQuantity minTenant.Spec.Log.Resources.Requests = logResourceRequest @@ -1501,7 +1503,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen if reflect.TypeOf(params.Data.LogMemRequest).Kind() == reflect.String { memQuantity, err := resource.ParseQuantity(params.Data.LogMemRequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } logResourceRequest["memory"] = memQuantity @@ -1538,7 +1540,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen if reflect.TypeOf(params.Data.LogDBCPURequest).Kind() == reflect.String && params.Data.LogDBCPURequest != "0Gi" && params.Data.LogDBCPURequest != "" { dbCPUQuantity, err := resource.ParseQuantity(params.Data.LogDBCPURequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } logDBResourceRequest["cpu"] = dbCPUQuantity minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest @@ -1546,7 +1548,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen if reflect.TypeOf(params.Data.LogDBMemRequest).Kind() == reflect.String { dbMemQuantity, err := resource.ParseQuantity(params.Data.LogDBMemRequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } logDBResourceRequest["memory"] = dbMemQuantity minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest @@ -1609,7 +1611,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen _, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{}) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } return true, nil } @@ -1617,12 +1619,12 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen // enableTenantLoggingResponse enables Tenant Logging func enableTenantLoggingResponse(session *models.Principal, params operator_api.EnableTenantLoggingParams) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } opClient := &operatorClient{ @@ -1631,7 +1633,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api. minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } minTenant.EnsureDefaults() @@ -1670,7 +1672,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api. _, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{}) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } return true, nil } @@ -1678,15 +1680,15 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api. // disableTenantLoggingResponse disables Tenant Logging func disableTenantLoggingResponse(session *models.Principal, params operator_api.DisableTenantLoggingParams) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } opClient := &operatorClient{ @@ -1695,31 +1697,31 @@ func disableTenantLoggingResponse(session *models.Principal, params operator_api minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } minTenant.EnsureDefaults() minTenant.Spec.Log = nil _, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{}) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } return true, nil } func getTenantPodsResponse(session *models.Principal, params operator_api.GetTenantPodsParams) ([]*models.TenantPod, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } listOpts := metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, params.Tenant), } pods, err := clientset.CoreV1().Pods(params.Namespace).List(ctx, listOpts) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } retval := []*models.TenantPod{} for _, pod := range pods.Items { @@ -1743,35 +1745,35 @@ func getTenantPodsResponse(session *models.Principal, params operator_api.GetTen } func getPodLogsResponse(session *models.Principal, params operator_api.GetPodLogsParams) (string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return "", prepareError(err) + return "", restapi.ErrorWithContext(ctx, err) } listOpts := &corev1.PodLogOptions{} logs := clientset.CoreV1().Pods(params.Namespace).GetLogs(params.PodName, listOpts) buff, err := logs.DoRaw(ctx) if err != nil { - return "", prepareError(err) + return "", restapi.ErrorWithContext(ctx, err) } return string(buff), nil } func getPodEventsResponse(session *models.Principal, params operator_api.GetPodEventsParams) (models.EventListWrapper, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } pod, err := clientset.CoreV1().Pods(params.Namespace).Get(ctx, params.PodName, metav1.GetOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", pod.UID)}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } retval := models.EventListWrapper{} for i := 0; i < len(events.Items); i++ { @@ -1791,12 +1793,12 @@ func getPodEventsResponse(session *models.Principal, params operator_api.GetPodE //get values for prometheus metrics func getTenantMonitoringResponse(session *models.Principal, params operator_api.GetTenantMonitoringParams) (*models.TenantMonitoringInfo, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ @@ -1805,7 +1807,7 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api. minInst, err := opClient.TenantGet(ctx, params.Namespace, params.Tenant, metav1.GetOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } monitoringInfo := &models.TenantMonitoringInfo{} @@ -1886,12 +1888,12 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api. //sets tenant Prometheus monitoring cofiguration fields to values provided func setTenantMonitoringResponse(session *models.Principal, params operator_api.SetTenantMonitoringParams) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } opClient := &operatorClient{ @@ -1900,7 +1902,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api. minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant) if err != nil { - return false, prepareError(err, errorUnableToGetTenantUsage) + return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } if params.Data.Toggle { @@ -1916,7 +1918,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api. } _, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{}) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } return true, nil } @@ -1944,7 +1946,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api. if params.Data.MonitoringCPURequest != "" { cpuQuantity, err := resource.ParseQuantity(params.Data.MonitoringCPURequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } monitoringResourceRequest["cpu"] = cpuQuantity } @@ -1952,7 +1954,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api. if params.Data.MonitoringMemRequest != "" { memQuantity, err := resource.ParseQuantity(params.Data.MonitoringMemRequest) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } monitoringResourceRequest["memory"] = memQuantity } @@ -1977,7 +1979,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api. minTenant.Spec.Prometheus.ServiceAccountName = params.Data.ServiceAccountName _, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{}) if err != nil { - return false, prepareError(err) + return false, restapi.ErrorWithContext(ctx, err) } return true, nil @@ -2422,11 +2424,11 @@ func parseNodeSelectorTerm(term *corev1.NodeSelectorTerm) *models.NodeSelectorTe } func getTenantUpdatePoolResponse(session *models.Principal, params operator_api.TenantUpdatePoolsParams) (*models.Tenant, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ @@ -2436,7 +2438,7 @@ func getTenantUpdatePoolResponse(session *models.Principal, params operator_api. t, err := updateTenantPools(ctx, opClient, params.Namespace, params.Tenant, params.Body.Pools) if err != nil { restapi.LogError("error updating Tenant's pools: %v", err) - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // parse it to models.Tenant @@ -2487,20 +2489,19 @@ func updateTenantPools( } func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLParams) (*models.TenantYAML, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() // get Kubernetes Client - opClient, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } - tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } // remove managed fields tenant.ManagedFields = []metav1.ManagedFieldsEntry{} - //yb, err := yaml.Marshal(tenant) j8sJSONSerializer := k8sJson.NewSerializerWithOptions( k8sJson.DefaultMetaFactory, nil, nil, @@ -2514,7 +2515,7 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP err = j8sJSONSerializer.Encode(tenant, buf) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } yb := buf.String() @@ -2523,6 +2524,8 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP } func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenantYAMLParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() // https://godoc.org/k8s.io/apimachinery/pkg/runtime#Scheme scheme := runtime.NewScheme() @@ -2540,12 +2543,12 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan // get Kubernetes Client opClient, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{}) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } upTenant := tenant.DeepCopy() // only update safe fields: spec, metadata.finalizers, metadata.labels and metadata.annotations @@ -2563,23 +2566,23 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan } func getTenantEventsResponse(session *models.Principal, params operator_api.GetTenantEventsParams) (models.EventListWrapper, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() client, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } tenant, err := client.MinioV2().Tenants(params.Namespace).Get(ctx, params.Tenant, metav1.GetOptions{}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", tenant.UID)}) if err != nil { - return nil, prepareError(err) + return nil, restapi.ErrorWithContext(ctx, err) } retval := models.EventListWrapper{} for _, event := range events.Items { @@ -2603,7 +2606,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd operatorCli, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } opClient := &operatorClient{ @@ -2613,7 +2616,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd err = updateTenantDomains(ctx, opClient, params.Namespace, params.Tenant, params.Body.Domains) if err != nil { - return prepareError(err) + return restapi.ErrorWithContext(ctx, err) } return nil diff --git a/operatorapi/tenants_helper.go b/operatorapi/tenants_helper.go index 616af379d..db6c27b89 100644 --- a/operatorapi/tenants_helper.go +++ b/operatorapi/tenants_helper.go @@ -27,6 +27,8 @@ import ( "strconv" "time" + xerrors "github.com/minio/console/restapi" + "github.com/minio/console/operatorapi/operations/operator_api" "errors" @@ -108,25 +110,25 @@ func tenantUpdateCertificates(ctx context.Context, operatorClient OperatorClient // getTenantUpdateCertificatesResponse wrapper of tenantUpdateCertificates func getTenantUpdateCertificatesResponse(session *models.Principal, params operator_api.TenantUpdateCertificateParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err, errorUnableToUpdateTenantCertificates) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates) } k8sClient := k8sClient{ client: clientSet, } opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err, errorUnableToUpdateTenantCertificates) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates) } opClient := operatorClient{ client: opClientClientSet, } if err := tenantUpdateCertificates(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil { - return prepareError(err, errorUnableToUpdateTenantCertificates) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates) } return nil } @@ -239,42 +241,42 @@ func tenantUpdateEncryption(ctx context.Context, operatorClient OperatorClientI, // getTenantDeleteEncryptionResponse is a wrapper for tenantDeleteEncryption func getTenantDeleteEncryptionResponse(session *models.Principal, params operator_api.TenantDeleteEncryptionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err, errorDeletingEncryptionConfig) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig) } opClient := operatorClient{ client: opClientClientSet, } if err := tenantDeleteEncryption(ctx, &opClient, params.Namespace, params); err != nil { - return prepareError(err, errorDeletingEncryptionConfig) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig) } return nil } // getTenantUpdateEncryptionResponse is a wrapper for tenantUpdateEncryption func getTenantUpdateEncryptionResponse(session *models.Principal, params operator_api.TenantUpdateEncryptionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err, errorUpdatingEncryptionConfig) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig) } k8sClient := k8sClient{ client: clientSet, } opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return prepareError(err, errorUpdatingEncryptionConfig) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig) } opClient := operatorClient{ client: opClientClientSet, } if err := tenantUpdateEncryption(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil { - return prepareError(err, errorUpdatingEncryptionConfig) + return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig) } return nil } @@ -453,26 +455,26 @@ func tenantEncryptionInfo(ctx context.Context, operatorClient OperatorClientI, c // getTenantEncryptionResponse is a wrapper for tenantEncryptionInfo func getTenantEncryptionInfoResponse(session *models.Principal, params operator_api.TenantEncryptionInfoParams) (*models.EncryptionConfigurationResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get Kubernetes Client clientSet, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorEncryptionConfigNotFound) + return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound) } k8sClient := k8sClient{ client: clientSet, } opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorEncryptionConfigNotFound) + return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound) } opClient := operatorClient{ client: opClientClientSet, } configuration, err := tenantEncryptionInfo(ctx, &opClient, &k8sClient, params.Namespace, params) if err != nil { - return nil, prepareError(err, errorEncryptionConfigNotFound) + return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound) } return configuration, nil } @@ -541,8 +543,8 @@ func createOrReplaceSecrets(ctx context.Context, clientSet K8sClientI, ns string // delete secret with same name if exists err := clientSet.deleteSecret(ctx, ns, secret.Name, metav1.DeleteOptions{}) if err != nil { - // log the error if any and continue - LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err) + // log the errors if any and continue + xerrors.LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err) } k8sSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -579,8 +581,8 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient // delete secret with same name if exists err := clientSet.deleteSecret(ctx, ns, keyPairSecretName, metav1.DeleteOptions{}) if err != nil { - // log the error if any and continue - LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err) + // log the errors if any and continue + xerrors.LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err) } imm := true tlsCrt, err := base64.StdEncoding.DecodeString(*keyPair.Crt) @@ -625,13 +627,13 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient func createOrReplaceKesConfigurationSecrets(ctx context.Context, clientSet K8sClientI, ns string, encryptionCfg *models.EncryptionConfiguration, kesConfigurationSecretName, kesClientCertSecretName, tenantName string) (*corev1.LocalObjectReference, *miniov2.LocalCertificateReference, error) { // delete KES configuration secret if exists if err := clientSet.deleteSecret(ctx, ns, kesConfigurationSecretName, metav1.DeleteOptions{}); err != nil { - // log the error if any and continue - LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err) + // log the errors if any and continue + xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err) } // delete KES client cert secret if exists if err := clientSet.deleteSecret(ctx, ns, kesClientCertSecretName, metav1.DeleteOptions{}); err != nil { - // log the error if any and continue - LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err) + // log the errors if any and continue + xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err) } // if autoCert is enabled then Operator will generate the client certificates, calculate the client cert identity // and pass it to KES via the ${MINIO_KES_IDENTITY} variable diff --git a/operatorapi/tenants_test.go b/operatorapi/tenants_test.go index 7be351040..332e917f3 100644 --- a/operatorapi/tenants_test.go +++ b/operatorapi/tenants_test.go @@ -28,7 +28,7 @@ import ( "testing" "time" - "github.com/minio/console/pkg/utils" + xhttp "github.com/minio/console/pkg/http" "github.com/minio/console/operatorapi/operations/operator_api" @@ -897,7 +897,7 @@ func Test_UpdateTenantAction(t *testing.T) { type args struct { ctx context.Context operatorClient OperatorClientI - httpCl utils.HTTPClientI + httpCl xhttp.ClientI nameSpace string tenantName string mockTenantPatch func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*miniov2.Tenant, error) diff --git a/operatorapi/version.go b/operatorapi/version.go index 0e9c027be..31339eb21 100644 --- a/operatorapi/version.go +++ b/operatorapi/version.go @@ -17,20 +17,24 @@ package operatorapi import ( + "context" + "net/http" + "time" + + errors "github.com/minio/console/restapi" + + xhttp "github.com/minio/console/pkg/http" + "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations" "github.com/minio/console/operatorapi/operations/user_api" - - "net/http" - "time" - "github.com/minio/console/pkg/utils" ) func registerVersionHandlers(api *operations.OperatorAPI) { api.UserAPICheckMinIOVersionHandler = user_api.CheckMinIOVersionHandlerFunc(func(params user_api.CheckMinIOVersionParams) middleware.Responder { - versionResponse, err := getVersionResponse() + versionResponse, err := getVersionResponse(params) if err != nil { return user_api.NewCheckMinIOVersionDefault(int(err.Code)).WithPayload(err) } @@ -39,13 +43,15 @@ func registerVersionHandlers(api *operations.OperatorAPI) { } // getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI -func getVersionResponse() (*models.CheckOperatorVersionResponse, *models.Error) { - ver, err := utils.GetLatestMinIOImage(&utils.HTTPClient{ +func getVersionResponse(params user_api.CheckMinIOVersionParams) (*models.CheckOperatorVersionResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + ver, err := utils.GetLatestMinIOImage(&xhttp.Client{ Client: &http.Client{ Timeout: 15 * time.Second, }}) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } return &models.CheckOperatorVersionResponse{ LatestVersion: *ver, diff --git a/operatorapi/volumes.go b/operatorapi/volumes.go index 22e727536..d281fb3be 100644 --- a/operatorapi/volumes.go +++ b/operatorapi/volumes.go @@ -21,6 +21,8 @@ import ( "fmt" "sort" + errors "github.com/minio/console/restapi" + miniov1 "github.com/minio/operator/pkg/apis/minio.min.io/v1" "github.com/go-openapi/runtime/middleware" @@ -33,7 +35,7 @@ import ( func registerVolumesHandlers(api *operations.OperatorAPI) { api.OperatorAPIListPVCsHandler = operator_api.ListPVCsHandlerFunc(func(params operator_api.ListPVCsParams, session *models.Principal) middleware.Responder { - payload, err := getPVCsResponse(session) + payload, err := getPVCsResponse(session, params) if err != nil { return operator_api.NewListPVCsDefault(int(err.Code)).WithPayload(err) @@ -72,13 +74,13 @@ func registerVolumesHandlers(api *operations.OperatorAPI) { } -func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getPVCsResponse(session *models.Principal, params operator_api.ListPVCsParams) (*models.ListPVCsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } // Filter Tenant PVCs. They keep their v1 tenant annotation @@ -90,7 +92,7 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims("").List(ctx, listOpts) if err2 != nil { - return nil, prepareError(err2) + return nil, errors.ErrorWithContext(ctx, err2) } var ListPVCs []*models.PvcsListResponse @@ -121,12 +123,12 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode } func getPVCsForTenantResponse(session *models.Principal, params operator_api.ListPVCsForTenantParams) (*models.ListPVCsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } // Filter Tenant PVCs. They keep their v1 tenant annotation @@ -138,7 +140,7 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).List(ctx, listOpts) if err2 != nil { - return nil, prepareError(err2) + return nil, errors.ErrorWithContext(ctx, err2) } var ListPVCs []*models.PvcsListResponse @@ -169,37 +171,37 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis } func getDeletePVCResponse(session *models.Principal, params operator_api.DeletePVCParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get Kubernetes Client clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return prepareError(err) + return errors.ErrorWithContext(ctx, err) } listOpts := metav1.ListOptions{ LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant), FieldSelector: fmt.Sprintf("metadata.name=%s", params.PVCName), } if err = clientset.CoreV1().PersistentVolumeClaims(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil { - return prepareError(err) + return errors.ErrorWithContext(ctx, err) } return nil } func getPVCEventsResponse(session *models.Principal, params operator_api.GetPVCEventsParams) (models.EventListWrapper, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } PVC, err := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).Get(ctx, params.PVCName, metav1.GetOptions{}) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", PVC.UID)}) if err != nil { - return nil, prepareError(err) + return nil, errors.ErrorWithContext(ctx, err) } retval := models.EventListWrapper{} for i := 0; i < len(events.Items); i++ { diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 1cda95da1..5ca4d9448 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -89,11 +89,14 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) { if token == "" { return nil, ErrNoAuthToken } - // decrypt encrypted token - claimTokens, err := decryptClaims(token) + decryptedToken, err := DecryptToken(token) if err != nil { - // we print decryption token error information for debugging purposes - // we return a generic error that doesn't give any information to attackers + // fail decrypting token + return nil, errReadingToken + } + claimTokens, err := ParseClaimsFromToken(string(decryptedToken)) + if err != nil { + // fail unmarshalling token into data structure return nil, errReadingToken } // claimsTokens contains the decrypted JWT for Console @@ -136,21 +139,26 @@ func encryptClaims(credentials *TokenClaims) (string, error) { return base64.StdEncoding.EncodeToString(ciphertext), nil } -// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *TokenClaims object -func decryptClaims(ciphertext string) (*TokenClaims, error) { +// ParseClaimsFromToken receive token claims in string format, then unmarshal them to produce a *TokenClaims object +func ParseClaimsFromToken(claims string) (*TokenClaims, error) { + tokenClaims := &TokenClaims{} + if err := json.Unmarshal([]byte(claims), tokenClaims); err != nil { + return nil, err + } + return tokenClaims, nil +} + +// DecryptToken receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces []byte +func DecryptToken(ciphertext string) (plaintext []byte, err error) { decoded, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, err } - plaintext, err := decrypt(decoded, []byte{}) + plaintext, err = decrypt(decoded, []byte{}) if err != nil { return nil, err } - tokenClaims := &TokenClaims{} - if err = json.Unmarshal(plaintext, tokenClaims); err != nil { - return nil, err - } - return tokenClaims, nil + return plaintext, nil } const ( diff --git a/pkg/certs/certs.go b/pkg/certs/certs.go index 858275df5..7f8cbb607 100644 --- a/pkg/certs/certs.go +++ b/pkg/certs/certs.go @@ -332,3 +332,12 @@ func GetAllCertificatesAndCAs() (*x509.CertPool, []*x509.Certificate, *xcerts.Ma } return rootCAs, publicCerts, certsManager, nil } + +// EnsureCertAndKey checks if both client certificate and key paths are provided +func EnsureCertAndKey(clientCert, clientKey string) error { + if (clientCert != "" && clientKey == "") || + (clientCert == "" && clientKey != "") { + return errors.New("cert and key must be specified as a pair") + } + return nil +} diff --git a/pkg/http/headers.go b/pkg/http/headers.go new file mode 100644 index 000000000..e864edbc3 --- /dev/null +++ b/pkg/http/headers.go @@ -0,0 +1,23 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package http + +// Standard S3 HTTP response constants +const ( + ETag = "ETag" + ContentType = "Content-Type" +) diff --git a/pkg/utils/http_client.go b/pkg/http/http.go similarity index 52% rename from pkg/utils/http_client.go rename to pkg/http/http.go index 4dff79349..84cb3ad19 100644 --- a/pkg/utils/http_client.go +++ b/pkg/http/http.go @@ -14,40 +14,61 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package utils +package http import ( "io" + "io/ioutil" "net/http" ) -// HTTPClientI interface with all functions to be implemented +// ClientI interface with all functions to be implemented // by mock when testing, it should include all HttpClient respective api calls // that are used within this project. -type HTTPClientI interface { +type ClientI interface { Get(url string) (resp *http.Response, err error) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) Do(req *http.Request) (*http.Response, error) } -// HTTPClient Interface implementation +// Client is an HTTP Interface implementation // // Define the structure of a http client and define the functions that are actually used -type HTTPClient struct { +type Client struct { Client *http.Client } // Get implements http.Client.Get() -func (c *HTTPClient) Get(url string) (resp *http.Response, err error) { +func (c *Client) Get(url string) (resp *http.Response, err error) { return c.Client.Get(url) } // Post implements http.Client.Post() -func (c *HTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { +func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { return c.Client.Post(url, contentType, body) } // Do implement http.Client.Do() -func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) { +func (c *Client) Do(req *http.Request) (*http.Response, error) { return c.Client.Do(req) } + +// DrainBody close non nil response with any response Body. +// convenient wrapper to drain any remaining data on response body. +// +// Subsequently this allows golang http RoundTripper +// to re-use the same connection for future requests. +func DrainBody(respBody io.ReadCloser) { + // Callers should close resp.Body when done reading from it. + // If resp.Body is not closed, the Client's underlying RoundTripper + // (typically Transport) may not be able to re-use a persistent TCP + // connection to the server for a subsequent "keep-alive" request. + if respBody != nil { + // Drain any remaining Body and then close the connection. + // Without this closing connection would disallow re-using + // the same connection for future uses. + // - http://stackoverflow.com/a/17961593/4465767 + defer respBody.Close() + io.Copy(ioutil.Discard, respBody) + } +} diff --git a/pkg/logger/audit.go b/pkg/logger/audit.go new file mode 100644 index 000000000..a5e07380d --- /dev/null +++ b/pkg/logger/audit.go @@ -0,0 +1,228 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strconv" + "sync/atomic" + "time" + + "github.com/minio/console/pkg/utils" + + "github.com/minio/console/pkg/logger/message/audit" +) + +// ResponseWriter - is a wrapper to trap the http response status code. +type ResponseWriter struct { + http.ResponseWriter + StatusCode int + // Log body of 4xx or 5xx responses + LogErrBody bool + // Log body of all responses + LogAllBody bool + + TimeToFirstByte time.Duration + StartTime time.Time + // number of bytes written + bytesWritten int + // Internal recording buffer + headers bytes.Buffer + body bytes.Buffer + // Indicate if headers are written in the log + headersLogged bool +} + +// NewResponseWriter - returns a wrapped response writer to trap +// http status codes for auditing purposes. +func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { + return &ResponseWriter{ + ResponseWriter: w, + StatusCode: http.StatusOK, + StartTime: time.Now().UTC(), + } +} + +func (lrw *ResponseWriter) Write(p []byte) (int, error) { + if !lrw.headersLogged { + // We assume the response code to be '200 OK' when WriteHeader() is not called, + // that way following Golang HTTP response behavior. + lrw.WriteHeader(http.StatusOK) + } + n, err := lrw.ResponseWriter.Write(p) + lrw.bytesWritten += n + if lrw.TimeToFirstByte == 0 { + lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime) + } + if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { + // Always logging error responses. + lrw.body.Write(p) + } + if err != nil { + return n, err + } + return n, err +} + +// Write the headers into the given buffer +func (lrw *ResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) { + n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode)) + lrw.bytesWritten += n + for k, v := range headers { + n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0]) + lrw.bytesWritten += n + } +} + +// BodyPlaceHolder returns a dummy body placeholder +var BodyPlaceHolder = []byte("") + +// Body - Return response body. +func (lrw *ResponseWriter) Body() []byte { + // If there was an error response or body logging is enabled + // then we return the body contents + if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody { + return lrw.body.Bytes() + } + // ... otherwise we return the place holder + return BodyPlaceHolder +} + +// WriteHeader - writes http status code +func (lrw *ResponseWriter) WriteHeader(code int) { + if !lrw.headersLogged { + lrw.StatusCode = code + lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header()) + lrw.headersLogged = true + lrw.ResponseWriter.WriteHeader(code) + } +} + +// Flush - Calls the underlying Flush. +func (lrw *ResponseWriter) Flush() { + lrw.ResponseWriter.(http.Flusher).Flush() +} + +// Size - reutrns the number of bytes written +func (lrw *ResponseWriter) Size() int { + return lrw.bytesWritten +} + +// 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, utils.ContextAuditKey, audit) +} + +// GetAuditEntry returns Audit entry if set. +func GetAuditEntry(ctx context.Context) *audit.Entry { + if ctx != nil { + r, ok := ctx.Value(utils.ContextAuditKey).(*audit.Entry) + if ok { + return r + } + r = &audit.Entry{ + Version: audit.Version, + //DeploymentID: globalDeploymentID, + Time: time.Now().UTC(), + } + SetAuditEntry(ctx, r) + return r + } + return nil +} + +// AuditLog - logs audit logs to all audit targets. +func AuditLog(ctx context.Context, w *ResponseWriter, r *http.Request, reqClaims map[string]interface{}, filterKeys ...string) { + // Fast exit if there is not audit target configured + if atomic.LoadInt32(&nAuditTargets) == 0 { + return + } + + var entry audit.Entry + + if w != nil && r != nil { + reqInfo := GetReqInfo(ctx) + if reqInfo == nil { + return + } + entry = audit.ToEntry(w, r, reqClaims, GetGlobalDeploymentID()) + // indicates all requests for this API call are inbound + entry.Trigger = "incoming" + + 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 + outputBytes int64 = -1 // -1: unknown output bytes + ) + + if w != nil { + statusCode = w.StatusCode + timeToResponse = time.Now().UTC().Sub(w.StartTime) + timeToFirstByte = w.TimeToFirstByte + outputBytes = int64(w.Size()) + } + + entry.API.Path = r.URL.Path + + entry.API.Status = http.StatusText(statusCode) + entry.API.StatusCode = statusCode + entry.API.Method = r.Method + entry.API.InputBytes = r.ContentLength + entry.API.OutputBytes = outputBytes + entry.RequestID = reqInfo.RequestID + + 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 + } + } + + if anonFlag { + entry.SessionID = hashString(entry.SessionID) + entry.RemoteHost = hashString(entry.RemoteHost) + } + + // Send audit logs only to http targets. + for _, t := range AuditTargets() { + if err := t.Send(entry, string(All)); err != nil { + LogAlwaysIf(context.Background(), fmt.Errorf("event(%v) was not sent to Audit target (%v): %v", entry, t, err), All) + } + } +} diff --git a/pkg/logger/color/color.go b/pkg/logger/color/color.go new file mode 100644 index 000000000..7c1d60c85 --- /dev/null +++ b/pkg/logger/color/color.go @@ -0,0 +1,60 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package color + +import ( + "fmt" + + "github.com/fatih/color" +) + +// global colors. +var ( + // Check if we stderr, stdout are dumb terminals, we do not apply + // ansi coloring on dumb terminals. + IsTerminal = func() bool { + return !color.NoColor + } + + Bold = func() func(a ...interface{}) string { + if IsTerminal() { + return color.New(color.Bold).SprintFunc() + } + return fmt.Sprint + }() + + FgRed = func() func(a ...interface{}) string { + if IsTerminal() { + return color.New(color.FgRed).SprintFunc() + } + return fmt.Sprint + }() + + BgRed = func() func(format string, a ...interface{}) string { + if IsTerminal() { + return color.New(color.BgRed).SprintfFunc() + } + return fmt.Sprintf + }() + + FgWhite = func() func(format string, a ...interface{}) string { + if IsTerminal() { + return color.New(color.FgWhite).SprintfFunc() + } + return fmt.Sprintf + }() +) diff --git a/pkg/logger/config.go b/pkg/logger/config.go new file mode 100644 index 000000000..4da4c0be5 --- /dev/null +++ b/pkg/logger/config.go @@ -0,0 +1,213 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "errors" + "strconv" + "strings" + + "github.com/google/uuid" + + "github.com/minio/console/pkg/logger/config" + "github.com/minio/console/pkg/logger/target/http" + "github.com/minio/pkg/env" +) + +// NewConfig - initialize new logger config. +func NewConfig() Config { + cfg := Config{ + HTTP: make(map[string]http.Config), + AuditWebhook: make(map[string]http.Config), + } + + return cfg +} + +func lookupLoggerWebhookConfig() (Config, error) { + cfg := NewConfig() + envs := env.List(EnvLoggerWebhookEndpoint) + var loggerTargets []string + for _, k := range envs { + target := strings.TrimPrefix(k, EnvLoggerWebhookEndpoint+config.Default) + if target == EnvLoggerWebhookEndpoint { + target = config.Default + } + loggerTargets = append(loggerTargets, target) + } + + // Load HTTP logger from the environment if found + for _, target := range loggerTargets { + if v, ok := cfg.HTTP[target]; ok && v.Enabled { + // This target is already enabled using the + // legacy environment variables, ignore. + continue + } + enableEnv := EnvLoggerWebhookEnable + if target != config.Default { + enableEnv = EnvLoggerWebhookEnable + config.Default + target + } + enable, err := config.ParseBool(env.Get(enableEnv, "")) + if err != nil || !enable { + continue + } + endpointEnv := EnvLoggerWebhookEndpoint + if target != config.Default { + endpointEnv = EnvLoggerWebhookEndpoint + config.Default + target + } + authTokenEnv := EnvLoggerWebhookAuthToken + if target != config.Default { + authTokenEnv = EnvLoggerWebhookAuthToken + config.Default + target + } + clientCertEnv := EnvLoggerWebhookClientCert + if target != config.Default { + clientCertEnv = EnvLoggerWebhookClientCert + config.Default + target + } + clientKeyEnv := EnvLoggerWebhookClientKey + if target != config.Default { + clientKeyEnv = EnvLoggerWebhookClientKey + config.Default + target + } + err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, "")) + if err != nil { + return cfg, err + } + queueSizeEnv := EnvLoggerWebhookQueueSize + if target != config.Default { + queueSizeEnv = EnvLoggerWebhookQueueSize + config.Default + target + } + queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000")) + if err != nil { + return cfg, err + } + if queueSize <= 0 { + return cfg, errors.New("invalid queue_size value") + } + cfg.HTTP[target] = http.Config{ + Enabled: true, + Endpoint: env.Get(endpointEnv, ""), + AuthToken: env.Get(authTokenEnv, ""), + ClientCert: env.Get(clientCertEnv, ""), + ClientKey: env.Get(clientKeyEnv, ""), + QueueSize: queueSize, + } + } + + return cfg, nil +} + +func lookupAuditWebhookConfig() (Config, error) { + cfg := NewConfig() + var loggerAuditTargets []string + envs := env.List(EnvAuditWebhookEndpoint) + for _, k := range envs { + target := strings.TrimPrefix(k, EnvAuditWebhookEndpoint+config.Default) + if target == EnvAuditWebhookEndpoint { + target = config.Default + } + loggerAuditTargets = append(loggerAuditTargets, target) + } + + for _, target := range loggerAuditTargets { + if v, ok := cfg.AuditWebhook[target]; ok && v.Enabled { + // This target is already enabled using the + // legacy environment variables, ignore. + continue + } + enableEnv := EnvAuditWebhookEnable + if target != config.Default { + enableEnv = EnvAuditWebhookEnable + config.Default + target + } + enable, err := config.ParseBool(env.Get(enableEnv, "")) + if err != nil || !enable { + continue + } + endpointEnv := EnvAuditWebhookEndpoint + if target != config.Default { + endpointEnv = EnvAuditWebhookEndpoint + config.Default + target + } + authTokenEnv := EnvAuditWebhookAuthToken + if target != config.Default { + authTokenEnv = EnvAuditWebhookAuthToken + config.Default + target + } + clientCertEnv := EnvAuditWebhookClientCert + if target != config.Default { + clientCertEnv = EnvAuditWebhookClientCert + config.Default + target + } + clientKeyEnv := EnvAuditWebhookClientKey + if target != config.Default { + clientKeyEnv = EnvAuditWebhookClientKey + config.Default + target + } + err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, "")) + if err != nil { + return cfg, err + } + queueSizeEnv := EnvAuditWebhookQueueSize + if target != config.Default { + queueSizeEnv = EnvAuditWebhookQueueSize + config.Default + target + } + queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000")) + if err != nil { + return cfg, err + } + if queueSize <= 0 { + return cfg, errors.New("invalid queue_size value") + } + cfg.AuditWebhook[target] = http.Config{ + Enabled: true, + Endpoint: env.Get(endpointEnv, ""), + AuthToken: env.Get(authTokenEnv, ""), + ClientCert: env.Get(clientCertEnv, ""), + ClientKey: env.Get(clientKeyEnv, ""), + QueueSize: queueSize, + } + } + + return cfg, nil +} + +// LookupConfigForSubSys - lookup logger config, override with ENVs if set, for the given sub-system +func LookupConfigForSubSys(subSys string) (cfg Config, err error) { + switch subSys { + case config.LoggerWebhookSubSys: + if cfg, err = lookupLoggerWebhookConfig(); err != nil { + return cfg, err + } + case config.AuditWebhookSubSys: + if cfg, err = lookupAuditWebhookConfig(); err != nil { + return cfg, err + } + } + return cfg, nil +} + +// GetGlobalDeploymentID : +func GetGlobalDeploymentID() string { + if globalDeploymentID != "" { + return globalDeploymentID + } + globalDeploymentID = env.Get(EnvGlobalDeploymentID, mustGetUUID()) + return globalDeploymentID +} + +// mustGetUUID - get a random UUID. +func mustGetUUID() string { + u, err := uuid.NewRandom() + if err != nil { + CriticalIf(GlobalContext, err) + } + return u.String() +} diff --git a/pkg/logger/config/bool-flag.go b/pkg/logger/config/bool-flag.go new file mode 100644 index 000000000..6f9eb96ab --- /dev/null +++ b/pkg/logger/config/bool-flag.go @@ -0,0 +1,80 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" +) + +// BoolFlag - wrapper bool type. +type BoolFlag bool + +// String - returns string of BoolFlag. +func (bf BoolFlag) String() string { + if bf { + return "on" + } + + return "off" +} + +// MarshalJSON - converts BoolFlag into JSON data. +func (bf BoolFlag) MarshalJSON() ([]byte, error) { + return json.Marshal(bf.String()) +} + +// UnmarshalJSON - parses given data into BoolFlag. +func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) { + var s string + if err = json.Unmarshal(data, &s); err == nil { + b := BoolFlag(true) + if s == "" { + // Empty string is treated as valid. + *bf = b + } else if b, err = ParseBoolFlag(s); err == nil { + *bf = b + } + } + + return err +} + +// ParseBool returns the boolean value represented by the string. +func ParseBool(str string) (bool, error) { + switch str { + case "1", "t", "T", "true", "TRUE", "True", "on", "ON", "On": + return true, nil + case "0", "f", "F", "false", "FALSE", "False", "off", "OFF", "Off": + return false, nil + } + if strings.EqualFold(str, "enabled") { + return true, nil + } + if strings.EqualFold(str, "disabled") { + return false, nil + } + return false, fmt.Errorf("ParseBool: parsing '%s': %w", str, strconv.ErrSyntax) +} + +// ParseBoolFlag - parses string into BoolFlag. +func ParseBoolFlag(s string) (bf BoolFlag, err error) { + b, err := ParseBool(s) + return BoolFlag(b), err +} diff --git a/pkg/logger/config/bool-flag_test.go b/pkg/logger/config/bool-flag_test.go new file mode 100644 index 000000000..d850f1ebe --- /dev/null +++ b/pkg/logger/config/bool-flag_test.go @@ -0,0 +1,126 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import "testing" + +// Test BoolFlag.String() +func TestBoolFlagString(t *testing.T) { + var bf BoolFlag + + testCases := []struct { + flag BoolFlag + expectedResult string + }{ + {bf, "off"}, + {BoolFlag(true), "on"}, + {BoolFlag(false), "off"}, + } + + for _, testCase := range testCases { + str := testCase.flag.String() + if testCase.expectedResult != str { + t.Fatalf("expected: %v, got: %v", testCase.expectedResult, str) + } + } +} + +// Test BoolFlag.MarshalJSON() +func TestBoolFlagMarshalJSON(t *testing.T) { + var bf BoolFlag + + testCases := []struct { + flag BoolFlag + expectedResult string + }{ + {bf, `"off"`}, + {BoolFlag(true), `"on"`}, + {BoolFlag(false), `"off"`}, + } + + for _, testCase := range testCases { + data, _ := testCase.flag.MarshalJSON() + if testCase.expectedResult != string(data) { + t.Fatalf("expected: %v, got: %v", testCase.expectedResult, string(data)) + } + } +} + +// Test BoolFlag.UnmarshalJSON() +func TestBoolFlagUnmarshalJSON(t *testing.T) { + testCases := []struct { + data []byte + expectedResult BoolFlag + expectedErr bool + }{ + {[]byte(`{}`), BoolFlag(false), true}, + {[]byte(`["on"]`), BoolFlag(false), true}, + {[]byte(`"junk"`), BoolFlag(false), true}, + {[]byte(`""`), BoolFlag(true), false}, + {[]byte(`"on"`), BoolFlag(true), false}, + {[]byte(`"off"`), BoolFlag(false), false}, + {[]byte(`"true"`), BoolFlag(true), false}, + {[]byte(`"false"`), BoolFlag(false), false}, + {[]byte(`"ON"`), BoolFlag(true), false}, + {[]byte(`"OFF"`), BoolFlag(false), false}, + } + + for _, testCase := range testCases { + var flag BoolFlag + err := (&flag).UnmarshalJSON(testCase.data) + if !testCase.expectedErr && err != nil { + t.Fatalf("error: expected = , got = %v", err) + } + if testCase.expectedErr && err == nil { + t.Fatalf("error: expected error, got = ") + } + if err == nil && testCase.expectedResult != flag { + t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, flag) + } + } +} + +// Test ParseBoolFlag() +func TestParseBoolFlag(t *testing.T) { + testCases := []struct { + flagStr string + expectedResult BoolFlag + expectedErr bool + }{ + {"", BoolFlag(false), true}, + {"junk", BoolFlag(false), true}, + {"true", BoolFlag(true), false}, + {"false", BoolFlag(false), false}, + {"ON", BoolFlag(true), false}, + {"OFF", BoolFlag(false), false}, + {"on", BoolFlag(true), false}, + {"off", BoolFlag(false), false}, + } + + for _, testCase := range testCases { + bf, err := ParseBoolFlag(testCase.flagStr) + if !testCase.expectedErr && err != nil { + t.Fatalf("error: expected = , got = %v", err) + } + if testCase.expectedErr && err == nil { + t.Fatalf("error: expected error, got = ") + } + if err == nil && testCase.expectedResult != bf { + t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, bf) + } + } +} diff --git a/pkg/logger/config/certs.go b/pkg/logger/config/certs.go new file mode 100644 index 000000000..3221c9ac9 --- /dev/null +++ b/pkg/logger/config/certs.go @@ -0,0 +1,30 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "errors" +) + +// EnsureCertAndKey checks if both client certificate and key paths are provided +func EnsureCertAndKey(clientCert, clientKey string) error { + if (clientCert != "" && clientKey == "") || + (clientCert == "" && clientKey != "") { + return errors.New("cert and key must be specified as a pair") + } + return nil +} diff --git a/pkg/logger/config/config.go b/pkg/logger/config/config.go new file mode 100644 index 000000000..a862dc374 --- /dev/null +++ b/pkg/logger/config/config.go @@ -0,0 +1,34 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "github.com/minio/madmin-go" +) + +// Default keys +const ( + Default = madmin.Default + Enable = madmin.EnableKey + License = "license" // Deprecated Dec 2021 +) + +// Top level config constants. +const ( + LoggerWebhookSubSys = "logger_webhook" + AuditWebhookSubSys = "audit_webhook" +) diff --git a/pkg/logger/console.go b/pkg/logger/console.go new file mode 100644 index 000000000..ee9c44caf --- /dev/null +++ b/pkg/logger/console.go @@ -0,0 +1,223 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/minio/console/pkg/logger/color" + "github.com/minio/console/pkg/logger/message/log" + c "github.com/minio/pkg/console" +) + +// ConsoleLoggerTgt is a stringified value to represent console logging +const ConsoleLoggerTgt = "console+http" + +// Logger interface describes the methods that need to be implemented to satisfy the interface requirements. +type Logger interface { + json(msg string, args ...interface{}) + quiet(msg string, args ...interface{}) + pretty(msg string, args ...interface{}) +} + +func consoleLog(console Logger, msg string, args ...interface{}) { + switch { + case jsonFlag: + // Strip escape control characters from json message + msg = ansiRE.ReplaceAllLiteralString(msg, "") + console.json(msg, args...) + case quietFlag: + console.quiet(msg+"\n", args...) + default: + console.pretty(msg+"\n", args...) + } +} + +// Fatal prints only fatal errors message with no stack trace +// it will be called for input validation failures +func Fatal(err error, msg string, data ...interface{}) { + fatal(err, msg, data...) +} + +func fatal(err error, msg string, data ...interface{}) { + var errMsg string + if msg != "" { + errMsg = errorFmtFunc(fmt.Sprintf(msg, data...), err, jsonFlag) + } else { + errMsg = err.Error() + } + consoleLog(fatalMessage, errMsg) +} + +var fatalMessage fatalMsg + +type fatalMsg struct{} + +func (f fatalMsg) json(msg string, args ...interface{}) { + var message string + if msg != "" { + message = fmt.Sprintf(msg, args...) + } else { + message = fmt.Sprint(args...) + } + logJSON, err := json.Marshal(&log.Entry{ + Level: FatalLvl.String(), + Message: message, + Time: time.Now().UTC(), + Trace: &log.Trace{Message: message, Source: []string{getSource(6)}}, + }) + if err != nil { + panic(err) + } + fmt.Println(string(logJSON)) + + os.Exit(1) +} + +func (f fatalMsg) quiet(msg string, args ...interface{}) { + f.pretty(msg, args...) +} + +var ( + logTag = "ERROR" + logBanner = color.BgRed(color.FgWhite(color.Bold(logTag))) + " " + emptyBanner = color.BgRed(strings.Repeat(" ", len(logTag))) + " " + bannerWidth = len(logTag) + 1 +) + +func (f fatalMsg) pretty(msg string, args ...interface{}) { + // Build the passed errors message + errMsg := fmt.Sprintf(msg, args...) + + tagPrinted := false + + // Print the errors message: the following code takes care + // of splitting errors text and always pretty printing the + // red banner along with the errors message. Since the errors + // message itself contains some colored text, we needed + // to use some ANSI control escapes to cursor color state + // and freely move in the screen. + for _, line := range strings.Split(errMsg, "\n") { + if len(line) == 0 { + // No more text to print, just quit. + break + } + + for { + // Save the attributes of the current cursor helps + // us save the text color of the passed errors message + ansiSaveAttributes() + // Print banner with or without the log tag + if !tagPrinted { + c.Print(logBanner) + tagPrinted = true + } else { + c.Print(emptyBanner) + } + // Restore the text color of the errors message + ansiRestoreAttributes() + ansiMoveRight(bannerWidth) + // Continue errors message printing + c.Println(line) + break + } + } + + // Exit because this is a fatal errors message + os.Exit(1) +} + +type infoMsg struct{} + +var info infoMsg + +func (i infoMsg) json(msg string, args ...interface{}) { + var message string + if msg != "" { + message = fmt.Sprintf(msg, args...) + } else { + message = fmt.Sprint(args...) + } + logJSON, err := json.Marshal(&log.Entry{ + Level: InformationLvl.String(), + Message: message, + Time: time.Now().UTC(), + }) + if err != nil { + panic(err) + } + fmt.Println(string(logJSON)) +} + +func (i infoMsg) quiet(msg string, args ...interface{}) { +} + +func (i infoMsg) pretty(msg string, args ...interface{}) { + if msg == "" { + c.Println(args...) + } + c.Printf(msg, args...) +} + +type errorMsg struct{} + +var errorm errorMsg + +func (i errorMsg) json(msg string, args ...interface{}) { + var message string + if msg != "" { + message = fmt.Sprintf(msg, args...) + } else { + message = fmt.Sprint(args...) + } + logJSON, err := json.Marshal(&log.Entry{ + Level: ErrorLvl.String(), + Message: message, + Time: time.Now().UTC(), + Trace: &log.Trace{Message: message, Source: []string{getSource(6)}}, + }) + if err != nil { + panic(err) + } + fmt.Println(string(logJSON)) +} + +func (i errorMsg) quiet(msg string, args ...interface{}) { + i.pretty(msg, args...) +} + +func (i errorMsg) pretty(msg string, args ...interface{}) { + if msg == "" { + c.Println(args...) + } + c.Printf(msg, args...) + c.Printf("\n") +} + +// Error : +func Error(msg string, data ...interface{}) { + consoleLog(errorm, msg, data...) +} + +// Info : +func Info(msg string, data ...interface{}) { + consoleLog(info, msg, data...) +} diff --git a/pkg/logger/const.go b/pkg/logger/const.go new file mode 100644 index 000000000..978d0c612 --- /dev/null +++ b/pkg/logger/const.go @@ -0,0 +1,56 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + + "github.com/minio/console/pkg/logger/target/http" +) + +// Audit/Logger constants +const ( + EnvLoggerJSONEnable = "CONSOLE_LOGGER_JSON_ENABLE" + EnvLoggerAnonymousEnable = "CONSOLE_LOGGER_ANONYMOUS_ENABLE" + EnvLoggerQuietEnable = "CONSOLE_LOGGER_QUIET_ENABLE" + + EnvGlobalDeploymentID = "CONSOLE_GLOBAL_DEPLOYMENT_ID" + EnvLoggerWebhookEnable = "CONSOLE_LOGGER_WEBHOOK_ENABLE" + EnvLoggerWebhookEndpoint = "CONSOLE_LOGGER_WEBHOOK_ENDPOINT" + EnvLoggerWebhookAuthToken = "CONSOLE_LOGGER_WEBHOOK_AUTH_TOKEN" + EnvLoggerWebhookClientCert = "CONSOLE_LOGGER_WEBHOOK_CLIENT_CERT" + EnvLoggerWebhookClientKey = "CONSOLE_LOGGER_WEBHOOK_CLIENT_KEY" + EnvLoggerWebhookQueueSize = "CONSOLE_LOGGER_WEBHOOK_QUEUE_SIZE" + + EnvAuditWebhookEnable = "CONSOLE_AUDIT_WEBHOOK_ENABLE" + EnvAuditWebhookEndpoint = "CONSOLE_AUDIT_WEBHOOK_ENDPOINT" + EnvAuditWebhookAuthToken = "CONSOLE_AUDIT_WEBHOOK_AUTH_TOKEN" + EnvAuditWebhookClientCert = "CONSOLE_AUDIT_WEBHOOK_CLIENT_CERT" + EnvAuditWebhookClientKey = "CONSOLE_AUDIT_WEBHOOK_CLIENT_KEY" + EnvAuditWebhookQueueSize = "CONSOLE_AUDIT_WEBHOOK_QUEUE_SIZE" +) + +// Config console and http logger targets +type Config struct { + HTTP map[string]http.Config `json:"http"` + AuditWebhook map[string]http.Config `json:"audit"` +} + +var ( + globalDeploymentID string + GlobalContext context.Context +) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 000000000..db01e25e5 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,480 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + "go/build" + "net/http" + "path/filepath" + "reflect" + "runtime" + "strings" + "syscall" + "time" + + "github.com/minio/pkg/env" + + "github.com/minio/console/pkg" + "github.com/minio/pkg/certs" + + "github.com/minio/console/pkg/logger/config" + "github.com/minio/console/pkg/logger/message/log" + "github.com/minio/highwayhash" + "github.com/minio/minio-go/v7/pkg/set" +) + +// HighwayHash key for logging in anonymous mode +var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0") + +// Disable disables all logging, false by default. (used for "go test") +var Disable = false + +// Level type +type Level int8 + +// Enumerated level types +const ( + InformationLvl Level = iota + 1 + ErrorLvl + FatalLvl +) + +var trimStrings []string + +// TimeFormat - logging time format. +const TimeFormat string = "15:04:05 MST 01/02/2006" + +var matchingFuncNames = [...]string{ + "http.HandlerFunc.ServeHTTP", + "cmd.serverMain", + "cmd.StartGateway", + // add more here .. +} + +func (level Level) String() string { + var lvlStr string + switch level { + case InformationLvl: + lvlStr = "INFO" + case ErrorLvl: + lvlStr = "ERROR" + case FatalLvl: + lvlStr = "FATAL" + } + return lvlStr +} + +// quietFlag: Hide startup messages if enabled +// jsonFlag: Display in JSON format, if enabled +var ( + quietFlag, jsonFlag, anonFlag bool + // Custom function to format errors + errorFmtFunc func(string, error, bool) string +) + +// EnableQuiet - turns quiet option on. +func EnableQuiet() { + quietFlag = true +} + +// EnableJSON - outputs logs in json format. +func EnableJSON() { + jsonFlag = true + quietFlag = true +} + +// EnableAnonymous - turns anonymous flag +// to avoid printing sensitive information. +func EnableAnonymous() { + anonFlag = true +} + +// IsAnonymous - returns true if anonFlag is true +func IsAnonymous() bool { + return anonFlag +} + +// IsJSON - returns true if jsonFlag is true +func IsJSON() bool { + return jsonFlag +} + +// IsQuiet - returns true if quietFlag is true +func IsQuiet() bool { + return quietFlag +} + +// RegisterError registers the specified rendering function. This latter +// will be called for a pretty rendering of fatal errors. +func RegisterError(f func(string, error, bool) string) { + errorFmtFunc = f +} + +// Remove any duplicates and return unique entries. +func uniqueEntries(paths []string) []string { + m := make(set.StringSet) + for _, p := range paths { + if !m.Contains(p) { + m.Add(p) + } + } + return m.ToSlice() +} + +// Init sets the trimStrings to possible GOPATHs +// and GOROOT directories. Also append github.com/minio/minio +// This is done to clean up the filename, when stack trace is +// displayed when an errors happens. +func Init(goPath string, goRoot string) { + var goPathList []string + var goRootList []string + var defaultgoPathList []string + var defaultgoRootList []string + pathSeperator := ":" + // Add all possible GOPATH paths into trimStrings + // Split GOPATH depending on the OS type + if runtime.GOOS == "windows" { + pathSeperator = ";" + } + + goPathList = strings.Split(goPath, pathSeperator) + goRootList = strings.Split(goRoot, pathSeperator) + defaultgoPathList = strings.Split(build.Default.GOPATH, pathSeperator) + defaultgoRootList = strings.Split(build.Default.GOROOT, pathSeperator) + + // Add trim string "{GOROOT}/src/" into trimStrings + trimStrings = []string{filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)} + + // Add all possible path from GOPATH=path1:path2...:pathN + // as "{path#}/src/" into trimStrings + for _, goPathString := range goPathList { + trimStrings = append(trimStrings, filepath.Join(goPathString, "src")+string(filepath.Separator)) + } + + for _, goRootString := range goRootList { + trimStrings = append(trimStrings, filepath.Join(goRootString, "src")+string(filepath.Separator)) + } + + for _, defaultgoPathString := range defaultgoPathList { + trimStrings = append(trimStrings, filepath.Join(defaultgoPathString, "src")+string(filepath.Separator)) + } + + for _, defaultgoRootString := range defaultgoRootList { + trimStrings = append(trimStrings, filepath.Join(defaultgoRootString, "src")+string(filepath.Separator)) + } + + // Remove duplicate entries. + trimStrings = uniqueEntries(trimStrings) + + // Add "github.com/minio/minio" as the last to cover + // paths like "{GOROOT}/src/github.com/minio/minio" + // and "{GOPATH}/src/github.com/minio/minio" + trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator)) +} + +func trimTrace(f string) string { + for _, trimString := range trimStrings { + f = strings.TrimPrefix(filepath.ToSlash(f), filepath.ToSlash(trimString)) + } + return filepath.FromSlash(f) +} + +func getSource(level int) string { + pc, file, lineNumber, ok := runtime.Caller(level) + if ok { + // Clean up the common prefixes + file = trimTrace(file) + _, funcName := filepath.Split(runtime.FuncForPC(pc).Name()) + return fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName) + } + return "" +} + +// getTrace method - creates and returns stack trace +func getTrace(traceLevel int) []string { + var trace []string + pc, file, lineNumber, ok := runtime.Caller(traceLevel) + + for ok && file != "" { + // Clean up the common prefixes + file = trimTrace(file) + // Get the function name + _, funcName := filepath.Split(runtime.FuncForPC(pc).Name()) + // Skip duplicate traces that start with file name, "" + // and also skip traces with function name that starts with "runtime." + if !strings.HasPrefix(file, "") && + !strings.HasPrefix(funcName, "runtime.") { + // Form and append a line of stack trace into a + // collection, 'trace', to build full stack trace + trace = append(trace, fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName)) + + // Ignore trace logs beyond the following conditions + for _, name := range matchingFuncNames { + if funcName == name { + return trace + } + } + } + traceLevel++ + // Read stack trace information from PC + pc, file, lineNumber, ok = runtime.Caller(traceLevel) + } + return trace +} + +// Return the highway hash of the passed string +func hashString(input string) string { + hh, _ := highwayhash.New(magicHighwayHash256Key) + hh.Write([]byte(input)) + return hex.EncodeToString(hh.Sum(nil)) +} + +// Kind specifies the kind of errors log +type Kind string + +const ( + // Minio errors + Minio Kind = "CONSOLE" + // Application errors + Application Kind = "APPLICATION" + // All errors + All Kind = "ALL" +) + +// LogAlwaysIf prints a detailed errors message during +// the execution of the server. +func LogAlwaysIf(ctx context.Context, err error, errKind ...interface{}) { + if err == nil { + return + } + + logIf(ctx, err, errKind...) +} + +// LogIf prints a detailed errors message during +// the execution of the server +func LogIf(ctx context.Context, err error, errKind ...interface{}) { + if err == nil { + return + } + + if errors.Is(err, context.Canceled) { + return + } + logIf(ctx, err, errKind...) +} + +// logIf prints a detailed errors message during +// the execution of the server. +func logIf(ctx context.Context, err error, errKind ...interface{}) { + if Disable { + return + } + logKind := string(Minio) + if len(errKind) > 0 { + if ek, ok := errKind[0].(Kind); ok { + logKind = string(ek) + } + } + req := GetReqInfo(ctx) + + if req == nil { + req = &ReqInfo{API: "SYSTEM"} + } + + kv := req.GetTags() + tags := make(map[string]interface{}, len(kv)) + for _, entry := range kv { + tags[entry.Key] = entry.Val + } + + // Get full stack trace + trace := getTrace(3) + + // Get the cause for the Error + message := fmt.Sprintf("%v (%T)", err, err) + if req.DeploymentID == "" { + req.DeploymentID = GetGlobalDeploymentID() + } + + entry := log.Entry{ + DeploymentID: req.DeploymentID, + Level: ErrorLvl.String(), + LogKind: logKind, + RemoteHost: req.RemoteHost, + Host: req.Host, + RequestID: req.RequestID, + SessionID: req.SessionID, + UserAgent: req.UserAgent, + Time: time.Now().UTC(), + Trace: &log.Trace{ + Message: message, + Source: trace, + Variables: tags, + }, + } + + if anonFlag { + entry.SessionID = hashString(entry.SessionID) + entry.RemoteHost = hashString(entry.RemoteHost) + entry.Trace.Message = reflect.TypeOf(err).String() + entry.Trace.Variables = make(map[string]interface{}) + } + + // Iterate over all logger targets to send the log entry + for _, t := range SystemTargets() { + if err := t.Send(entry, entry.LogKind); err != nil { + if consoleTgt != nil { + entry.Trace.Message = fmt.Sprintf("event(%#v) was not sent to Logger target (%#v): %#v", entry, t, err) + consoleTgt.Send(entry, entry.LogKind) + } + } + } +} + +// ErrCritical is the value panic'd whenever CriticalIf is called. +var ErrCritical struct{} + +// CriticalIf logs the provided errors on the console. It fails the +// current go-routine by causing a `panic(ErrCritical)`. +func CriticalIf(ctx context.Context, err error, errKind ...interface{}) { + if err != nil { + LogIf(ctx, err, errKind...) + panic(ErrCritical) + } +} + +// FatalIf is similar to Fatal() but it ignores passed nil errors +func FatalIf(err error, msg string, data ...interface{}) { + if err == nil { + return + } + fatal(err, msg, data...) +} + +func applyDynamicConfigForSubSys(ctx context.Context, transport *http.Transport, subSys string) error { + switch subSys { + case config.LoggerWebhookSubSys: + loggerCfg, err := LookupConfigForSubSys(config.LoggerWebhookSubSys) + if err != nil { + LogIf(ctx, fmt.Errorf("unable to load logger webhook config: %w", err)) + return err + } + userAgent := getUserAgent() + for n, l := range loggerCfg.HTTP { + if l.Enabled { + l.LogOnce = LogOnceIf + l.UserAgent = userAgent + l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey) + loggerCfg.HTTP[n] = l + } + } + err = UpdateSystemTargets(loggerCfg) + if err != nil { + LogIf(ctx, fmt.Errorf("unable to update logger webhook config: %w", err)) + return err + } + case config.AuditWebhookSubSys: + loggerCfg, err := LookupConfigForSubSys(config.AuditWebhookSubSys) + if err != nil { + LogIf(ctx, fmt.Errorf("unable to load audit webhook config: %w", err)) + return err + } + userAgent := getUserAgent() + for n, l := range loggerCfg.AuditWebhook { + if l.Enabled { + l.LogOnce = LogOnceIf + l.UserAgent = userAgent + l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey) + loggerCfg.AuditWebhook[n] = l + } + } + + err = UpdateAuditWebhookTargets(loggerCfg) + if err != nil { + LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err)) + return err + } + } + return nil +} + +// InitializeLogger : +func InitializeLogger(ctx context.Context, transport *http.Transport) error { + err := applyDynamicConfigForSubSys(ctx, transport, config.LoggerWebhookSubSys) + if err != nil { + return err + } + err = applyDynamicConfigForSubSys(ctx, transport, config.AuditWebhookSubSys) + if err != nil { + return err + } + + if enable, _ := config.ParseBool(env.Get(EnvLoggerJSONEnable, "")); enable { + EnableJSON() + } + if enable, _ := config.ParseBool(env.Get(EnvLoggerAnonymousEnable, "")); enable { + EnableAnonymous() + } + if enable, _ := config.ParseBool(env.Get(EnvLoggerQuietEnable, "")); enable { + EnableQuiet() + } + + return nil +} + +func getUserAgent() string { + userAgentParts := []string{} + // Helper function to concisely append a pair of strings to a + // the user-agent slice. + uaAppend := func(p, q string) { + userAgentParts = append(userAgentParts, p, q) + } + uaAppend("Console (", runtime.GOOS) + uaAppend("; ", runtime.GOARCH) + uaAppend(") Console/", pkg.Version) + uaAppend(" Console/", pkg.ReleaseTag) + uaAppend(" Console/", pkg.CommitID) + + return strings.Join(userAgentParts, "") +} + +// NewHTTPTransportWithClientCerts returns a new http configuration +// used while communicating with the cloud backends. +func NewHTTPTransportWithClientCerts(parentTransport *http.Transport, clientCert, clientKey string) *http.Transport { + transport := parentTransport.Clone() + if clientCert != "" && clientKey != "" { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + c, err := certs.NewManager(ctx, clientCert, clientKey, tls.LoadX509KeyPair) + if err != nil { + LogIf(ctx, fmt.Errorf("failed to load client key and cert, please check your endpoint configuration: %s", + err.Error())) + } + if c != nil { + c.UpdateReloadDuration(10 * time.Second) + c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP + transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate + } + } + return transport +} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go new file mode 100644 index 000000000..624adc137 --- /dev/null +++ b/pkg/logger/logger_test.go @@ -0,0 +1,243 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + "fmt" + "net/http" + "os" + "testing" +) + +func testServer(w http.ResponseWriter, r *http.Request) { +} + +func TestInitializeLogger(t *testing.T) { + testServerWillStart := make(chan interface{}) + http.HandleFunc("/", testServer) + go func() { + close(testServerWillStart) + err := http.ListenAndServe("127.0.0.1:1337", nil) + if err != nil { + return + } + }() + <-testServerWillStart + + loggerWebhookEnable := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEnable) + loggerWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEndpoint) + loggerWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvLoggerWebhookAuthToken) + loggerWebhookClientCert := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientCert) + loggerWebhookClientKey := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientKey) + loggerWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvLoggerWebhookQueueSize) + + auditWebhookEnable := fmt.Sprintf("%s_TEST", EnvAuditWebhookEnable) + auditWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvAuditWebhookEndpoint) + auditWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvAuditWebhookAuthToken) + auditWebhookClientCert := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientCert) + auditWebhookClientKey := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientKey) + auditWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvAuditWebhookQueueSize) + + type args struct { + ctx context.Context + transport *http.Transport + } + tests := []struct { + name string + args args + wantErr bool + setEnvVars func() + unsetEnvVars func() + }{ + { + name: "logger or auditlog is not enabled", + args: args{ + ctx: context.Background(), + transport: http.DefaultTransport.(*http.Transport).Clone(), + }, + wantErr: false, + setEnvVars: func() { + }, + unsetEnvVars: func() { + }, + }, + { + name: "logger webhook initialized correctly", + args: args{ + ctx: context.Background(), + transport: http.DefaultTransport.(*http.Transport).Clone(), + }, + wantErr: false, + setEnvVars: func() { + os.Setenv(loggerWebhookEnable, "on") + os.Setenv(loggerWebhookEndpoint, "http://127.0.0.1:1337/logger") + os.Setenv(loggerWebhookAuthToken, "test") + os.Setenv(loggerWebhookClientCert, "") + os.Setenv(loggerWebhookClientKey, "") + os.Setenv(loggerWebhookQueueSize, "1000") + }, + unsetEnvVars: func() { + os.Unsetenv(loggerWebhookEnable) + os.Unsetenv(loggerWebhookEndpoint) + os.Unsetenv(loggerWebhookAuthToken) + os.Unsetenv(loggerWebhookClientCert) + os.Unsetenv(loggerWebhookClientKey) + os.Unsetenv(loggerWebhookQueueSize) + }, + }, + { + name: "logger webhook failed to initialize", + args: args{ + ctx: context.Background(), + transport: http.DefaultTransport.(*http.Transport).Clone(), + }, + wantErr: true, + setEnvVars: func() { + os.Setenv(loggerWebhookEnable, "on") + os.Setenv(loggerWebhookEndpoint, "https://aklsjdakljdjkalsd.com") + os.Setenv(loggerWebhookAuthToken, "test") + os.Setenv(loggerWebhookClientCert, "") + os.Setenv(loggerWebhookClientKey, "") + os.Setenv(loggerWebhookQueueSize, "1000") + }, + unsetEnvVars: func() { + os.Unsetenv(loggerWebhookEnable) + os.Unsetenv(loggerWebhookEndpoint) + os.Unsetenv(loggerWebhookAuthToken) + os.Unsetenv(loggerWebhookClientCert) + os.Unsetenv(loggerWebhookClientKey) + os.Unsetenv(loggerWebhookQueueSize) + }, + }, + { + name: "auditlog webhook initialized correctly", + args: args{ + ctx: context.Background(), + transport: http.DefaultTransport.(*http.Transport).Clone(), + }, + wantErr: false, + setEnvVars: func() { + os.Setenv(auditWebhookEnable, "on") + os.Setenv(auditWebhookEndpoint, "http://127.0.0.1:1337/audit") + os.Setenv(auditWebhookAuthToken, "test") + os.Setenv(auditWebhookClientCert, "") + os.Setenv(auditWebhookClientKey, "") + os.Setenv(auditWebhookQueueSize, "1000") + }, + unsetEnvVars: func() { + os.Unsetenv(auditWebhookEnable) + os.Unsetenv(auditWebhookEndpoint) + os.Unsetenv(auditWebhookAuthToken) + os.Unsetenv(auditWebhookClientCert) + os.Unsetenv(auditWebhookClientKey) + os.Unsetenv(auditWebhookQueueSize) + }, + }, + { + name: "auditlog webhook failed to initialize", + args: args{ + ctx: context.Background(), + transport: http.DefaultTransport.(*http.Transport).Clone(), + }, + wantErr: true, + setEnvVars: func() { + os.Setenv(auditWebhookEnable, "on") + os.Setenv(auditWebhookEndpoint, "https://aklsjdakljdjkalsd.com") + os.Setenv(auditWebhookAuthToken, "test") + os.Setenv(auditWebhookClientCert, "") + os.Setenv(auditWebhookClientKey, "") + os.Setenv(auditWebhookQueueSize, "1000") + }, + unsetEnvVars: func() { + os.Unsetenv(auditWebhookEnable) + os.Unsetenv(auditWebhookEndpoint) + os.Unsetenv(auditWebhookAuthToken) + os.Unsetenv(auditWebhookClientCert) + os.Unsetenv(auditWebhookClientKey) + os.Unsetenv(auditWebhookQueueSize) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setEnvVars != nil { + tt.setEnvVars() + } + if err := InitializeLogger(tt.args.ctx, tt.args.transport); (err != nil) != tt.wantErr { + t.Errorf("InitializeLogger() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.unsetEnvVars != nil { + tt.unsetEnvVars() + } + }) + } +} + +func TestEnableJSON(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "enable json", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + EnableJSON() + if !IsJSON() { + t.Errorf("EnableJSON() = %v, want %v", IsJSON(), true) + } + }) + } +} + +func TestEnableQuiet(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "enable quiet", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + EnableQuiet() + if !IsQuiet() { + t.Errorf("EnableQuiet() = %v, want %v", IsQuiet(), true) + } + }) + } +} + +func TestEnableAnonymous(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "enable anonymous", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + EnableAnonymous() + if !IsAnonymous() { + t.Errorf("EnableAnonymous() = %v, want %v", IsAnonymous(), true) + } + }) + } +} diff --git a/pkg/logger/logonce.go b/pkg/logger/logonce.go new file mode 100644 index 000000000..1db87c283 --- /dev/null +++ b/pkg/logger/logonce.go @@ -0,0 +1,92 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + "errors" + "net/http" + "sync" + "time" +) + +// Holds a map of recently logged errors. +type logOnceType struct { + IDMap map[interface{}]error + sync.Mutex +} + +// One log message per errors. +func (l *logOnceType) logOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) { + if err == nil { + return + } + l.Lock() + shouldLog := false + prevErr := l.IDMap[id] + if prevErr == nil { + l.IDMap[id] = err + shouldLog = true + } else if prevErr.Error() != err.Error() { + l.IDMap[id] = err + shouldLog = true + } + l.Unlock() + + if shouldLog { + LogIf(ctx, err, errKind...) + } +} + +// Cleanup the map every 30 minutes so that the log message is printed again for the user to notice. +func (l *logOnceType) cleanupRoutine() { + for { + l.Lock() + l.IDMap = make(map[interface{}]error) + l.Unlock() + + time.Sleep(30 * time.Minute) + } +} + +// Returns logOnceType +func newLogOnceType() *logOnceType { + l := &logOnceType{IDMap: make(map[interface{}]error)} + go l.cleanupRoutine() + return l +} + +var logOnce = newLogOnceType() + +// LogOnceIf - Logs notification errors - once per errors. +// id is a unique identifier for related log messages, refer to cmd/notification.go +// on how it is used. +func LogOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) { + if err == nil { + return + } + + if errors.Is(err, context.Canceled) { + return + } + + if err.Error() == http.ErrServerClosed.Error() || err.Error() == "disk not found" { + return + } + + logOnce.logOnceIf(ctx, err, id, errKind...) +} diff --git a/pkg/logger/message/audit/entry.go b/pkg/logger/message/audit/entry.go new file mode 100644 index 000000000..43148375f --- /dev/null +++ b/pkg/logger/message/audit/entry.go @@ -0,0 +1,130 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package audit + +import ( + "net/http" + "os" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + + "github.com/minio/console/pkg/utils" + + xhttp "github.com/minio/console/pkg/http" +) + +// Version - represents the current version of audit log structure. +const Version = "1" + +// ObjectVersion object version key/versionId +type ObjectVersion struct { + ObjectName string `json:"objectName"` + VersionID string `json:"versionId,omitempty"` +} + +// Entry - audit entry logs. +type Entry struct { + Version string `json:"version"` + DeploymentID string `json:"deploymentid,omitempty"` + Time time.Time `json:"time"` + Trigger string `json:"trigger"` + API struct { + Path string `json:"path,omitempty"` + Status string `json:"status,omitempty"` + Method string `json:"method"` + StatusCode int `json:"statusCode,omitempty"` + InputBytes int64 `json:"rx"` + OutputBytes int64 `json:"tx"` + TimeToFirstByte string `json:"timeToFirstByte,omitempty"` + TimeToResponse string `json:"timeToResponse,omitempty"` + } `json:"api"` + RemoteHost string `json:"remotehost,omitempty"` + RequestID string `json:"requestID,omitempty"` + SessionID string `json:"sessionID,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + ReqClaims map[string]interface{} `json:"requestClaims,omitempty"` + ReqQuery map[string]string `json:"requestQuery,omitempty"` + ReqHeader map[string]string `json:"requestHeader,omitempty"` + RespHeader map[string]string `json:"responseHeader,omitempty"` + Tags map[string]interface{} `json:"tags,omitempty"` +} + +// 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(), + } +} + +// 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 = r.RemoteAddr + 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() + + var requestID interface{} + requestID = r.Context().Value(utils.ContextRequestID) + if requestID == nil { + requestID, _ = utils.NewUUID() + } + entry.RequestID = requestID.(string) + + if val := r.Context().Value(utils.ContextRequestUserID); val != nil { + sessionID := val.(string) + if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" { + claims := jwt.MapClaims{} + _, _ = jwt.ParseWithClaims(sessionID, claims, nil) + if sub, ok := claims["sub"]; ok { + sessionID = sub.(string) + } + } + entry.SessionID = sessionID + } + + 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, `"`) + } + + return entry +} diff --git a/pkg/logger/message/audit/entry_test.go b/pkg/logger/message/audit/entry_test.go new file mode 100644 index 000000000..3da4bdbdb --- /dev/null +++ b/pkg/logger/message/audit/entry_test.go @@ -0,0 +1,126 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package audit + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "reflect" + "testing" + "time" + + "github.com/minio/console/pkg/utils" +) + +func TestNewEntry(t *testing.T) { + type args struct { + deploymentID string + } + tests := []struct { + name string + args args + want Entry + }{ + { + name: "constructs an audit entry object with some fields filled", + args: args{ + deploymentID: "1", + }, + want: Entry{ + Version: Version, + DeploymentID: "1", + Time: time.Now().UTC(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewEntry(tt.args.deploymentID); got.DeploymentID != tt.want.DeploymentID { + t.Errorf("NewEntry() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestToEntry(t *testing.T) { + + req := httptest.NewRequest(http.MethodGet, "/api/v1/tenants?test=xyz", nil) + req.Header.Set("Authorization", "xyz") + req.Header.Set("ETag", "\"ABCDE\"") + + // applying context information + ctx := context.WithValue(req.Context(), utils.ContextRequestUserID, "eyJhbGciOiJSUzI1NiIsImtpZCI6Ing5cS0wSkEwQzFMWDJlRlR3dHo2b0t0NVNnRzJad0llMGVNczMxbjU0b2sifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJtaW5pby1vcGVyYXRvciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjb25zb2xlLXNhLXRva2VuLWJrZzZwIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNvbnNvbGUtc2EiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJhZTE2ZGVkNS01MmM3LTRkZTQtOWUxYS1iNmI4NGU2OGMzM2UiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tb3BlcmF0b3I6Y29uc29sZS1zYSJ9.AjhzekAPC59SQVBQL5sr-1dqr57-jH8a5LVazpnEr_cC0JqT4jXYjdfbrZSF9yaL4gHRv2l0kOhBlrjRK7y-IpMbxE71Fne_lSzaptSuqgI5I9dFvpVfZWP1yMAqav8mrlUoWkWDq9IAkyH4bvvZrVgQJGgd5t9U_7DQCVwbkQvy0wGS5zoMcZhYenn_Ub1BoxWcviADQ1aY1wQju8OP0IOwKTIMXMQqciOFdJ9T5-tQEGUrikTu_tW-1shUHzOxBcEzGVtBvBy2OmbNnRFYogbhmp-Dze6EAi035bY32bfL7XKBUNCW6_3VbN_h3pQNAuT2NJOSKuhJ3cGldCB2zg") + req = req.WithContext(ctx) + + w := httptest.NewRecorder() + w.Header().Set("Authorization", "xyz") + w.Header().Set("ETag", "\"ABCDE\"") + + type args struct { + w http.ResponseWriter + r *http.Request + reqClaims map[string]interface{} + deploymentID string + } + tests := []struct { + name string + args args + want Entry + preFunc func() + postFunc func() + }{ + { + preFunc: func() { + os.Setenv("CONSOLE_OPERATOR_MODE", "on") + }, + postFunc: func() { + os.Unsetenv("CONSOLE_OPERATOR_MODE") + + }, + name: "constructs an audit entry from a http request", + args: args{ + w: w, + r: req, + reqClaims: map[string]interface{}{}, + deploymentID: "1", + }, + want: Entry{ + Version: "1", + DeploymentID: "1", + SessionID: "system:serviceaccount:minio-operator:console-sa", + ReqQuery: map[string]string{"test": "xyz"}, + ReqHeader: map[string]string{"test": "xyz"}, + RespHeader: map[string]string{"test": "xyz", "ETag": "ABCDE"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc() + } + if got := ToEntry(tt.args.w, tt.args.r, tt.args.reqClaims, tt.args.deploymentID); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ToEntry() = %v, want %v", got, tt.want) + } + if tt.postFunc != nil { + tt.postFunc() + } + }) + } +} diff --git a/pkg/logger/message/log/entry.go b/pkg/logger/message/log/entry.go new file mode 100644 index 000000000..0177fd9dd --- /dev/null +++ b/pkg/logger/message/log/entry.go @@ -0,0 +1,64 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package log + +import ( + "time" +) + +// ObjectVersion object version key/versionId +type ObjectVersion struct { + ObjectName string `json:"objectName"` + VersionID string `json:"versionId,omitempty"` +} + +// Args - defines the arguments for the API. +type Args struct { + Bucket string `json:"bucket,omitempty"` + Object string `json:"object,omitempty"` + VersionID string `json:"versionId,omitempty"` + Objects []ObjectVersion `json:"objects,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// Trace - defines the trace. +type Trace struct { + Message string `json:"message,omitempty"` + Source []string `json:"source,omitempty"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +// API - defines the api type and its args. +type API struct { + Name string `json:"name,omitempty"` +} + +// Entry - defines fields and values of each log entry. +type Entry struct { + DeploymentID string `json:"deploymentid,omitempty"` + Level string `json:"level"` + LogKind string `json:"errKind"` + Time time.Time `json:"time"` + API *API `json:"api,omitempty"` + RemoteHost string `json:"remotehost,omitempty"` + Host string `json:"host,omitempty"` + RequestID string `json:"requestID,omitempty"` + SessionID string `json:"sessionID,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Message string `json:"message,omitempty"` + Trace *Trace `json:"errors,omitempty"` +} diff --git a/pkg/logger/reqinfo.go b/pkg/logger/reqinfo.go new file mode 100644 index 000000000..58b5ca0cc --- /dev/null +++ b/pkg/logger/reqinfo.go @@ -0,0 +1,117 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + "fmt" + "sync" + + "github.com/minio/console/pkg/utils" +) + +// KeyVal - appended to ReqInfo.Tags +type KeyVal struct { + Key string + Val interface{} +} + +// ObjectVersion object version key/versionId +type ObjectVersion struct { + ObjectName string + VersionID string `json:"VersionId,omitempty"` +} + +// ReqInfo stores the request info. +type ReqInfo struct { + RemoteHost string // Client Host/IP + Host string // Node Host/IP + UserAgent string // User Agent + DeploymentID string // x-minio-deployment-id + RequestID string // x-amz-request-id + SessionID string // custom session id + API string // API name - GetObject PutObject NewMultipartUpload etc. + BucketName string `json:",omitempty"` // Bucket name + ObjectName string `json:",omitempty"` // Object name + VersionID string `json:",omitempty"` // corresponding versionID for the object + Objects []ObjectVersion `json:",omitempty"` // Only set during MultiObject delete handler. + AccessKey string // Access Key + tags []KeyVal // Any additional info not accommodated by above fields + sync.RWMutex +} + +// GetTags - returns the user defined tags +func (r *ReqInfo) GetTags() []KeyVal { + if r == nil { + return nil + } + r.RLock() + defer r.RUnlock() + return append([]KeyVal(nil), r.tags...) +} + +// GetTagsMap - returns the user defined tags in a map structure +func (r *ReqInfo) GetTagsMap() map[string]interface{} { + if r == nil { + return nil + } + r.RLock() + defer r.RUnlock() + m := make(map[string]interface{}, len(r.tags)) + for _, t := range r.tags { + m[t.Key] = t.Val + } + return m +} + +// SetReqInfo sets ReqInfo in the context. +func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context { + if ctx == nil { + LogIf(context.Background(), fmt.Errorf("context is nil")) + return nil + } + return context.WithValue(ctx, utils.ContextLogKey, req) +} + +// GetReqInfo returns ReqInfo if set. +func GetReqInfo(ctx context.Context) *ReqInfo { + if ctx != nil { + r, ok := ctx.Value(utils.ContextLogKey).(*ReqInfo) + if ok { + return r + } + r = &ReqInfo{} + if val, o := ctx.Value(utils.ContextRequestID).(string); o { + r.RequestID = val + } + if val, o := ctx.Value(utils.ContextRequestUserID).(string); o { + r.SessionID = val + } + if val, o := ctx.Value(utils.ContextRequestUserAgent).(string); o { + r.UserAgent = val + } + if val, o := ctx.Value(utils.ContextRequestHost).(string); o { + r.Host = val + } + if val, o := ctx.Value(utils.ContextRequestRemoteAddr).(string); o { + r.RemoteHost = val + } + SetReqInfo(ctx, r) + return r + } + return nil +} diff --git a/pkg/logger/target/http/http.go b/pkg/logger/target/http/http.go new file mode 100644 index 000000000..4dec59e23 --- /dev/null +++ b/pkg/logger/target/http/http.go @@ -0,0 +1,226 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package http + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + xhttp "github.com/minio/console/pkg/http" + "github.com/minio/console/pkg/logger/target/types" +) + +// Timeout for the webhook http call +const webhookCallTimeout = 5 * time.Second + +// Config http logger target +type Config struct { + Enabled bool `json:"enabled"` + Name string `json:"name"` + UserAgent string `json:"userAgent"` + Endpoint string `json:"endpoint"` + AuthToken string `json:"authToken"` + ClientCert string `json:"clientCert"` + ClientKey string `json:"clientKey"` + QueueSize int `json:"queueSize"` + Transport http.RoundTripper `json:"-"` + + // Custom logger + LogOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) `json:"-"` +} + +// Target implements logger.Target and sends the json +// format of a log entry to the configured http endpoint. +// An internal buffer of logs is maintained but when the +// buffer is full, new logs are just ignored and an errors +// is returned to the caller. +type Target struct { + status int32 + wg sync.WaitGroup + + // Channel of log entries + logCh chan interface{} + + config Config +} + +// Endpoint returns the backend endpoint +func (h *Target) Endpoint() string { + return h.config.Endpoint +} + +func (h *Target) String() string { + return h.config.Name +} + +// Init validate and initialize the http target +func (h *Target) Init() error { + ctx, cancel := context.WithTimeout(context.Background(), 2*webhookCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.config.Endpoint, strings.NewReader(`{}`)) + if err != nil { + return err + } + + req.Header.Set(xhttp.ContentType, "application/json") + + // Set user-agent to indicate MinIO release + // version to the configured log endpoint + req.Header.Set("User-Agent", h.config.UserAgent) + + if h.config.AuthToken != "" { + req.Header.Set("Authorization", h.config.AuthToken) + } + + client := http.Client{Transport: h.config.Transport} + resp, err := client.Do(req) + if err != nil { + return err + } + + // Drain any response. + xhttp.DrainBody(resp.Body) + + if !acceptedResponseStatusCode(resp.StatusCode) { + switch resp.StatusCode { + case http.StatusForbidden: + return fmt.Errorf("%s returned '%s', please check if your auth token is correctly set", + h.config.Endpoint, resp.Status) + } + return fmt.Errorf("%s returned '%s', please check your endpoint configuration", + h.config.Endpoint, resp.Status) + } + + h.status = 1 + go h.startHTTPLogger() + return nil +} + +// Accepted HTTP Status Codes +var acceptedStatusCodeMap = map[int]bool{http.StatusOK: true, http.StatusCreated: true, http.StatusAccepted: true, http.StatusNoContent: true} + +func acceptedResponseStatusCode(code int) bool { + return acceptedStatusCodeMap[code] +} + +func (h *Target) logEntry(entry interface{}) { + logJSON, err := json.Marshal(&entry) + if err != nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), webhookCallTimeout) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + h.config.Endpoint, bytes.NewReader(logJSON)) + if err != nil { + h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint) + cancel() + return + } + req.Header.Set(xhttp.ContentType, "application/json") + + // Set user-agent to indicate MinIO release + // version to the configured log endpoint + req.Header.Set("User-Agent", h.config.UserAgent) + + if h.config.AuthToken != "" { + req.Header.Set("Authorization", h.config.AuthToken) + } + + client := http.Client{Transport: h.config.Transport} + resp, err := client.Do(req) + cancel() + if err != nil { + h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint) + return + } + + // Drain any response. + xhttp.DrainBody(resp.Body) + + if !acceptedResponseStatusCode(resp.StatusCode) { + switch resp.StatusCode { + case http.StatusForbidden: + h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check if your auth token is correctly set", h.config.Endpoint, resp.Status), h.config.Endpoint) + default: + h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check your endpoint configuration", h.config.Endpoint, resp.Status), h.config.Endpoint) + } + } +} + +func (h *Target) startHTTPLogger() { + // Create a routine which sends json logs received + // from an internal channel. + go func() { + h.wg.Add(1) + defer h.wg.Done() + for entry := range h.logCh { + h.logEntry(entry) + } + }() +} + +// New initializes a new logger target which +// sends log over http to the specified endpoint +func New(config Config) *Target { + h := &Target{ + logCh: make(chan interface{}, config.QueueSize), + config: config, + } + + return h +} + +// Send log message 'e' to http target. +func (h *Target) Send(entry interface{}, errKind string) error { + if atomic.LoadInt32(&h.status) == 0 { + // Channel was closed or used before init. + return nil + } + + select { + case h.logCh <- entry: + default: + // log channel is full, do not wait and return + // an errors immediately to the caller + return errors.New("log buffer full") + } + + return nil +} + +// Cancel - cancels the target +func (h *Target) Cancel() { + if atomic.CompareAndSwapInt32(&h.status, 1, 0) { + close(h.logCh) + } + h.wg.Wait() +} + +// Type - returns type of the target +func (h *Target) Type() types.TargetType { + return types.TargetHTTP +} diff --git a/pkg/logger/target/types/types.go b/pkg/logger/target/types/types.go new file mode 100644 index 000000000..92ef0a04a --- /dev/null +++ b/pkg/logger/target/types/types.go @@ -0,0 +1,27 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package types + +// TargetType indicates type of the target e.g. console, http, kafka +type TargetType uint8 + +// Constants for target types +const ( + _ TargetType = iota + TargetConsole + TargetHTTP +) diff --git a/pkg/logger/targets.go b/pkg/logger/targets.go new file mode 100644 index 000000000..6be0618ce --- /dev/null +++ b/pkg/logger/targets.go @@ -0,0 +1,151 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "sync" + "sync/atomic" + + "github.com/minio/console/pkg/logger/target/http" + "github.com/minio/console/pkg/logger/target/types" +) + +// Target is the entity that we will receive +// a single log entry and Send it to the log target +// e.g. Send the log to a http server +type Target interface { + String() string + Endpoint() string + Init() error + Cancel() + Send(entry interface{}, errKind string) error + Type() types.TargetType +} + +var ( + // swapMu must be held while reading slice info or swapping targets or auditTargets. + swapMu sync.Mutex + + // systemTargets is the set of enabled loggers. + // Must be immutable at all times. + // Can be swapped to another while holding swapMu + systemTargets = []Target{} + + // This is always set represent /dev/console target + consoleTgt Target + + nTargets int32 // atomic count of len(targets) +) + +// SystemTargets returns active targets. +// Returned slice may not be modified in any way. +func SystemTargets() []Target { + if atomic.LoadInt32(&nTargets) == 0 { + // Lock free if none... + return nil + } + swapMu.Lock() + res := systemTargets + swapMu.Unlock() + return res +} + +// AuditTargets returns active audit targets. +// Returned slice may not be modified in any way. +func AuditTargets() []Target { + if atomic.LoadInt32(&nAuditTargets) == 0 { + // Lock free if none... + return nil + } + swapMu.Lock() + res := auditTargets + swapMu.Unlock() + return res +} + +// auditTargets is the list of enabled audit loggers +// Must be immutable at all times. +// Can be swapped to another while holding swapMu +var ( + auditTargets = []Target{} + nAuditTargets int32 // atomic count of len(auditTargets) +) + +func cancelAllSystemTargets() { + for _, tgt := range systemTargets { + tgt.Cancel() + } +} + +func initSystemTargets(cfgMap map[string]http.Config) (tgts []Target, err error) { + for _, l := range cfgMap { + if l.Enabled { + t := http.New(l) + if err = t.Init(); err != nil { + return tgts, err + } + tgts = append(tgts, t) + } + } + return tgts, err +} + +// UpdateSystemTargets swaps targets with newly loaded ones from the cfg +func UpdateSystemTargets(cfg Config) error { + updated, err := initSystemTargets(cfg.HTTP) + if err != nil { + return err + } + + swapMu.Lock() + for _, tgt := range systemTargets { + // Preserve console target when dynamically updating + // other HTTP targets, console target is always present. + if tgt.Type() == types.TargetConsole { + updated = append(updated, tgt) + break + } + } + atomic.StoreInt32(&nTargets, int32(len(updated))) + cancelAllSystemTargets() // cancel running targets + systemTargets = updated + swapMu.Unlock() + return nil +} + +func cancelAuditTargetType(t types.TargetType) { + for _, tgt := range auditTargets { + if tgt.Type() == t { + tgt.Cancel() + } + } +} + +// UpdateAuditWebhookTargets swaps audit webhook targets with newly loaded ones from the cfg +func UpdateAuditWebhookTargets(cfg Config) error { + updated, err := initSystemTargets(cfg.AuditWebhook) + if err != nil { + return err + } + + swapMu.Lock() + atomic.StoreInt32(&nAuditTargets, int32(len(updated))) + cancelAuditTargetType(types.TargetHTTP) // cancel running targets + auditTargets = updated + swapMu.Unlock() + return nil +} diff --git a/pkg/logger/utils.go b/pkg/logger/utils.go new file mode 100644 index 000000000..87b58a11a --- /dev/null +++ b/pkg/logger/utils.go @@ -0,0 +1,60 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "fmt" + "regexp" + "runtime" + + "github.com/minio/console/pkg/logger/color" +) + +var ansiRE = regexp.MustCompile("(\x1b[^m]*m)") + +// Print ANSI Control escape +func ansiEscape(format string, args ...interface{}) { + Esc := "\x1b" + fmt.Printf("%s%s", Esc, fmt.Sprintf(format, args...)) +} + +func ansiMoveRight(n int) { + if runtime.GOOS == "windows" { + return + } + if color.IsTerminal() { + ansiEscape("[%dC", n) + } +} + +func ansiSaveAttributes() { + if runtime.GOOS == "windows" { + return + } + if color.IsTerminal() { + ansiEscape("7") + } +} + +func ansiRestoreAttributes() { + if runtime.GOOS == "windows" { + return + } + if color.IsTerminal() { + ansiEscape("8") + } +} diff --git a/pkg/subnet/subnet.go b/pkg/subnet/subnet.go index d9fe00f03..2b90a1777 100644 --- a/pkg/subnet/subnet.go +++ b/pkg/subnet/subnet.go @@ -24,7 +24,7 @@ import ( "fmt" "log" - "github.com/minio/console/pkg/utils" + "github.com/minio/console/pkg/http" "github.com/minio/pkg/licverifier" @@ -34,7 +34,7 @@ import ( "github.com/tidwall/gjson" ) -func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*LoginResp, error) { +func LoginWithMFA(client http.ClientI, username, mfaToken, otp string) (*LoginResp, error) { mfaLoginReq := MfaReq{Username: username, OTP: otp, Token: mfaToken} resp, err := subnetPostReq(client, subnetMFAURL(), mfaLoginReq, nil) if err != nil { @@ -47,7 +47,7 @@ func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*Lo return nil, errors.New("access token not found in response") } -func Login(client utils.HTTPClientI, username, password string) (*LoginResp, error) { +func Login(client http.ClientI, username, password string) (*LoginResp, error) { loginReq := map[string]string{ "username": username, "password": password, @@ -71,7 +71,7 @@ func Login(client utils.HTTPClientI, username, password string) (*LoginResp, err return nil, errors.New("access token not found in response") } -func GetOrganizations(client utils.HTTPClientI, token string) ([]*models.SubnetOrganization, error) { +func GetOrganizations(client http.ClientI, token string) ([]*models.SubnetOrganization, error) { headers := subnetAuthHeaders(token) respStr, err := subnetGetReq(client, subnetOrgsURL(), headers) if err != nil { @@ -90,7 +90,7 @@ type LicenseTokenConfig struct { Proxy string } -func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) { +func Register(client http.ClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) { var headers map[string]string regInfo := GetClusterRegInfo(admInfo) regURL := subnetRegisterURL() @@ -128,7 +128,7 @@ func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, toke const publicKey = "/downloads/license-pubkey.pem" // downloadSubnetPublicKey will download the current subnet public key. -func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) { +func downloadSubnetPublicKey(client http.ClientI) (string, error) { // Get the public key directly from Subnet url := fmt.Sprintf("%s%s", subnetBaseURL(), publicKey) resp, err := client.Get(url) @@ -145,7 +145,7 @@ func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) { } // ParseLicense parses the license with the bundle public key and return it's information -func ParseLicense(client utils.HTTPClientI, license string) (*licverifier.LicenseInfo, error) { +func ParseLicense(client http.ClientI, license string) (*licverifier.LicenseInfo, error) { var publicKeys []string subnetPubKey, err := downloadSubnetPublicKey(client) diff --git a/pkg/subnet/utils.go b/pkg/subnet/utils.go index 811ce9e50..496c3f576 100644 --- a/pkg/subnet/utils.go +++ b/pkg/subnet/utils.go @@ -25,7 +25,7 @@ import ( "io/ioutil" "net/http" - "github.com/minio/console/pkg/utils" + xhttp "github.com/minio/console/pkg/http" "github.com/minio/madmin-go" mc "github.com/minio/mc/cmd" @@ -69,11 +69,11 @@ func subnetAuthHeaders(authToken string) map[string]string { return map[string]string{"Authorization": "Bearer " + authToken} } -func httpDo(client utils.HTTPClientI, req *http.Request) (*http.Response, error) { +func httpDo(client xhttp.ClientI, req *http.Request) (*http.Response, error) { return client.Do(req) } -func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]string) (string, error) { +func subnetReqDo(client xhttp.ClientI, r *http.Request, headers map[string]string) (string, error) { for k, v := range headers { r.Header.Add(k, v) } @@ -98,10 +98,10 @@ func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]s if resp.StatusCode == http.StatusOK { return respStr, nil } - return respStr, fmt.Errorf("Request failed with code %d and error: %s", resp.StatusCode, respStr) + return respStr, fmt.Errorf("Request failed with code %d and errors: %s", resp.StatusCode, respStr) } -func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]string) (string, error) { +func subnetGetReq(client xhttp.ClientI, reqURL string, headers map[string]string) (string, error) { r, e := http.NewRequest(http.MethodGet, reqURL, nil) if e != nil { return "", e @@ -109,7 +109,7 @@ func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]st return subnetReqDo(client, r, headers) } -func subnetPostReq(client utils.HTTPClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) { +func subnetPostReq(client xhttp.ClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) { body, e := json.Marshal(payload) if e != nil { return "", e diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 000000000..ddee049e2 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,39 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package utils + +import "github.com/google/uuid" + +// NewUUID - get a random UUID. +func NewUUID() (string, error) { + u, err := uuid.NewRandom() + if err != nil { + return "", err + } + return u.String(), nil +} + +// Key used for Get/SetReqInfo +type key string + +const ContextLogKey = key("console-log") +const ContextRequestID = key("request-id") +const ContextRequestUserID = key("request-user-id") +const ContextRequestUserAgent = key("request-user-agent") +const ContextRequestHost = key("request-host") +const ContextRequestRemoteAddr = key("request-remote-addr") +const ContextAuditKey = key("request-audit-entry") diff --git a/pkg/utils/version.go b/pkg/utils/version.go index 10498da75..5eb514ec4 100644 --- a/pkg/utils/version.go +++ b/pkg/utils/version.go @@ -21,6 +21,8 @@ import ( "fmt" "io/ioutil" "regexp" + + "github.com/minio/console/pkg/http" ) var ( @@ -28,7 +30,7 @@ var ( ) // getLatestMinIOImage returns the latest docker image for MinIO if found on the internet -func GetLatestMinIOImage(client HTTPClientI) (*string, error) { +func GetLatestMinIOImage(client http.ClientI) (*string, error) { resp, err := client.Get("https://dl.min.io/server/minio/release/linux-amd64/") if err != nil { return nil, err diff --git a/restapi/admin_arns.go b/restapi/admin_arns.go index 0ab0b85f9..645831261 100644 --- a/restapi/admin_arns.go +++ b/restapi/admin_arns.go @@ -29,7 +29,7 @@ import ( func registerAdminArnsHandlers(api *operations.ConsoleAPI) { // return a list of arns api.SystemArnListHandler = systemApi.ArnListHandlerFunc(func(params systemApi.ArnListParams, session *models.Principal) middleware.Responder { - arnsResp, err := getArnsResponse(session) + arnsResp, err := getArnsResponse(session, params) if err != nil { return systemApi.NewArnListDefault(int(err.Code)).WithPayload(err) } @@ -51,21 +51,21 @@ func getArns(ctx context.Context, client MinioAdmin) (*models.ArnsResponse, erro } // getArnsResponse returns a list of active arns in the instance -func getArnsResponse(session *models.Principal) (*models.ArnsResponse, *models.Error) { +func getArnsResponse(session *models.Principal, params systemApi.ArnListParams) (*models.ArnsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output arnsList, err := getArns(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return arnsList, nil } diff --git a/restapi/admin_config.go b/restapi/admin_config.go index 728d9536e..d39e96080 100644 --- a/restapi/admin_config.go +++ b/restapi/admin_config.go @@ -33,7 +33,7 @@ import ( func registerConfigHandlers(api *operations.ConsoleAPI) { // List Configurations api.ConfigurationListConfigHandler = cfgApi.ListConfigHandlerFunc(func(params cfgApi.ListConfigParams, session *models.Principal) middleware.Responder { - configListResp, err := getListConfigResponse(session) + configListResp, err := getListConfigResponse(session, params) if err != nil { return cfgApi.NewListConfigDefault(int(err.Code)).WithPayload(err) } @@ -49,7 +49,7 @@ func registerConfigHandlers(api *operations.ConsoleAPI) { }) // Set Configuration api.ConfigurationSetConfigHandler = cfgApi.SetConfigHandlerFunc(func(params cfgApi.SetConfigParams, session *models.Principal) middleware.Responder { - resp, err := setConfigResponse(session, params.Name, params.Body) + resp, err := setConfigResponse(session, params) if err != nil { return cfgApi.NewSetConfigDefault(int(err.Code)).WithPayload(err) } @@ -57,7 +57,7 @@ func registerConfigHandlers(api *operations.ConsoleAPI) { }) // Reset Configuration api.ConfigurationResetConfigHandler = cfgApi.ResetConfigHandlerFunc(func(params cfgApi.ResetConfigParams, session *models.Principal) middleware.Responder { - resp, err := resetConfigResponse(session, params.Name) + resp, err := resetConfigResponse(session, params) if err != nil { return cfgApi.NewResetConfigDefault(int(err.Code)).WithPayload(err) } @@ -86,10 +86,12 @@ func listConfig(client MinioAdmin) ([]*models.ConfigDescription, error) { } // getListConfigResponse performs listConfig() and serializes it to the handler's output -func getListConfigResponse(session *models.Principal) (*models.ListConfigResponse, *models.Error) { +func getListConfigResponse(session *models.Principal, params cfgApi.ListConfigParams) (*models.ListConfigResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -97,7 +99,7 @@ func getListConfigResponse(session *models.Principal) (*models.ListConfigRespons configDescs, err := listConfig(adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } listGroupsResponse := &models.ListConfigResponse{ Configurations: configDescs, @@ -134,11 +136,11 @@ func getConfig(ctx context.Context, client MinioAdmin, name string) ([]*models.C // getConfigResponse performs getConfig() and serializes it to the handler's output func getConfigResponse(session *models.Principal, params cfgApi.ConfigInfoParams) (*models.Configuration, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -146,7 +148,7 @@ func getConfigResponse(session *models.Principal, params cfgApi.ConfigInfoParams configkv, err := getConfig(ctx, adminClient, params.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } configurationObj := &models.Configuration{ Name: params.Name, @@ -191,22 +193,22 @@ func buildConfig(configName *string, kvs []*models.ConfigurationKV) *string { } // setConfigResponse implements setConfig() to be used by handler -func setConfigResponse(session *models.Principal, name string, configRequest *models.SetConfigRequest) (*models.SetConfigResponse, *models.Error) { +func setConfigResponse(session *models.Principal, params cfgApi.SetConfigParams) (*models.SetConfigResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - configName := name + configName := params.Name - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - needsRestart, err := setConfigWithARNAccountID(ctx, adminClient, &configName, configRequest.KeyValues, configRequest.ArnResourceID) + needsRestart, err := setConfigWithARNAccountID(ctx, adminClient, &configName, params.Body.KeyValues, params.Body.ArnResourceID) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.SetConfigResponse{Restart: needsRestart}, nil } @@ -217,22 +219,22 @@ func resetConfig(ctx context.Context, client MinioAdmin, configName *string) (er } // resetConfigResponse implements resetConfig() to be used by handler -func resetConfigResponse(session *models.Principal, configName string) (*models.SetConfigResponse, *models.Error) { +func resetConfigResponse(session *models.Principal, params cfgApi.ResetConfigParams) (*models.SetConfigResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - err = resetConfig(ctx, adminClient, &configName) + err = resetConfig(ctx, adminClient, ¶ms.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.SetConfigResponse{Restart: true}, nil diff --git a/restapi/admin_groups.go b/restapi/admin_groups.go index c7b75e31c..8fbc5c5cd 100644 --- a/restapi/admin_groups.go +++ b/restapi/admin_groups.go @@ -32,7 +32,7 @@ import ( func registerGroupsHandlers(api *operations.ConsoleAPI) { // List Groups api.GroupListGroupsHandler = groupApi.ListGroupsHandlerFunc(func(params groupApi.ListGroupsParams, session *models.Principal) middleware.Responder { - listGroupsResponse, err := getListGroupsResponse(session) + listGroupsResponse, err := getListGroupsResponse(session, params) if err != nil { return groupApi.NewListGroupsDefault(int(err.Code)).WithPayload(err) } @@ -48,7 +48,7 @@ func registerGroupsHandlers(api *operations.ConsoleAPI) { }) // Add Group api.GroupAddGroupHandler = groupApi.AddGroupHandlerFunc(func(params groupApi.AddGroupParams, session *models.Principal) middleware.Responder { - if err := getAddGroupResponse(session, params.Body); err != nil { + if err := getAddGroupResponse(session, params); err != nil { return groupApi.NewAddGroupDefault(int(err.Code)).WithPayload(err) } return groupApi.NewAddGroupCreated() @@ -71,12 +71,12 @@ func registerGroupsHandlers(api *operations.ConsoleAPI) { } // getListGroupsResponse performs listGroups() and serializes it to the handler's output -func getListGroupsResponse(session *models.Principal) (*models.ListGroupsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListGroupsResponse(session *models.Principal, params groupApi.ListGroupsParams) (*models.ListGroupsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -84,7 +84,7 @@ func getListGroupsResponse(session *models.Principal) (*models.ListGroupsRespons groups, err := adminClient.listGroups(ctx) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output @@ -107,11 +107,11 @@ func groupInfo(ctx context.Context, client MinioAdmin, group string) (*madmin.Gr // getGroupInfoResponse performs groupInfo() and serializes it to the handler's output func getGroupInfoResponse(session *models.Principal, params groupApi.GroupInfoParams) (*models.Group, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -119,7 +119,7 @@ func getGroupInfoResponse(session *models.Principal, params groupApi.GroupInfoPa groupDesc, err := groupInfo(ctx, adminClient, params.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } groupResponse := &models.Group{ @@ -146,16 +146,17 @@ func addGroup(ctx context.Context, client MinioAdmin, group string, members []st } // getAddGroupResponse performs addGroup() and serializes it to the handler's output -func getAddGroupResponse(session *models.Principal, params *models.AddGroupRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getAddGroupResponse(session *models.Principal, params groupApi.AddGroupParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // AddGroup request needed to proceed - if params == nil { - return prepareError(errGroupBodyNotInRequest) + if params.Body == nil { + return ErrorWithContext(ctx, ErrGroupBodyNotInRequest) } + groupRequest := params.Body mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -164,13 +165,13 @@ func getAddGroupResponse(session *models.Principal, params *models.AddGroupReque groupList, _ := adminClient.listGroups(ctx) for _, b := range groupList { - if b == *params.Group { - return prepareError(errGroupAlreadyExists) + if b == *groupRequest.Group { + return ErrorWithContext(ctx, ErrGroupAlreadyExists) } } - if err := addGroup(ctx, adminClient, *params.Group, params.Members); err != nil { - return prepareError(err) + if err := addGroup(ctx, adminClient, *groupRequest.Group, groupRequest.Members); err != nil { + return ErrorWithContext(ctx, err) } return nil } @@ -191,21 +192,21 @@ func removeGroup(ctx context.Context, client MinioAdmin, group string) error { // getRemoveGroupResponse performs removeGroup() and serializes it to the handler's output func getRemoveGroupResponse(session *models.Principal, params groupApi.RemoveGroupParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if params.Name == "" { - return prepareError(errGroupNameNotInRequest) + return ErrorWithContext(ctx, ErrGroupNameNotInRequest) } mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // createad a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} if err := removeGroup(ctx, adminClient, params.Name); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -265,13 +266,13 @@ func setGroupStatus(ctx context.Context, client MinioAdmin, group, status string // also sets the group's status if status in the request is different than the current one. // Then serializes the output to be used by the handler. func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGroupParams) (*models.Group, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if params.Name == "" { - return nil, prepareError(errGroupNameNotInRequest) + return nil, ErrorWithContext(ctx, ErrGroupNameNotInRequest) } if params.Body == nil { - return nil, prepareError(errGroupBodyNotInRequest) + return nil, ErrorWithContext(ctx, ErrGroupBodyNotInRequest) } expectedGroupUpdate := params.Body @@ -279,7 +280,7 @@ func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGro mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -287,7 +288,7 @@ func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGro groupUpdated, err := groupUpdate(ctx, adminClient, groupName, expectedGroupUpdate) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } groupResponse := &models.Group{ Name: groupUpdated.Name, diff --git a/restapi/admin_heal.go b/restapi/admin_heal.go index edd76f553..3eadfc1aa 100644 --- a/restapi/admin_heal.go +++ b/restapi/admin_heal.go @@ -56,7 +56,7 @@ var ( type healItemStatus struct { Status string `json:"status"` - Error string `json:"error,omitempty"` + Error string `json:"errors,omitempty"` Type string `json:"type"` Name string `json:"name"` Before struct { @@ -143,7 +143,7 @@ func startHeal(ctx context.Context, conn WSConn, client MinioAdmin, hOpts *healO } if res.Summary == "stopped" { - return fmt.Errorf("heal had an error - %s", res.FailureDetail) + return fmt.Errorf("heal had an errors - %s", res.FailureDetail) } time.Sleep(time.Second) diff --git a/restapi/admin_info.go b/restapi/admin_info.go index 91693f2b9..83a310864 100644 --- a/restapi/admin_info.go +++ b/restapi/admin_info.go @@ -826,6 +826,8 @@ type LabelResults struct { // getAdminInfoResponse returns the response containing total buckets, objects and usage. func getAdminInfoResponse(session *models.Principal, params systemApi.AdminInfoParams) (*models.AdminInfoResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() prometheusURL := "" if !*params.DefaultOnly { @@ -834,35 +836,30 @@ func getAdminInfoResponse(session *models.Principal, params systemApi.AdminInfoP mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } - sessionResp, err2 := getUsageWidgetsForDeployment(prometheusURL, mAdmin) + sessionResp, err2 := getUsageWidgetsForDeployment(ctx, prometheusURL, mAdmin) if err2 != nil { - return nil, err2 + return nil, ErrorWithContext(ctx, err2) } return sessionResp, nil } -func getUsageWidgetsForDeployment(prometheusURL string, mAdmin *madmin.AdminClient) (*models.AdminInfoResponse, *models.Error) { +func getUsageWidgetsForDeployment(ctx context.Context, prometheusURL string, mAdmin *madmin.AdminClient) (*models.AdminInfoResponse, error) { prometheusNotReady := false - - if prometheusURL != "" && !testPrometheusURL(prometheusURL) { + if prometheusURL != "" && !testPrometheusURL(ctx, prometheusURL) { prometheusNotReady = true } if prometheusURL == "" || prometheusNotReady { // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - defer cancel() // serialize output usage, err := GetAdminInfo(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, err } sessionResp := &models.AdminInfoResponse{ Buckets: usage.Buckets, @@ -901,80 +898,69 @@ func getUsageWidgetsForDeployment(prometheusURL string, mAdmin *madmin.AdminClie return sessionResp, nil } -func unmarshalPrometheus(endpoint string, data interface{}) bool { +func unmarshalPrometheus(ctx context.Context, endpoint string, data interface{}) bool { httpClnt := GetConsoleHTTPClient() resp, err := httpClnt.Get(endpoint) if err != nil { - LogError("Unable to fetch labels from prometheus (%s)", resp.Status) + ErrorWithContext(ctx, fmt.Errorf("Unable to fetch labels from prometheus (%s)", resp.Status)) return true } defer resp.Body.Close() if resp.StatusCode != 200 { - LogError("Unexpected error from prometheus (%s)", resp.Status) + ErrorWithContext(ctx, fmt.Errorf("Unexpected errors from prometheus (%s)", resp.Status)) return true } if err = json.NewDecoder(resp.Body).Decode(data); err != nil { - LogError("Unexpected error reading response from prometheus, %v", err) + ErrorWithContext(ctx, fmt.Errorf("Unexpected errors from prometheus (%s)", resp.Status)) return true } return false } -func testPrometheusURL(url string) bool { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func testPrometheusURL(ctx context.Context, url string) bool { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/-/healthy", nil) - if err != nil { - LogError("Error Building Request: (%v)", err) + ErrorWithContext(ctx, fmt.Errorf("error Building Request: (%v)", err)) return false } - response, err := GetConsoleHTTPClient().Do(req) - if err != nil { - LogError("Default Prometheus URL not reachable, trying root testing: (%v)", err) - + ErrorWithContext(ctx, fmt.Errorf("default Prometheus URL not reachable, trying root testing: (%v)", err)) newTestURL := req.URL.Scheme + "://" + req.URL.Host + "/-/healthy" - req2, err := http.NewRequestWithContext(ctx, http.MethodGet, newTestURL, nil) - if err != nil { - LogError("Error Building Root Request: (%v)", err) + ErrorWithContext(ctx, fmt.Errorf("error Building Root Request: (%v)", err)) return false } - rootResponse, err := GetConsoleHTTPClient().Do(req2) - if err != nil { // URL & Root tests didn't work. Prometheus not reachable - LogError("Root Prometheus URL not reachable: (%v)", err) + ErrorWithContext(ctx, fmt.Errorf("root Prometheus URL not reachable: (%v)", err)) return false } - return rootResponse.StatusCode == http.StatusOK } - return response.StatusCode == http.StatusOK } func getAdminInfoWidgetResponse(params systemApi.DashboardWidgetDetailsParams) (*models.WidgetDetails, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() prometheusURL := getPrometheusURL() prometheusJobID := getPrometheusJobID() // We test if prometheus URL is reachable. this is meant to avoid unuseful calls and application hang. - if !testPrometheusURL(prometheusURL) { - error := errors.New("Prometheus URL is unreachable") - return nil, prepareError(error) + if !testPrometheusURL(ctx, prometheusURL) { + return nil, ErrorWithContext(ctx, errors.New("Prometheus URL is unreachable")) } - return getWidgetDetails(prometheusURL, prometheusJobID, params.WidgetID, params.Step, params.Start, params.End) + return getWidgetDetails(ctx, prometheusURL, prometheusJobID, params.WidgetID, params.Step, params.Start, params.End) } -func getWidgetDetails(prometheusURL string, prometheusJobID string, widgetID int32, step *int32, start *int64, end *int64) (*models.WidgetDetails, *models.Error) { +func getWidgetDetails(ctx context.Context, prometheusURL string, prometheusJobID string, widgetID int32, step *int32, start *int64, end *int64) (*models.WidgetDetails, *models.Error) { labelResultsCh := make(chan LabelResults) for _, lbl := range labels { @@ -982,7 +968,7 @@ func getWidgetDetails(prometheusURL string, prometheusJobID string, widgetID int endpoint := fmt.Sprintf("%s/api/v1/label/%s/values", prometheusURL, lbl.Name) var response LabelResponse - if unmarshalPrometheus(endpoint, &response) { + if unmarshalPrometheus(ctx, endpoint, &response) { return } @@ -1054,7 +1040,7 @@ LabelsWaitLoop: endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", prometheusURL, apiType, url.QueryEscape(queryExpr), extraParamters) var response PromResp - if unmarshalPrometheus(endpoint, &response) { + if unmarshalPrometheus(ctx, endpoint, &response) { return } diff --git a/restapi/admin_inspect.go b/restapi/admin_inspect.go index 8e740f79d..95acb3f59 100644 --- a/restapi/admin_inspect.go +++ b/restapi/admin_inspect.go @@ -49,11 +49,11 @@ func registerInspectHandler(api *operations.ConsoleAPI) { } func getInspectResult(session *models.Principal, params *inspectApi.InspectParams) (*[32]byte, io.ReadCloser, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, nil, prepareError(err) + return nil, nil, ErrorWithContext(ctx, err) } var cfg madmin.InspectOptions @@ -67,7 +67,7 @@ func getInspectResult(session *models.Principal, params *inspectApi.InspectParam k, r, err := adminClient.inspect(ctx, cfg) if err != nil { - return nil, nil, prepareError(err) + return nil, nil, ErrorWithContext(ctx, err) } return &k, r, nil } diff --git a/restapi/admin_nodes.go b/restapi/admin_nodes.go index 72d2d7c5b..5c486ef7f 100644 --- a/restapi/admin_nodes.go +++ b/restapi/admin_nodes.go @@ -28,7 +28,7 @@ import ( func registerNodesHandler(api *operations.ConsoleAPI) { api.SystemListNodesHandler = systemApi.ListNodesHandlerFunc(func(params systemApi.ListNodesParams, session *models.Principal) middleware.Responder { - listNodesResponse, err := getListNodesResponse(session) + listNodesResponse, err := getListNodesResponse(session, params) if err != nil { return systemApi.NewListNodesDefault(int(err.Code)).WithPayload(err) } @@ -37,12 +37,12 @@ func registerNodesHandler(api *operations.ConsoleAPI) { } // getListNodesResponse returns a list of available node endpoints . -func getListNodesResponse(session *models.Principal) ([]string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListNodesResponse(session *models.Principal, params systemApi.ListNodesParams) ([]string, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } var nodeList []string diff --git a/restapi/admin_notification_endpoints.go b/restapi/admin_notification_endpoints.go index 9ab9c5f49..1eefb092d 100644 --- a/restapi/admin_notification_endpoints.go +++ b/restapi/admin_notification_endpoints.go @@ -29,7 +29,7 @@ import ( func registerAdminNotificationEndpointsHandlers(api *operations.ConsoleAPI) { // return a list of notification endpoints api.ConfigurationNotificationEndpointListHandler = configurationApi.NotificationEndpointListHandlerFunc(func(params configurationApi.NotificationEndpointListParams, session *models.Principal) middleware.Responder { - notifEndpoints, err := getNotificationEndpointsResponse(session) + notifEndpoints, err := getNotificationEndpointsResponse(session, params) if err != nil { return configurationApi.NewNotificationEndpointListDefault(int(err.Code)).WithPayload(err) } @@ -37,7 +37,7 @@ func registerAdminNotificationEndpointsHandlers(api *operations.ConsoleAPI) { }) // add a new notification endpoints api.ConfigurationAddNotificationEndpointHandler = configurationApi.AddNotificationEndpointHandlerFunc(func(params configurationApi.AddNotificationEndpointParams, session *models.Principal) middleware.Responder { - notifEndpoints, err := getAddNotificationEndpointResponse(session, ¶ms) + notifEndpoints, err := getAddNotificationEndpointResponse(session, params) if err != nil { return configurationApi.NewAddNotificationEndpointDefault(int(err.Code)).WithPayload(err) } @@ -75,20 +75,20 @@ func getNotificationEndpoints(ctx context.Context, client MinioAdmin) (*models.N } // getNotificationEndpointsResponse returns a list of notification endpoints in the instance -func getNotificationEndpointsResponse(session *models.Principal) (*models.NotifEndpointResponse, *models.Error) { +func getNotificationEndpointsResponse(session *models.Principal, params configurationApi.NotificationEndpointListParams) (*models.NotifEndpointResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output notfEndpointResp, err := getNotificationEndpoints(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return notfEndpointResp, nil } @@ -145,20 +145,20 @@ func addNotificationEndpoint(ctx context.Context, client MinioAdmin, params *con } // getNotificationEndpointsResponse returns a list of notification endpoints in the instance -func getAddNotificationEndpointResponse(session *models.Principal, params *configurationApi.AddNotificationEndpointParams) (*models.SetNotificationEndpointResponse, *models.Error) { +func getAddNotificationEndpointResponse(session *models.Principal, params configurationApi.AddNotificationEndpointParams) (*models.SetNotificationEndpointResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output - notfEndpointResp, err := addNotificationEndpoint(ctx, adminClient, params) + notfEndpointResp, err := addNotificationEndpoint(ctx, adminClient, ¶ms) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return notfEndpointResp, nil } diff --git a/restapi/admin_policies.go b/restapi/admin_policies.go index f45b818f0..79fd8f14d 100644 --- a/restapi/admin_policies.go +++ b/restapi/admin_policies.go @@ -28,7 +28,6 @@ import ( policyApi "github.com/minio/console/restapi/operations/policy" "github.com/go-openapi/runtime/middleware" - "github.com/go-openapi/swag" "github.com/minio/console/models" "github.com/minio/console/restapi/operations" iampolicy "github.com/minio/pkg/iam/policy" @@ -37,7 +36,7 @@ import ( func registersPoliciesHandler(api *operations.ConsoleAPI) { // List Policies api.PolicyListPoliciesHandler = policyApi.ListPoliciesHandlerFunc(func(params policyApi.ListPoliciesParams, session *models.Principal) middleware.Responder { - listPoliciesResponse, err := getListPoliciesResponse(session) + listPoliciesResponse, err := getListPoliciesResponse(session, params) if err != nil { return policyApi.NewListPoliciesDefault(int(err.Code)).WithPayload(err) } @@ -53,7 +52,7 @@ func registersPoliciesHandler(api *operations.ConsoleAPI) { }) // Add Policy api.PolicyAddPolicyHandler = policyApi.AddPolicyHandlerFunc(func(params policyApi.AddPolicyParams, session *models.Principal) middleware.Responder { - policyResponse, err := getAddPolicyResponse(session, params.Body) + policyResponse, err := getAddPolicyResponse(session, params) if err != nil { return policyApi.NewAddPolicyDefault(int(err.Code)).WithPayload(err) } @@ -68,55 +67,55 @@ func registersPoliciesHandler(api *operations.ConsoleAPI) { }) // Set Policy api.PolicySetPolicyHandler = policyApi.SetPolicyHandlerFunc(func(params policyApi.SetPolicyParams, session *models.Principal) middleware.Responder { - if err := getSetPolicyResponse(session, params.Body); err != nil { + if err := getSetPolicyResponse(session, params); err != nil { return policyApi.NewSetPolicyDefault(int(err.Code)).WithPayload(err) } return policyApi.NewSetPolicyNoContent() }) // Set Policy Multiple User/Groups api.PolicySetPolicyMultipleHandler = policyApi.SetPolicyMultipleHandlerFunc(func(params policyApi.SetPolicyMultipleParams, session *models.Principal) middleware.Responder { - if err := getSetPolicyMultipleResponse(session, params.Body); err != nil { + if err := getSetPolicyMultipleResponse(session, params); err != nil { return policyApi.NewSetPolicyMultipleDefault(int(err.Code)).WithPayload(err) } return policyApi.NewSetPolicyMultipleNoContent() }) api.BucketListPoliciesWithBucketHandler = bucketApi.ListPoliciesWithBucketHandlerFunc(func(params bucketApi.ListPoliciesWithBucketParams, session *models.Principal) middleware.Responder { - policyResponse, err := getListPoliciesWithBucketResponse(session, params.Bucket) + policyResponse, err := getListPoliciesWithBucketResponse(session, params) if err != nil { return bucketApi.NewListPoliciesWithBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewListPoliciesWithBucketOK().WithPayload(policyResponse) }) api.BucketListAccessRulesWithBucketHandler = bucketApi.ListAccessRulesWithBucketHandlerFunc(func(params bucketApi.ListAccessRulesWithBucketParams, session *models.Principal) middleware.Responder { - policyResponse, err := getListAccessRulesWithBucketResponse(session, params.Bucket) + policyResponse, err := getListAccessRulesWithBucketResponse(session, params) if err != nil { return bucketApi.NewListAccessRulesWithBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewListAccessRulesWithBucketOK().WithPayload(policyResponse) }) api.BucketSetAccessRuleWithBucketHandler = bucketApi.SetAccessRuleWithBucketHandlerFunc(func(params bucketApi.SetAccessRuleWithBucketParams, session *models.Principal) middleware.Responder { - policyResponse, err := getSetAccessRuleWithBucketResponse(session, params.Bucket, params.Prefixaccess) + policyResponse, err := getSetAccessRuleWithBucketResponse(session, params) if err != nil { return bucketApi.NewSetAccessRuleWithBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewSetAccessRuleWithBucketOK().WithPayload(policyResponse) }) api.BucketDeleteAccessRuleWithBucketHandler = bucketApi.DeleteAccessRuleWithBucketHandlerFunc(func(params bucketApi.DeleteAccessRuleWithBucketParams, session *models.Principal) middleware.Responder { - policyResponse, err := getDeleteAccessRuleWithBucketResponse(session, params.Bucket, params.Prefix) + policyResponse, err := getDeleteAccessRuleWithBucketResponse(session, params) if err != nil { return bucketApi.NewDeleteAccessRuleWithBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteAccessRuleWithBucketOK().WithPayload(policyResponse) }) api.PolicyListUsersForPolicyHandler = policyApi.ListUsersForPolicyHandlerFunc(func(params policyApi.ListUsersForPolicyParams, session *models.Principal) middleware.Responder { - policyUsersResponse, err := getListUsersForPolicyResponse(session, params.Policy) + policyUsersResponse, err := getListUsersForPolicyResponse(session, params) if err != nil { return policyApi.NewListUsersForPolicyDefault(int(err.Code)).WithPayload(err) } return policyApi.NewListUsersForPolicyOK().WithPayload(policyUsersResponse) }) api.PolicyListGroupsForPolicyHandler = policyApi.ListGroupsForPolicyHandlerFunc(func(params policyApi.ListGroupsForPolicyParams, session *models.Principal) middleware.Responder { - policyGroupsResponse, err := getListGroupsForPolicyResponse(session, params.Policy) + policyGroupsResponse, err := getListGroupsForPolicyResponse(session, params) if err != nil { return policyApi.NewListGroupsForPolicyDefault(int(err.Code)).WithPayload(err) } @@ -124,12 +123,13 @@ func registersPoliciesHandler(api *operations.ConsoleAPI) { }) } -func getListAccessRulesWithBucketResponse(session *models.Principal, bucket string) (*models.ListAccessRulesResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListAccessRulesWithBucketResponse(session *models.Principal, params bucketApi.ListAccessRulesWithBucketParams) (*models.ListAccessRulesResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + bucket := params.Bucket client, err := newS3BucketClient(session, bucket, "") if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } accessRules, _ := client.GetAccessRules(ctx) var accessRuleList []*models.AccessRule @@ -139,48 +139,51 @@ func getListAccessRulesWithBucketResponse(session *models.Principal, bucket stri return &models.ListAccessRulesResponse{AccessRules: accessRuleList}, nil } -func getSetAccessRuleWithBucketResponse(session *models.Principal, bucket string, prefixAccess *models.PrefixAccessPair) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getSetAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.SetAccessRuleWithBucketParams) (bool, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - client, err := newS3BucketClient(session, bucket, prefixAccess.Prefix) + prefixAccess := params.Prefixaccess + client, err := newS3BucketClient(session, params.Bucket, prefixAccess.Prefix) if err != nil { - return false, prepareError(err) + return false, ErrorWithContext(ctx, err) } errorVal := client.SetAccess(ctx, prefixAccess.Access, false) if errorVal != nil { - return false, prepareError(errorVal.Cause) + return false, ErrorWithContext(ctx, errorVal.Cause) } return true, nil } -func getDeleteAccessRuleWithBucketResponse(session *models.Principal, bucket string, prefix *models.PrefixWrapper) (bool, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getDeleteAccessRuleWithBucketResponse(session *models.Principal, params bucketApi.DeleteAccessRuleWithBucketParams) (bool, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + bucket := params.Bucket + prefix := params.Prefix client, err := newS3BucketClient(session, bucket, prefix.Prefix) if err != nil { - return false, prepareError(err) + return false, ErrorWithContext(ctx, err) } errorVal := client.SetAccess(ctx, "none", false) if errorVal != nil { - return false, prepareError(errorVal.Cause) + return false, ErrorWithContext(ctx, errorVal.Cause) } return true, nil } -func getListPoliciesWithBucketResponse(session *models.Principal, bucket string) (*models.ListPoliciesResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListPoliciesWithBucketResponse(session *models.Principal, params bucketApi.ListPoliciesWithBucketParams) (*models.ListPoliciesResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - policies, err := listPoliciesWithBucket(ctx, bucket, adminClient) + policies, err := listPoliciesWithBucket(ctx, params.Bucket, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output listPoliciesResponse := &models.ListPoliciesResponse{ @@ -205,18 +208,18 @@ func listPoliciesWithBucket(ctx context.Context, bucket string, client MinioAdmi if err != nil { return nil, err } - if policyMatchesBucket(policy, bucket) { + if policyMatchesBucket(ctx, policy, bucket) { policies = append(policies, policy) } } return policies, nil } -func policyMatchesBucket(policy *models.Policy, bucket string) bool { +func policyMatchesBucket(ctx context.Context, policy *models.Policy, bucket string) bool { policyData := &iampolicy.Policy{} err := json.Unmarshal([]byte(policy.Policy), policyData) if err != nil { - LogError("error parsing policy: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error parsing policy: %v", err)) return false } policyStatements := policyData.Statements @@ -253,12 +256,12 @@ func listPolicies(ctx context.Context, client MinioAdmin) ([]*models.Policy, err } // getListPoliciesResponse performs listPolicies() and serializes it to the handler's output -func getListPoliciesResponse(session *models.Principal) (*models.ListPoliciesResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListPoliciesResponse(session *models.Principal, params policyApi.ListPoliciesParams) (*models.ListPoliciesResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used @@ -266,7 +269,7 @@ func getListPoliciesResponse(session *models.Principal) (*models.ListPoliciesRes policies, err := listPolicies(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output listPoliciesResponse := &models.ListPoliciesResponse{ @@ -277,19 +280,20 @@ func getListPoliciesResponse(session *models.Principal) (*models.ListPoliciesRes } // getListUsersForPoliciesResponse performs lists users affected by a given policy. -func getListUsersForPolicyResponse(session *models.Principal, policy string) ([]string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListUsersForPolicyResponse(session *models.Principal, params policyApi.ListUsersForPolicyParams) ([]string, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + policy := params.Policy mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} policies, err := listPolicies(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } found := false for i := range policies { @@ -298,15 +302,11 @@ func getListUsersForPolicyResponse(session *models.Principal, policy string) ([] } } if !found { - return nil, &models.Error{ - Code: int32(404), - Message: swag.String("Policy does not exist"), - DetailedMessage: swag.String(fmt.Sprintf("The policy %s does not extist", policy)), - } + return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", policy)) } users, err := listUsers(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } var filteredUsers []string @@ -322,19 +322,20 @@ func getListUsersForPolicyResponse(session *models.Principal, policy string) ([] return filteredUsers, nil } -func getListGroupsForPolicyResponse(session *models.Principal, policy string) ([]string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListGroupsForPolicyResponse(session *models.Principal, params policyApi.ListGroupsForPolicyParams) ([]string, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + policy := params.Policy mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} policies, err := listPolicies(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } found := false for i := range policies { @@ -343,23 +344,19 @@ func getListGroupsForPolicyResponse(session *models.Principal, policy string) ([ } } if !found { - return nil, &models.Error{ - Code: int32(404), - Message: swag.String("Policy does not exist"), - DetailedMessage: swag.String(fmt.Sprintf("The policy %s does not extist", policy)), - } + return nil, ErrorWithContext(ctx, ErrPolicyNotFound, fmt.Errorf("the policy %s does not exist", policy)) } groups, err := adminClient.listGroups(ctx) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } var filteredGroups []string for _, group := range groups { info, err := groupInfo(ctx, adminClient, group) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } if info.Policy == policy { filteredGroups = append(filteredGroups, group) @@ -380,21 +377,21 @@ func removePolicy(ctx context.Context, client MinioAdmin, name string) error { // getRemovePolicyResponse() performs removePolicy() and serializes it to the handler's output func getRemovePolicyResponse(session *models.Principal, params policyApi.RemovePolicyParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if params.Name == "" { - return prepareError(errPolicyNameNotInRequest) + return ErrorWithContext(ctx, ErrPolicyNameNotInRequest) } mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} if err := removePolicy(ctx, adminClient, params.Name); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -419,23 +416,22 @@ func addPolicy(ctx context.Context, client MinioAdmin, name, policy string) (*mo } // getAddPolicyResponse performs addPolicy() and serializes it to the handler's output -func getAddPolicyResponse(session *models.Principal, params *models.AddPolicyRequest) (*models.Policy, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getAddPolicyResponse(session *models.Principal, params policyApi.AddPolicyParams) (*models.Policy, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - if params == nil { - return nil, prepareError(errPolicyBodyNotInRequest) + if params.Body == nil { + return nil, ErrorWithContext(ctx, ErrPolicyBodyNotInRequest) } - mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - policy, err := addPolicy(ctx, adminClient, *params.Name, *params.Policy) + policy, err := addPolicy(ctx, adminClient, *params.Body.Name, *params.Body.Policy) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return policy, nil } @@ -458,18 +454,18 @@ func policyInfo(ctx context.Context, client MinioAdmin, name string) (*models.Po // getPolicyInfoResponse performs policyInfo() and serializes it to the handler's output func getPolicyInfoResponse(session *models.Principal, params policyApi.PolicyInfoParams) (*models.Policy, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} policy, err := policyInfo(ctx, adminClient, params.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return policy, nil } @@ -484,40 +480,37 @@ func setPolicy(ctx context.Context, client MinioAdmin, name, entityName string, } // getSetPolicyResponse() performs setPolicy() and serializes it to the handler's output -func getSetPolicyResponse(session *models.Principal, params *models.SetPolicyNameRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getSetPolicyResponse(session *models.Principal, params policyApi.SetPolicyParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - // if len(params.Name) == 0 { - // return prepareError(errPolicyNameNotInRequest) - // } // Removing this section mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - if err := setPolicy(ctx, adminClient, strings.Join(params.Name, ","), *params.EntityName, *params.EntityType); err != nil { - return prepareError(err) + if err := setPolicy(ctx, adminClient, strings.Join(params.Body.Name, ","), *params.Body.EntityName, *params.Body.EntityType); err != nil { + return ErrorWithContext(ctx, err) } return nil } -func getSetPolicyMultipleResponse(session *models.Principal, params *models.SetPolicyMultipleNameRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getSetPolicyMultipleResponse(session *models.Principal, params policyApi.SetPolicyMultipleParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - if err := setPolicyMultipleEntities(ctx, adminClient, strings.Join(params.Name, ","), params.Users, params.Groups); err != nil { - return prepareError(err) + if err := setPolicyMultipleEntities(ctx, adminClient, strings.Join(params.Body.Name, ","), params.Body.Users, params.Body.Groups); err != nil { + return ErrorWithContext(ctx, err) } return nil } diff --git a/restapi/admin_policies_test.go b/restapi/admin_policies_test.go index d92f15613..dc7010881 100644 --- a/restapi/admin_policies_test.go +++ b/restapi/admin_policies_test.go @@ -300,6 +300,7 @@ func Test_SetPolicyMultiple(t *testing.T) { func Test_policyMatchesBucket(t *testing.T) { type args struct { + ctx context.Context policy *models.Policy bucket string } @@ -310,7 +311,7 @@ func Test_policyMatchesBucket(t *testing.T) { }{ { name: "Test1", - args: args{policy: &models.Policy{Name: "consoleAdmin", Policy: `{ + args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{ "Version": "2012-10-17", "Statement": [ { @@ -334,7 +335,7 @@ func Test_policyMatchesBucket(t *testing.T) { }, { name: "Test2", - args: args{policy: &models.Policy{Name: "consoleAdmin", Policy: `{ + args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{ "Version": "2012-10-17", "Statement": [ { @@ -352,7 +353,7 @@ func Test_policyMatchesBucket(t *testing.T) { }, { name: "Test3", - args: args{policy: &models.Policy{Name: "consoleAdmin", Policy: `{ + args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{ "Version": "2012-10-17", "Statement": [ { @@ -388,7 +389,7 @@ func Test_policyMatchesBucket(t *testing.T) { }, { name: "Test4", - args: args{policy: &models.Policy{Name: "consoleAdmin", Policy: `{ + args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{ "Version": "2012-10-17", "Statement": [ { @@ -407,7 +408,7 @@ func Test_policyMatchesBucket(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := policyMatchesBucket(tt.args.policy, tt.args.bucket); got != tt.want { + if got := policyMatchesBucket(tt.args.ctx, tt.args.policy, tt.args.bucket); got != tt.want { t.Errorf("policyMatchesBucket() = %v, want %v", got, tt.want) } }) diff --git a/restapi/admin_profiling.go b/restapi/admin_profiling.go index 9103f92f9..91f764237 100644 --- a/restapi/admin_profiling.go +++ b/restapi/admin_profiling.go @@ -32,7 +32,7 @@ import ( func registerProfilingHandler(api *operations.ConsoleAPI) { // Start Profiling api.ProfileProfilingStartHandler = profileApi.ProfilingStartHandlerFunc(func(params profileApi.ProfilingStartParams, session *models.Principal) middleware.Responder { - profilingStartResponse, err := getProfilingStartResponse(session, params.Body) + profilingStartResponse, err := getProfilingStartResponse(session, params) if err != nil { return profileApi.NewProfilingStartDefault(int(err.Code)).WithPayload(err) } @@ -40,7 +40,7 @@ func registerProfilingHandler(api *operations.ConsoleAPI) { }) // Stop and download profiling data api.ProfileProfilingStopHandler = profileApi.ProfilingStopHandlerFunc(func(params profileApi.ProfilingStopParams, session *models.Principal) middleware.Responder { - profilingStopResponse, err := getProfilingStopResponse(session) + profilingStopResponse, err := getProfilingStopResponse(session, params) if err != nil { return profileApi.NewProfilingStopDefault(int(err.Code)).WithPayload(err) } @@ -63,7 +63,7 @@ func registerProfilingHandler(api *operations.ConsoleAPI) { // { // "Success": true, // "nodeName": "127.0.0.1:9000" -// "error": "" +// "errors": "" // } func startProfiling(ctx context.Context, client MinioAdmin, profilerType string) ([]*models.StartProfilingItem, error) { profilingResults, err := client.startProfiling(ctx, madmin.ProfilerType(profilerType)) @@ -82,22 +82,22 @@ func startProfiling(ctx context.Context, client MinioAdmin, profilerType string) } // getProfilingStartResponse performs startProfiling() and serializes it to the handler's output -func getProfilingStartResponse(session *models.Principal, params *models.ProfilingStartRequest) (*models.StartProfilingList, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getProfilingStartResponse(session *models.Principal, params profileApi.ProfilingStartParams) (*models.StartProfilingList, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - if params == nil { - return nil, prepareError(errPolicyBodyNotInRequest) + if params.Body == nil { + return nil, ErrorWithContext(ctx, ErrPolicyBodyNotInRequest) } mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - profilingItems, err := startProfiling(ctx, adminClient, *params.Type) + profilingItems, err := startProfiling(ctx, adminClient, *params.Body.Type) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } profilingList := &models.StartProfilingList{ StartResults: profilingItems, @@ -117,18 +117,18 @@ func stopProfiling(ctx context.Context, client MinioAdmin) (io.ReadCloser, error } // getProfilingStopResponse() performs setPolicy() and serializes it to the handler's output -func getProfilingStopResponse(session *models.Principal) (io.ReadCloser, *models.Error) { - ctx := context.Background() +func getProfilingStopResponse(session *models.Principal, params profileApi.ProfilingStopParams) (io.ReadCloser, *models.Error) { + ctx := params.HTTPRequest.Context() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} profilingData, err := stopProfiling(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return profilingData, nil } diff --git a/restapi/admin_remote_buckets.go b/restapi/admin_remote_buckets.go index 37ac73071..d4956bd07 100644 --- a/restapi/admin_remote_buckets.go +++ b/restapi/admin_remote_buckets.go @@ -43,9 +43,9 @@ type RemoteBucketResult struct { func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { // return list of remote buckets api.BucketListRemoteBucketsHandler = bucketApi.ListRemoteBucketsHandlerFunc(func(params bucketApi.ListRemoteBucketsParams, session *models.Principal) middleware.Responder { - listResp, err := getListRemoteBucketsResponse(session) + listResp, err := getListRemoteBucketsResponse(session, params) if err != nil { - return bucketApi.NewListRemoteBucketsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewListRemoteBucketsDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewListRemoteBucketsOK().WithPayload(listResp) }) @@ -54,7 +54,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { api.BucketRemoteBucketDetailsHandler = bucketApi.RemoteBucketDetailsHandlerFunc(func(params bucketApi.RemoteBucketDetailsParams, session *models.Principal) middleware.Responder { response, err := getRemoteBucketDetailsResponse(session, params) if err != nil { - return bucketApi.NewRemoteBucketDetailsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewRemoteBucketDetailsDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewRemoteBucketDetailsOK().WithPayload(response) }) @@ -63,7 +63,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { api.BucketDeleteRemoteBucketHandler = bucketApi.DeleteRemoteBucketHandlerFunc(func(params bucketApi.DeleteRemoteBucketParams, session *models.Principal) middleware.Responder { err := getDeleteRemoteBucketResponse(session, params) if err != nil { - return bucketApi.NewDeleteRemoteBucketDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewDeleteRemoteBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteRemoteBucketNoContent() }) @@ -72,7 +72,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { api.BucketAddRemoteBucketHandler = bucketApi.AddRemoteBucketHandlerFunc(func(params bucketApi.AddRemoteBucketParams, session *models.Principal) middleware.Responder { err := getAddRemoteBucketResponse(session, params) if err != nil { - return bucketApi.NewAddRemoteBucketDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewAddRemoteBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewAddRemoteBucketCreated() }) @@ -82,7 +82,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { response, err := setMultiBucketReplicationResponse(session, params) if err != nil { - return bucketApi.NewSetMultiBucketReplicationDefault(500).WithPayload(err) + return bucketApi.NewSetMultiBucketReplicationDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewSetMultiBucketReplicationOK().WithPayload(response) @@ -93,7 +93,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { response, err := listExternalBucketsResponse(params) if err != nil { - return bucketApi.NewListExternalBucketsDefault(500).WithPayload(err) + return bucketApi.NewListExternalBucketsDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewListExternalBucketsOK().WithPayload(response) @@ -104,7 +104,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { err := deleteReplicationRuleResponse(session, params) if err != nil { - return bucketApi.NewDeleteBucketReplicationRuleDefault(500).WithPayload(err) + return bucketApi.NewDeleteBucketReplicationRuleDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteBucketReplicationRuleNoContent() @@ -115,7 +115,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { err := deleteBucketReplicationRulesResponse(session, params) if err != nil { - return bucketApi.NewDeleteAllReplicationRulesDefault(500).WithPayload(err) + return bucketApi.NewDeleteAllReplicationRulesDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteAllReplicationRulesNoContent() @@ -126,7 +126,7 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { err := deleteSelectedReplicationRulesResponse(session, params) if err != nil { - return bucketApi.NewDeleteSelectedReplicationRulesDefault(500).WithPayload(err) + return bucketApi.NewDeleteSelectedReplicationRulesDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteSelectedReplicationRulesNoContent() @@ -136,25 +136,23 @@ func registerAdminBucketRemoteHandlers(api *operations.ConsoleAPI) { api.BucketUpdateMultiBucketReplicationHandler = bucketApi.UpdateMultiBucketReplicationHandlerFunc(func(params bucketApi.UpdateMultiBucketReplicationParams, session *models.Principal) middleware.Responder { err := updateBucketReplicationResponse(session, params) if err != nil { - return bucketApi.NewUpdateMultiBucketReplicationDefault(500).WithPayload(err) + return bucketApi.NewUpdateMultiBucketReplicationDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewUpdateMultiBucketReplicationCreated() }) } -func getListRemoteBucketsResponse(session *models.Principal) (*models.ListRemoteBucketsResponse, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListRemoteBucketsResponse(session *models.Principal, params bucketApi.ListRemoteBucketsParams) (*models.ListRemoteBucketsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Madmin Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} buckets, err := listRemoteBuckets(ctx, adminClient) if err != nil { - LogError("error listing remote buckets: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error listing remote buckets: %v", err)) } return &models.ListRemoteBucketsResponse{ Buckets: buckets, @@ -162,55 +160,49 @@ func getListRemoteBucketsResponse(session *models.Principal) (*models.ListRemote }, nil } -func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi.RemoteBucketDetailsParams) (*models.RemoteBucket, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi.RemoteBucketDetailsParams) (*models.RemoteBucket, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Madmin Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} bucket, err := getRemoteBucket(ctx, adminClient, params.Name) if err != nil { - LogError("error getting remote bucket details: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error getting remote bucket details: %v", err)) } return bucket, nil } -func getDeleteRemoteBucketResponse(session *models.Principal, params bucketApi.DeleteRemoteBucketParams) error { - ctx, cancel := context.WithCancel(context.Background()) +func getDeleteRemoteBucketResponse(session *models.Principal, params bucketApi.DeleteRemoteBucketParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Madmin Client: %v", err) - return err + return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} err = deleteRemoteBucket(ctx, adminClient, params.SourceBucketName, params.Arn) if err != nil { - LogError("error deleting remote bucket: %v", err) - return err + return ErrorWithContext(ctx, fmt.Errorf("error deleting remote bucket: %v", err)) } - return err + return nil } -func getAddRemoteBucketResponse(session *models.Principal, params bucketApi.AddRemoteBucketParams) error { - ctx, cancel := context.WithCancel(context.Background()) +func getAddRemoteBucketResponse(session *models.Principal, params bucketApi.AddRemoteBucketParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Madmin Client: %v", err) - return err + return ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} _, err = addRemoteBucket(ctx, adminClient, *params.Body) if err != nil { - LogError("error adding remote bucket: %v", err) - return err + return ErrorWithContext(ctx, fmt.Errorf("error adding remote bucket: %v", err)) } - return err + return nil } func listRemoteBuckets(ctx context.Context, client MinioAdmin) ([]*models.RemoteBucket, error) { @@ -309,7 +301,7 @@ func addBucketReplicationItem(ctx context.Context, session *models.Principal, mi // we will tolerate this call failing cfg, err := minClient.getBucketReplication(ctx, bucketName) if err != nil { - LogError("error fetching replication configuration for bucket %s: %v", bucketName, err) + ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err)) } // add rule @@ -328,7 +320,7 @@ func addBucketReplicationItem(ctx context.Context, session *models.Principal, mi s3Client, err := newS3BucketClient(session, bucketName, prefix) if err != nil { - LogError("error creating S3Client: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error creating S3Client: %v", err)) return err } // create a mc S3Client interface implementation @@ -365,7 +357,7 @@ func addBucketReplicationItem(ctx context.Context, session *models.Principal, mi err2 := mcClient.setReplication(ctx, &cfg, opts) if err2 != nil { - LogError("error creating replication for bucket:", err2.Cause) + ErrorWithContext(ctx, fmt.Errorf("error creating replication for bucket: %v", err2.Cause)) return err2.Cause } return nil @@ -375,15 +367,14 @@ func editBucketReplicationItem(ctx context.Context, session *models.Principal, m // we will tolerate this call failing cfg, err := minClient.getBucketReplication(ctx, bucketName) if err != nil { - LogError("error fetching replication configuration for bucket %s: %v", bucketName, err) + ErrorWithContext(ctx, fmt.Errorf("error fetching replication configuration for bucket %s: %v", bucketName, err)) } maxPrio := int(priority) s3Client, err := newS3BucketClient(session, bucketName, prefix) if err != nil { - LogError("error creating S3Client: %v", err) - return err + return fmt.Errorf("error creating S3Client: %v", err) } // create a mc S3Client interface implementation // defining the client to be used @@ -432,8 +423,7 @@ func editBucketReplicationItem(ctx context.Context, session *models.Principal, m err2 := mcClient.setReplication(ctx, &cfg, opts) if err2 != nil { - LogError("error modifying replication for bucket:", err2.Cause) - return err2.Cause + return fmt.Errorf("error modifying replication for bucket: %v", err2.Cause) } return nil } @@ -516,20 +506,18 @@ func setMultiBucketReplication(ctx context.Context, session *models.Principal, c } func setMultiBucketReplicationResponse(session *models.Principal, params bucketApi.SetMultiBucketReplicationParams) (*models.MultiBucketResponseState, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Madmin Client:", err) - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client:", err) - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating MinIO Client: %v", err)) } // create a minioClient interface implementation // defining the client to be used @@ -538,8 +526,7 @@ func setMultiBucketReplicationResponse(session *models.Principal, params bucketA replicationResults := setMultiBucketReplication(ctx, session, adminClient, mnClient, params) if replicationResults == nil { - err = errors.New("error setting buckets replication") - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, errors.New("error setting buckets replication")) } resParsed := []*models.MultiBucketResponseItem{} @@ -562,18 +549,18 @@ func setMultiBucketReplicationResponse(session *models.Principal, params bucketA } func listExternalBucketsResponse(params bucketApi.ListExternalBucketsParams) (*models.ListBucketsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() remoteAdmin, err := newAdminFromCreds(*params.Body.AccessKey, *params.Body.SecretKey, *params.Body.TargetURL, *params.Body.UseTLS) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used remoteClient := AdminClient{Client: remoteAdmin} buckets, err := getAccountBuckets(ctx, remoteClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output @@ -610,8 +597,7 @@ func getARNsFromIDs(conf *replication.Config, rules []string) []string { func deleteReplicationRule(ctx context.Context, session *models.Principal, bucketName, ruleID string) error { mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return err + return fmt.Errorf("error creating MinIO Client: %v", err) } // create a minioClient interface implementation // defining the client to be used @@ -619,19 +605,17 @@ func deleteReplicationRule(ctx context.Context, session *models.Principal, bucke cfg, err := minClient.getBucketReplication(ctx, bucketName) if err != nil { - LogError("error versioning bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err)) } s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - LogError("error creating S3Client: %v", err) - return err + return fmt.Errorf("error creating S3Client: %v", err) } mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Admin Client: %v", err) - return err + return fmt.Errorf("error creating Admin Client: %v", err) } admClient := AdminClient{Client: mAdmin} @@ -659,8 +643,7 @@ func deleteReplicationRule(ctx context.Context, session *models.Principal, bucke func deleteAllReplicationRules(ctx context.Context, session *models.Principal, bucketName string) error { s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - LogError("error creating S3Client: %v", err) - return err + return fmt.Errorf("error creating S3Client: %v", err) } // create a mc S3Client interface implementation // defining the client to be used @@ -668,8 +651,7 @@ func deleteAllReplicationRules(ctx context.Context, session *models.Principal, b mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return err + return fmt.Errorf("error creating MinIO Client: %v", err) } // create a minioClient interface implementation // defining the client to be used @@ -677,13 +659,12 @@ func deleteAllReplicationRules(ctx context.Context, session *models.Principal, b cfg, err := minClient.getBucketReplication(ctx, bucketName) if err != nil { - LogError("error versioning bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err)) } mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Admin Client: %v", err) - return err + return fmt.Errorf("error creating Admin Client: %v", err) } admClient := AdminClient{Client: mAdmin} @@ -707,8 +688,7 @@ func deleteAllReplicationRules(ctx context.Context, session *models.Principal, b func deleteSelectedReplicationRules(ctx context.Context, session *models.Principal, bucketName string, rules []string) error { mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return err + return fmt.Errorf("error creating MinIO Client: %v", err) } // create a minioClient interface implementation // defining the client to be used @@ -716,13 +696,12 @@ func deleteSelectedReplicationRules(ctx context.Context, session *models.Princip cfg, err := minClient.getBucketReplication(ctx, bucketName) if err != nil { - LogError("error versioning bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err)) } s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - LogError("error creating S3Client: %v", err) - return err + return fmt.Errorf("error creating S3Client: %v", err) } // create a mc S3Client interface implementation // defining the client to be used @@ -730,8 +709,7 @@ func deleteSelectedReplicationRules(ctx context.Context, session *models.Princip mAdmin, err := NewMinioAdminClient(session) if err != nil { - LogError("error creating Admin Client: %v", err) - return err + return fmt.Errorf("error creating Admin Client: %v", err) } admClient := AdminClient{Client: mAdmin} @@ -756,49 +734,48 @@ func deleteSelectedReplicationRules(ctx context.Context, session *models.Princip } func deleteReplicationRuleResponse(session *models.Principal, params bucketApi.DeleteBucketReplicationRuleParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() err := deleteReplicationRule(ctx, session, params.BucketName, params.RuleID) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } func deleteBucketReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteAllReplicationRulesParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() err := deleteAllReplicationRules(ctx, session, params.BucketName) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } func deleteSelectedReplicationRulesResponse(session *models.Principal, params bucketApi.DeleteSelectedReplicationRulesParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() err := deleteSelectedReplicationRules(ctx, session, params.BucketName, params.Rules.Rules) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } func updateBucketReplicationResponse(session *models.Principal, params bucketApi.UpdateMultiBucketReplicationParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client:", err) - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -822,7 +799,7 @@ func updateBucketReplicationResponse(session *models.Principal, params bucketApi params.Body.StorageClass) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil diff --git a/restapi/admin_replication_status.go b/restapi/admin_replication_status.go index b9eacda43..b50f600e0 100644 --- a/restapi/admin_replication_status.go +++ b/restapi/admin_replication_status.go @@ -31,26 +31,24 @@ func registerSiteReplicationStatusHandler(api *operations.ConsoleAPI) { api.SiteReplicationGetSiteReplicationStatusHandler = siteRepApi.GetSiteReplicationStatusHandlerFunc(func(params siteRepApi.GetSiteReplicationStatusParams, session *models.Principal) middleware.Responder { rInfo, err := getSRStatusResponse(session, params) if err != nil { - return siteRepApi.NewGetSiteReplicationStatusDefault(500).WithPayload(prepareError(err)) + return siteRepApi.NewGetSiteReplicationStatusDefault(int(err.Code)).WithPayload(err) } return siteRepApi.NewGetSiteReplicationStatusOK().WithPayload(rInfo) }) } -func getSRStatusResponse(session *models.Principal, params siteRepApi.GetSiteReplicationStatusParams) (info *models.SiteReplicationStatusResponse, err error) { - +func getSRStatusResponse(session *models.Principal, params siteRepApi.GetSiteReplicationStatusParams) (*models.SiteReplicationStatusResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} - ctx := context.Background() - res, err := getSRStats(ctx, adminClient, params) - if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, err) } return res, nil } diff --git a/restapi/admin_service.go b/restapi/admin_service.go index 0e767e1b1..dd2b1cac9 100644 --- a/restapi/admin_service.go +++ b/restapi/admin_service.go @@ -30,7 +30,7 @@ import ( func registerServiceHandlers(api *operations.ConsoleAPI) { // Restart Service api.ServiceRestartServiceHandler = svcApi.RestartServiceHandlerFunc(func(params svcApi.RestartServiceParams, session *models.Principal) middleware.Responder { - if err := getRestartServiceResponse(session); err != nil { + if err := getRestartServiceResponse(session, params); err != nil { return svcApi.NewRestartServiceDefault(int(err.Code)).WithPayload(err) } return svcApi.NewRestartServiceNoContent() @@ -59,19 +59,19 @@ func serviceRestart(ctx context.Context, client MinioAdmin) error { } // getRestartServiceResponse performs serviceRestart() -func getRestartServiceResponse(session *models.Principal) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getRestartServiceResponse(session *models.Principal, params svcApi.RestartServiceParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO Admin Client interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} if err := serviceRestart(ctx, adminClient); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } diff --git a/restapi/admin_service_test.go b/restapi/admin_service_test.go index a61f83b6e..20d5af1f2 100644 --- a/restapi/admin_service_test.go +++ b/restapi/admin_service_test.go @@ -37,10 +37,9 @@ func (ac adminClientMock) serviceRestart(ctx context.Context) error { func TestServiceRestart(t *testing.T) { assert := assert.New(t) adminClient := adminClientMock{} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() function := "serviceRestart()" - // Test-1 : serviceRestart() restart services no error + // Test-1 : serviceRestart() restart services no errors // mock function response from listGroups() minioServiceRestartMock = func(ctx context.Context) error { return nil @@ -49,11 +48,11 @@ func TestServiceRestart(t *testing.T) { return madmin.InfoMessage{}, nil } if err := serviceRestart(ctx, adminClient); err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } - // Test-2 : serviceRestart() returns error on client.serviceRestart call - // and see that the error is handled correctly and returned + // Test-2 : serviceRestart() returns errors on client.serviceRestart call + // and see that the errors is handled correctly and returned minioServiceRestartMock = func(ctx context.Context) error { return errors.New("error") } @@ -64,8 +63,8 @@ func TestServiceRestart(t *testing.T) { assert.Equal("error", err.Error()) } - // Test-3 : serviceRestart() returns error on client.serverInfo() call - // and see that the error is handled correctly and returned + // Test-3 : serviceRestart() returns errors on client.serverInfo() call + // and see that the errors is handled correctly and returned minioServiceRestartMock = func(ctx context.Context) error { return nil } diff --git a/restapi/admin_site_replication.go b/restapi/admin_site_replication.go index 83d7c0f6f..de6c0ce60 100644 --- a/restapi/admin_site_replication.go +++ b/restapi/admin_site_replication.go @@ -29,105 +29,98 @@ import ( func registerSiteReplicationHandler(api *operations.ConsoleAPI) { api.SiteReplicationGetSiteReplicationInfoHandler = siteRepApi.GetSiteReplicationInfoHandlerFunc(func(params siteRepApi.GetSiteReplicationInfoParams, session *models.Principal) middleware.Responder { - rInfo, err := getSRInfoResponse(session) + rInfo, err := getSRInfoResponse(session, params) if err != nil { - return siteRepApi.NewGetSiteReplicationInfoDefault(500).WithPayload(prepareError(err)) + return siteRepApi.NewGetSiteReplicationInfoDefault(int(err.Code)).WithPayload(err) } return siteRepApi.NewGetSiteReplicationInfoOK().WithPayload(rInfo) }) api.SiteReplicationSiteReplicationInfoAddHandler = siteRepApi.SiteReplicationInfoAddHandlerFunc(func(params siteRepApi.SiteReplicationInfoAddParams, session *models.Principal) middleware.Responder { - eInfo, err := getSRAddResponse(session, ¶ms) + eInfo, err := getSRAddResponse(session, params) if err != nil { - return siteRepApi.NewSiteReplicationInfoAddDefault(500).WithPayload(err) + return siteRepApi.NewSiteReplicationInfoAddDefault(int(err.Code)).WithPayload(err) } return siteRepApi.NewSiteReplicationInfoAddOK().WithPayload(eInfo) }) api.SiteReplicationSiteReplicationRemoveHandler = siteRepApi.SiteReplicationRemoveHandlerFunc(func(params siteRepApi.SiteReplicationRemoveParams, session *models.Principal) middleware.Responder { - remRes, err := getSRRemoveResponse(session, ¶ms) + remRes, err := getSRRemoveResponse(session, params) if err != nil { - return siteRepApi.NewSiteReplicationRemoveDefault(500).WithPayload(err) + return siteRepApi.NewSiteReplicationRemoveDefault(int(err.Code)).WithPayload(err) } return siteRepApi.NewSiteReplicationRemoveNoContent().WithPayload(remRes) }) api.SiteReplicationSiteReplicationEditHandler = siteRepApi.SiteReplicationEditHandlerFunc(func(params siteRepApi.SiteReplicationEditParams, session *models.Principal) middleware.Responder { - - eInfo, err := getSREditResponse(session, ¶ms) + eInfo, err := getSREditResponse(session, params) if err != nil { - return siteRepApi.NewSiteReplicationRemoveDefault(500).WithPayload(err) + return siteRepApi.NewSiteReplicationRemoveDefault(int(err.Code)).WithPayload(err) } - return siteRepApi.NewSiteReplicationEditOK().WithPayload(eInfo) }) - } -func getSRInfoResponse(session *models.Principal) (info *models.SiteReplicationInfoResponse, err error) { +func getSRInfoResponse(session *models.Principal, params siteRepApi.GetSiteReplicationInfoParams) (*models.SiteReplicationInfoResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() res, err := getSRConfig(ctx, adminClient) if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, err) } return res, nil } -func getSRAddResponse(session *models.Principal, params *siteRepApi.SiteReplicationInfoAddParams) (*models.SiteReplicationAddResponse, *models.Error) { - +func getSRAddResponse(session *models.Principal, params siteRepApi.SiteReplicationInfoAddParams) (*models.SiteReplicationAddResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - res, err := addSiteReplication(ctx, adminClient, params) + res, err := addSiteReplication(ctx, adminClient, ¶ms) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return res, nil } -func getSREditResponse(session *models.Principal, params *siteRepApi.SiteReplicationEditParams) (*models.PeerSiteEditResponse, *models.Error) { +func getSREditResponse(session *models.Principal, params siteRepApi.SiteReplicationEditParams) (*models.PeerSiteEditResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - eRes, err := editSiteReplication(ctx, adminClient, params) - + eRes, err := editSiteReplication(ctx, adminClient, ¶ms) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } - return eRes, nil } -func getSRRemoveResponse(session *models.Principal, params *siteRepApi.SiteReplicationRemoveParams) (*models.PeerSiteRemoveResponse, *models.Error) { +func getSRRemoveResponse(session *models.Principal, params siteRepApi.SiteReplicationRemoveParams) (*models.PeerSiteRemoveResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - rRes, err := removeSiteReplication(ctx, adminClient, params) + rRes, err := removeSiteReplication(ctx, adminClient, ¶ms) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return rRes, nil } diff --git a/restapi/admin_subnet.go b/restapi/admin_subnet.go index c46e3dfee..5d0f17881 100644 --- a/restapi/admin_subnet.go +++ b/restapi/admin_subnet.go @@ -24,7 +24,7 @@ import ( "net/http" "net/url" - "github.com/minio/console/pkg/utils" + xhttp "github.com/minio/console/pkg/http" "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" @@ -61,7 +61,7 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) { }) // Get subnet info api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder { - resp, err := GetSubnetInfoResponse(session) + resp, err := GetSubnetInfoResponse(session, params) if err != nil { return subnetApi.NewSubnetInfoDefault(int(err.Code)).WithPayload(err) } @@ -69,7 +69,7 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) { }) // Get subnet registration token api.SubnetSubnetRegTokenHandler = subnetApi.SubnetRegTokenHandlerFunc(func(params subnetApi.SubnetRegTokenParams, session *models.Principal) middleware.Responder { - resp, err := GetSubnetRegTokenResponse(session) + resp, err := GetSubnetRegTokenResponse(session, params) if err != nil { return subnetApi.NewSubnetRegTokenDefault(int(err.Code)).WithPayload(err) } @@ -100,7 +100,7 @@ func SubnetRegisterWithAPIKey(ctx context.Context, minioClient MinioAdmin, apiKe return true, nil } -func SubnetLogin(client utils.HTTPClientI, username, password string) (string, string, error) { +func SubnetLogin(client xhttp.ClientI, username, password string) (string, string, error) { tokens, err := subnet.Login(client, username, password) if err != nil { return "", "", err @@ -117,22 +117,22 @@ func SubnetLogin(client utils.HTTPClientI, username, password string) (string, s } func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLoginParams) (*models.SubnetLoginResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } minioClient := AdminClient{Client: mAdmin} subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } apiKey := params.Body.APIKey if apiKey != "" { registered, err := SubnetRegisterWithAPIKey(ctx, minioClient, apiKey) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.SubnetLoginResponse{ Registered: registered, @@ -144,7 +144,7 @@ func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLo if username != "" && password != "" { token, mfa, err := SubnetLogin(subnetHTTPClient, username, password) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.SubnetLoginResponse{ MfaToken: mfa, @@ -152,7 +152,7 @@ func GetSubnetLoginResponse(session *models.Principal, params subnetApi.SubnetLo Organizations: []*models.SubnetOrganization{}, }, nil } - return nil, prepareError(ErrorGeneric) + return nil, ErrorWithContext(ctx, ErrDefault) } type SubnetRegistration struct { @@ -161,7 +161,7 @@ type SubnetRegistration struct { Organizations []models.SubnetOrganization } -func SubnetLoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*models.SubnetLoginResponse, error) { +func SubnetLoginWithMFA(client xhttp.ClientI, username, mfaToken, otp string) (*models.SubnetLoginResponse, error) { tokens, err := subnet.LoginWithMFA(client, username, mfaToken, otp) if err != nil { return nil, err @@ -180,7 +180,7 @@ func SubnetLoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string } // GetSubnetHTTPClient will return a client with proxy if configured, otherwise will return the default console http client -func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*utils.HTTPClient, error) { +func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*xhttp.Client, error) { var subnetHTTPClient *http.Client var proxy string envProxy := getSubnetProxy() @@ -194,7 +194,7 @@ func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*utils.HT proxy = envProxy } if proxy != "" { - transport := prepareSTSClientTransport(false) + transport := PrepareSTSClientTransport(false) subnetHTTPClient = &http.Client{ Transport: transport, } @@ -206,27 +206,27 @@ func GetSubnetHTTPClient(ctx context.Context, minioClient MinioAdmin) (*utils.HT } else { subnetHTTPClient = GetConsoleHTTPClient() } - clientI := &utils.HTTPClient{ + clientI := &xhttp.Client{ Client: subnetHTTPClient, } return clientI, nil } func GetSubnetLoginWithMFAResponse(session *models.Principal, params subnetApi.SubnetLoginMFAParams) (*models.SubnetLoginResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } minioClient := AdminClient{Client: mAdmin} subnetHTTPClient, err := GetSubnetHTTPClient(ctx, minioClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } resp, err := SubnetLoginWithMFA(subnetHTTPClient, *params.Body.Username, *params.Body.MfaToken, *params.Body.Otp) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return resp, nil } @@ -257,7 +257,7 @@ func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (* return &res, nil } -func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient utils.HTTPClientI, params subnetApi.SubnetRegisterParams) error { +func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient xhttp.ClientI, params subnetApi.SubnetRegisterParams) error { serverInfo, err := minioClient.serverInfo(ctx) if err != nil { return err @@ -280,45 +280,45 @@ func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient u } func GetSubnetRegisterResponse(session *models.Principal, params subnetApi.SubnetRegisterParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} subnetHTTPClient, err := GetSubnetHTTPClient(ctx, adminClient) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } err = GetSubnetRegister(ctx, adminClient, subnetHTTPClient, params) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } -func GetSubnetInfoResponse(session *models.Principal) (*models.License, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} subnetTokens, err := GetSubnetKeyFromMinIOConfig(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } if subnetTokens.APIKey == "" { - return nil, prepareError(errLicenseNotFound) + return nil, ErrorWithContext(ctx, ErrLicenseNotFound) } - client := &utils.HTTPClient{ + client := &xhttp.Client{ Client: GetConsoleHTTPClient(), } licenseInfo, err := subnet.ParseLicense(client, subnetTokens.License) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } license := &models.License{ Email: licenseInfo.Email, @@ -345,17 +345,17 @@ func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, err return regToken, nil } -func GetSubnetRegTokenResponse(session *models.Principal) (*models.SubnetRegTokenResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func GetSubnetRegTokenResponse(session *models.Principal, params subnetApi.SubnetRegTokenParams) (*models.SubnetRegTokenResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } adminClient := AdminClient{Client: mAdmin} token, err := GetSubnetRegToken(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.SubnetRegTokenResponse{ RegToken: token, diff --git a/restapi/admin_tiers.go b/restapi/admin_tiers.go index 697004c83..dc51214c6 100644 --- a/restapi/admin_tiers.go +++ b/restapi/admin_tiers.go @@ -32,7 +32,7 @@ import ( func registerAdminTiersHandlers(api *operations.ConsoleAPI) { // return a list of notification endpoints api.TieringTiersListHandler = tieringApi.TiersListHandlerFunc(func(params tieringApi.TiersListParams, session *models.Principal) middleware.Responder { - tierList, err := getTiersResponse(session) + tierList, err := getTiersResponse(session, params) if err != nil { return tieringApi.NewTiersListDefault(int(err.Code)).WithPayload(err) } @@ -40,7 +40,7 @@ func registerAdminTiersHandlers(api *operations.ConsoleAPI) { }) // add a new tiers api.TieringAddTierHandler = tieringApi.AddTierHandlerFunc(func(params tieringApi.AddTierParams, session *models.Principal) middleware.Responder { - err := getAddTierResponse(session, ¶ms) + err := getAddTierResponse(session, params) if err != nil { return tieringApi.NewAddTierDefault(int(err.Code)).WithPayload(err) } @@ -48,7 +48,7 @@ func registerAdminTiersHandlers(api *operations.ConsoleAPI) { }) // get a tier api.TieringGetTierHandler = tieringApi.GetTierHandlerFunc(func(params tieringApi.GetTierParams, session *models.Principal) middleware.Responder { - notifEndpoints, err := getGetTierResponse(session, ¶ms) + notifEndpoints, err := getGetTierResponse(session, params) if err != nil { return tieringApi.NewGetTierDefault(int(err.Code)).WithPayload(err) } @@ -56,7 +56,7 @@ func registerAdminTiersHandlers(api *operations.ConsoleAPI) { }) // edit credentials for a tier api.TieringEditTierCredentialsHandler = tieringApi.EditTierCredentialsHandlerFunc(func(params tieringApi.EditTierCredentialsParams, session *models.Principal) middleware.Responder { - err := getEditTierCredentialsResponse(session, ¶ms) + err := getEditTierCredentialsResponse(session, params) if err != nil { return tieringApi.NewEditTierCredentialsDefault(int(err.Code)).WithPayload(err) } @@ -141,21 +141,20 @@ func getTiers(ctx context.Context, client MinioAdmin) (*models.TierListResponse, } // getTiersResponse returns a response with a list of tiers -func getTiersResponse(session *models.Principal) (*models.TierListResponse, *models.Error) { +func getTiersResponse(session *models.Principal, params tieringApi.TiersListParams) (*models.TierListResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output tiersResp, err := getTiers(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return tiersResp, nil } @@ -231,20 +230,21 @@ func addTier(ctx context.Context, client MinioAdmin, params *tieringApi.AddTierP } // getAddTierResponse returns the response of admin tier -func getAddTierResponse(session *models.Principal, params *tieringApi.AddTierParams) *models.Error { +func getAddTierResponse(session *models.Principal, params tieringApi.AddTierParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + // serialize output - errTier := addTier(ctx, adminClient, params) + errTier := addTier(ctx, adminClient, ¶ms) if errTier != nil { - return prepareError(errTier) + return ErrorWithContext(ctx, err) } return nil } @@ -309,25 +309,24 @@ func getTier(ctx context.Context, client MinioAdmin, params *tieringApi.GetTierP } // build response - return nil, ErrorGenericNotFound + return nil, ErrNotFound } // getGetTierResponse returns a tier -func getGetTierResponse(session *models.Principal, params *tieringApi.GetTierParams) (*models.Tier, *models.Error) { +func getGetTierResponse(session *models.Principal, params tieringApi.GetTierParams) (*models.Tier, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output - addTierResp, err := getTier(ctx, adminClient, params) + addTierResp, err := getTier(ctx, adminClient, ¶ms) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return addTierResp, nil } @@ -349,21 +348,20 @@ func editTierCredentials(ctx context.Context, client MinioAdmin, params *tiering } // getEditTierCredentialsResponse returns the result of editing credentials for a tier -func getEditTierCredentialsResponse(session *models.Principal, params *tieringApi.EditTierCredentialsParams) *models.Error { +func getEditTierCredentialsResponse(session *models.Principal, params tieringApi.EditTierCredentialsParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // serialize output - err = editTierCredentials(ctx, adminClient, params) + err = editTierCredentials(ctx, adminClient, ¶ms) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } diff --git a/restapi/admin_users.go b/restapi/admin_users.go index 4b61be325..39179fc93 100644 --- a/restapi/admin_users.go +++ b/restapi/admin_users.go @@ -45,7 +45,7 @@ const ( func registerUsersHandlers(api *operations.ConsoleAPI) { // List Users api.UserListUsersHandler = userApi.ListUsersHandlerFunc(func(params userApi.ListUsersParams, session *models.Principal) middleware.Responder { - listUsersResponse, err := getListUsersResponse(session) + listUsersResponse, err := getListUsersResponse(session, params) if err != nil { return userApi.NewListUsersDefault(int(err.Code)).WithPayload(err) } @@ -104,7 +104,7 @@ func registerUsersHandlers(api *operations.ConsoleAPI) { return userApi.NewBulkUpdateUsersGroupsOK() }) api.BucketListUsersWithAccessToBucketHandler = bucketApi.ListUsersWithAccessToBucketHandlerFunc(func(params bucketApi.ListUsersWithAccessToBucketParams, session *models.Principal) middleware.Responder { - response, err := getListUsersWithAccessToBucketResponse(session, params.Bucket) + response, err := getListUsersWithAccessToBucketResponse(session, params) if err != nil { return bucketApi.NewListUsersWithAccessToBucketDefault(int(err.Code)).WithPayload(err) } @@ -145,20 +145,19 @@ func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) { } // getListUsersResponse performs listUsers() and serializes it to the handler's output -func getListUsersResponse(session *models.Principal) (*models.ListUsersResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListUsersResponse(session *models.Principal, params userApi.ListUsersParams) (*models.ListUsersResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - users, err := listUsers(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output listUsersResponse := &models.ListUsersResponse{ @@ -169,7 +168,7 @@ func getListUsersResponse(session *models.Principal) (*models.ListUsersResponse, // addUser invokes adding a users on `MinioAdmin` and builds the response `models.User` func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *string, groups []string, policies []string) (*models.User, error) { - // Calls into MinIO to add a new user if there's an error return it + // Calls into MinIO to add a new user if there's an errors return it if err := client.addUser(ctx, *accessKey, *secretKey); err != nil { return nil, err } @@ -208,11 +207,11 @@ func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *strin } func getUserAddResponse(session *models.Principal, params userApi.AddUserParams) (*models.User, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -223,7 +222,7 @@ func getUserAddResponse(session *models.Principal, params userApi.AddUserParams) userExists = err == nil if userExists { - return nil, prepareError(errNonUniqueAccessKey) + return nil, ErrorWithContext(ctx, ErrNonUniqueAccessKey) } user, err := addUser( ctx, @@ -234,7 +233,7 @@ func getUserAddResponse(session *models.Principal, params userApi.AddUserParams) params.Body.Policies, ) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return user, nil } @@ -245,26 +244,21 @@ func removeUser(ctx context.Context, client MinioAdmin, accessKey string) error } func getRemoveUserResponse(session *models.Principal, params userApi.RemoveUserParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } - if session.AccountAccessKey == params.Name { - return prepareError(errAvoidSelfAccountDelete) + return ErrorWithContext(ctx, ErrAvoidSelfAccountDelete) } - // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - if err := removeUser(ctx, adminClient, params.Name); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } - return nil } @@ -279,12 +273,12 @@ func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*mad } func getUserInfoResponse(session *models.Principal, params userApi.GetUserInfoParams) (*models.User, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -299,7 +293,7 @@ func getUserInfoResponse(session *models.Principal, params userApi.GetUserInfoPa errorMessage := "User doesn't exist" return nil, &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err.Error())} } - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } var policies []string @@ -425,12 +419,12 @@ func updateUserGroups(ctx context.Context, client MinioAdmin, user string, group } func getUpdateUserGroupsResponse(session *models.Principal, params userApi.UpdateUserGroupsParams) (*models.User, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -440,7 +434,7 @@ func getUpdateUserGroupsResponse(session *models.Principal, params userApi.Updat user, err := updateUserGroups(ctx, adminClient, params.Name, params.Body.Groups) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return user, nil @@ -462,12 +456,12 @@ func setUserStatus(ctx context.Context, client MinioAdmin, user string, status s } func getUpdateUserResponse(session *models.Principal, params userApi.UpdateUserInfoParams) (*models.User, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -479,13 +473,13 @@ func getUpdateUserResponse(session *models.Principal, params userApi.UpdateUserI groups := params.Body.Groups if err := setUserStatus(ctx, adminClient, name, status); err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } userElem, errUG := updateUserGroups(ctx, adminClient, name, groups) if errUG != nil { - return nil, prepareError(errUG) + return nil, ErrorWithContext(ctx, errUG) } return userElem, nil } @@ -517,14 +511,14 @@ func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate errorsList := []string{} // We get the errors list because we want to have all errors at once. for _, err := range groupsUpdateList { - errorFromUpdate := <-err // We store the error to avoid Data Race + errorFromUpdate := <-err // We store the errors to avoid Data Race if errorFromUpdate != nil { - // If there is an error, we store the errors strings so we can join them after we receive all errors + // If there is an errors, we store the errors strings so we can join them after we receive all errors errorsList = append(errorsList, errorFromUpdate.Error()) // We wait until all the channels have been closed. } } - // If there are errors, we throw the final error with the errors inside + // If there are errors, we throw the final errors with the errors inside if len(errorsList) > 0 { errGen := fmt.Errorf("error in users-groups assignation: %q", strings.Join(errorsList[:], ",")) return errGen @@ -534,12 +528,12 @@ func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate } func getAddUsersListToGroupsResponse(session *models.Principal, params userApi.BulkUpdateUsersGroupsParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -550,24 +544,27 @@ func getAddUsersListToGroupsResponse(session *models.Principal, params userApi.B groupsList := params.Body.Groups if err := addUsersListToGroups(ctx, adminClient, usersList, groupsList); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } -func getListUsersWithAccessToBucketResponse(session *models.Principal, bucket string) ([]string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListUsersWithAccessToBucketResponse(session *models.Principal, params bucketApi.ListUsersWithAccessToBucketParams) ([]string, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - return listUsersWithAccessToBucket(ctx, adminClient, bucket) + list, err := listUsersWithAccessToBucket(ctx, adminClient, params.Bucket) + if err != nil { + return nil, ErrorWithContext(ctx, err) + } + return list, nil } func policyAllowsAndMatchesBucket(policy *iampolicy.Policy, bucket string) int { @@ -587,10 +584,10 @@ func policyAllowsAndMatchesBucket(policy *iampolicy.Policy, bucket string) int { return Unknown } -func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bucket string) ([]string, *models.Error) { +func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bucket string) ([]string, error) { users, err := adminClient.listUsers(ctx) if err != nil { - return nil, prepareError(err) + return nil, err } var retval []string akHasAccess := make(map[string]struct{}) @@ -603,7 +600,7 @@ func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bu } policy, err := adminClient.getPolicy(ctx, policyName) if err != nil { - LogError("unable to fetch policy %s: %v", policyName, err) + ErrorWithContext(ctx, fmt.Errorf("unable to fetch policy %s: %v", policyName, err)) continue } if _, ok := akIsDenied[k]; !ok { @@ -622,19 +619,19 @@ func listUsersWithAccessToBucket(ctx context.Context, adminClient MinioAdmin, bu groups, err := adminClient.listGroups(ctx) if err != nil { - LogError("unable to list groups: %v", err) + ErrorWithContext(ctx, fmt.Errorf("unable to list groups: %v", err)) return retval, nil } for _, groupName := range groups { info, err := groupInfo(ctx, adminClient, groupName) if err != nil { - LogError("unable to fetch group info %s: %v", groupName, err) + ErrorWithContext(ctx, fmt.Errorf("unable to fetch group info %s: %v", groupName, err)) continue } policy, err := adminClient.getPolicy(ctx, info.Policy) if err != nil { - LogError("unable to fetch group policy %s: %v", info.Policy, err) + ErrorWithContext(ctx, fmt.Errorf("unable to fetch group policy %s: %v", info.Policy, err)) continue } for _, member := range info.Members { @@ -665,11 +662,11 @@ func changeUserPassword(ctx context.Context, client MinioAdmin, selectedUser str // getChangeUserPasswordResponse will change the password of selctedUser to newSecretKey func getChangeUserPasswordResponse(session *models.Principal, params accountApi.ChangeUserPasswordParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -681,7 +678,7 @@ func getChangeUserPasswordResponse(session *models.Principal, params accountApi. // changes password of user to newSecretKey if err := changeUserPassword(ctx, adminClient, user, newSecretKey); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } diff --git a/restapi/admin_users_test.go b/restapi/admin_users_test.go index 019ec9140..f9896f973 100644 --- a/restapi/admin_users_test.go +++ b/restapi/admin_users_test.go @@ -525,7 +525,7 @@ func TestListUsersWithAccessToBucket(t *testing.T) { } return mockResponse, nil } - return nil, ErrorGeneric + return nil, ErrDefault } type args struct { bucket string diff --git a/restapi/client.go b/restapi/client.go index bdb2f7f64..5078b2ae2 100644 --- a/restapi/client.go +++ b/restapi/client.go @@ -454,7 +454,7 @@ func newS3Config(endpoint, accessKey, secretKey, sessionToken string, insecure b s3Config.SecretKey = secretKey s3Config.SessionToken = sessionToken s3Config.Signature = "S3v4" - s3Config.Transport = prepareSTSClientTransport(insecure) + s3Config.Transport = PrepareSTSClientTransport(insecure) return s3Config } diff --git a/restapi/client_test.go b/restapi/client_test.go index 5f61cb80e..52780e669 100644 --- a/restapi/client_test.go +++ b/restapi/client_test.go @@ -79,7 +79,7 @@ func Test_computeObjectURLWithoutEncode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := computeObjectURLWithoutEncode(tt.args.bucketName, tt.args.prefix) if (err != nil) != tt.wantErr { - t.Errorf("computeObjectURLWithoutEncode() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("computeObjectURLWithoutEncode() errors = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { diff --git a/restapi/configure_console.go b/restapi/configure_console.go index b91fefc2a..340301d34 100644 --- a/restapi/configure_console.go +++ b/restapi/configure_console.go @@ -20,6 +20,7 @@ package restapi import ( "bytes" + "context" "crypto/tls" "fmt" "io" @@ -34,6 +35,9 @@ import ( "sync" "time" + "github.com/minio/console/pkg/logger" + "github.com/minio/console/pkg/utils" + "github.com/klauspost/compress/gzhttp" portal_ui "github.com/minio/console/portal-ui" @@ -75,7 +79,7 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler { api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) { // we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt // was generated and signed by us in the first place - claims, err := auth.SessionTokenAuthenticate(token) + claims, err := auth.ParseClaimsFromToken(token) if err != nil { api.Logger("Unable to validate the session token %s: %v", token, err) return nil, errors.New(401, "incorrect api key auth") @@ -167,13 +171,43 @@ func setupMiddlewares(handler http.Handler) http.Handler { return handler } +func ContextMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID, err := utils.NewUUID() + if err != nil && err != auth.ErrNoAuthToken { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ctx := context.WithValue(r.Context(), utils.ContextRequestID, requestID) + ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, r.UserAgent()) + ctx = context.WithValue(ctx, utils.ContextRequestHost, r.Host) + ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, r.RemoteAddr) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func AuditLogMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rw := logger.NewResponseWriter(w) + next.ServeHTTP(rw, r) + if strings.HasPrefix(r.URL.Path, "/ws") || strings.HasPrefix(r.URL.Path, "/api") { + logger.AuditLog(r.Context(), rw, r, map[string]interface{}{}, "Authorization", "Cookie", "Set-Cookie") + } + }) +} + // The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document. -// So this is a good place to plug in a panic handling middleware, logging and metrics +// So this is a good place to plug in a panic handling middleware, logger and metrics func setupGlobalMiddleware(handler http.Handler) http.Handler { - // handle cookie or authorization header for session - next := AuthenticationMiddleware(handler) + gnext := gzhttp.GzipHandler(handler) + // if audit-log is enabled console will log all incoming request + next := AuditLogMiddleware(gnext) // serve static files next = FileServerMiddleware(next) + // add information to request context + next = ContextMiddleware(next) + // handle cookie or authorization header for session + next = AuthenticationMiddleware(next) sslHostFn := secure.SSLHostFunc(func(host string) string { h, _, err := net.SplitHostPort(host) @@ -210,8 +244,7 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler { } secureMiddleware := secure.New(secureOptions) next = secureMiddleware.Handler(next) - gnext := gzhttp.GzipHandler(next) - return RejectS3Middleware(gnext) + return RejectS3Middleware(next) } // RejectS3Middleware will reject requests that have AWS S3 specific headers. @@ -242,14 +275,21 @@ func AuthenticationMiddleware(next http.Handler) http.Handler { http.Error(w, err.Error(), http.StatusUnauthorized) return } + sessionToken, _ := auth.DecryptToken(token) // All handlers handle appropriately to return errors // based on their swagger rules, we do not need to // additionally return error here, let the next ServeHTTPs // handle it appropriately. - if token != "" { - r.Header.Add("Authorization", "Bearer "+token) + if len(sessionToken) > 0 { + r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken))) } - next.ServeHTTP(w, r) + ctx := r.Context() + claims, _ := auth.ParseClaimsFromToken(string(sessionToken)) + if claims != nil { + // save user session id context + ctx = context.WithValue(r.Context(), utils.ContextRequestUserID, claims.STSSessionToken) + } + next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -362,7 +402,7 @@ func (lw nullWriter) Write(b []byte) (int, error) { // This function can be called multiple times, depending on the number of serving schemes. // scheme value will be set accordingly: "http", "https" or "unix" func configureServer(s *http.Server, _, _ string) { - // Turn-off random logging by Go net/http + // Turn-off random logger by Go net/http s.ErrorLog = log.New(&nullWriter{}, "", 0) } diff --git a/restapi/error.go b/restapi/error.go deleted file mode 100644 index ee11422e2..000000000 --- a/restapi/error.go +++ /dev/null @@ -1,232 +0,0 @@ -// This file is part of MinIO Console Server -// Copyright (c) 2021 MinIO, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package restapi - -import ( - "errors" - "runtime" - "strings" - - "github.com/minio/minio-go/v7" - - "github.com/go-openapi/swag" - "github.com/minio/console/models" - "github.com/minio/madmin-go" -) - -var ( - // ErrorGeneric is a generic error message - ErrorGeneric = errors.New("an error occurred, please try again") - errInvalidCredentials = errors.New("invalid Login") - errForbidden = errors.New("403 Forbidden") - errFileTooLarge = errors.New("413 File too Large") - errorGenericInvalidSession = errors.New("invalid session") - // ErrorGenericNotFound Generic error for not found - ErrorGenericNotFound = errors.New("not found") - // Explicit error messages - errorInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value") - errBucketBodyNotInRequest = errors.New("error bucket body not in request") - errBucketNameNotInRequest = errors.New("error bucket name not in request") - errGroupBodyNotInRequest = errors.New("error group body not in request") - errGroupNameNotInRequest = errors.New("error group name not in request") - errGroupAlreadyExists = errors.New("error group name already in use") - errPolicyNameNotInRequest = errors.New("error policy name not in request") - errPolicyBodyNotInRequest = errors.New("error policy body not in request") - errInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm") - errSSENotConfigured = errors.New("error server side encryption configuration not found") - errBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found") - errChangePassword = errors.New("error please check your current password") - errInvalidLicense = errors.New("invalid license key") - errLicenseNotFound = errors.New("license not found") - errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself") - errAccessDenied = errors.New("access denied") - errOauth2Provider = errors.New("unable to contact configured identity provider") - errNonUniqueAccessKey = errors.New("access key already in use") -) - -// Tiering errors -var ( - errRemoteTierExists = errors.New("Specified remote tier already exists") - errRemoteTierNotFound = errors.New("Specified remote tier was not found") - errRemoteTierUppercase = errors.New("Tier name must be in uppercase") - errRemoteTierBucketNotFound = errors.New("Remote tier bucket not found") - errRemoteInvalidCredentials = errors.New("Invalid remote tier credentials") -) - -// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message -func prepareError(err ...error) *models.Error { - errorCode := int32(500) - errorMessage := ErrorGeneric.Error() - if len(err) > 0 { - frame := getFrame(2) - fileParts := strings.Split(frame.File, "/") - LogError("original error -> (%s:%d: %v)", fileParts[len(fileParts)-1], frame.Line, err[0]) - if err[0].Error() == errForbidden.Error() { - errorCode = 403 - } - if err[0] == ErrorGenericNotFound { - errorCode = 404 - errorMessage = ErrorGenericNotFound.Error() - } - if errors.Is(err[0], errInvalidCredentials) { - errorCode = 401 - errorMessage = errInvalidCredentials.Error() - } - // console invalid erasure coding value - if errors.Is(err[0], errorInvalidErasureCodingValue) { - errorCode = 400 - errorMessage = errorInvalidErasureCodingValue.Error() - } - if errors.Is(err[0], errBucketBodyNotInRequest) { - errorCode = 400 - errorMessage = errBucketBodyNotInRequest.Error() - } - if errors.Is(err[0], errBucketNameNotInRequest) { - errorCode = 400 - errorMessage = errBucketNameNotInRequest.Error() - } - if errors.Is(err[0], errGroupBodyNotInRequest) { - errorCode = 400 - errorMessage = errGroupBodyNotInRequest.Error() - } - if errors.Is(err[0], errGroupNameNotInRequest) { - errorCode = 400 - errorMessage = errGroupNameNotInRequest.Error() - } - if errors.Is(err[0], errGroupAlreadyExists) { - errorCode = 400 - errorMessage = errGroupAlreadyExists.Error() - } - if errors.Is(err[0], errPolicyNameNotInRequest) { - errorCode = 400 - errorMessage = errPolicyNameNotInRequest.Error() - } - if errors.Is(err[0], errPolicyBodyNotInRequest) { - errorCode = 400 - errorMessage = errPolicyBodyNotInRequest.Error() - } - // console invalid session error - if errors.Is(err[0], errorGenericInvalidSession) { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // Bucket life cycle not configured - if errors.Is(err[0], errBucketLifeCycleNotConfigured) { - errorCode = 404 - errorMessage = errBucketLifeCycleNotConfigured.Error() - } - // Encryption not configured - if errors.Is(err[0], errSSENotConfigured) { - errorCode = 404 - errorMessage = errSSENotConfigured.Error() - } - // account change password - if madmin.ToErrorResponse(err[0]).Code == "SignatureDoesNotMatch" { - errorCode = 403 - errorMessage = errChangePassword.Error() - } - if errors.Is(err[0], errLicenseNotFound) { - errorCode = 404 - errorMessage = errLicenseNotFound.Error() - } - if errors.Is(err[0], errInvalidLicense) { - errorCode = 404 - errorMessage = errInvalidLicense.Error() - } - if errors.Is(err[0], errAvoidSelfAccountDelete) { - errorCode = 403 - errorMessage = errAvoidSelfAccountDelete.Error() - } - if madmin.ToErrorResponse(err[0]).Code == "AccessDenied" { - errorCode = 403 - errorMessage = errAccessDenied.Error() - } - if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // console invalid session error - if madmin.ToErrorResponse(err[0]).Code == "XMinioAdminNoSuchUser" { - errorCode = 401 - errorMessage = errorGenericInvalidSession.Error() - } - // if we received a second error take that as friendly message but don't override the code - if len(err) > 1 && err[1] != nil { - LogError("friendly error: %v", err[1].Error()) - errorMessage = err[1].Error() - } - // if we receive third error we just print that as debugging - if len(err) > 2 && err[2] != nil { - LogError("debugging error: %v", err[2].Error()) - } - // tiering errors - if err[0].Error() == errRemoteTierExists.Error() { - errorCode = 400 - errorMessage = err[0].Error() - } - if err[0].Error() == errRemoteTierNotFound.Error() { - errorCode = 400 - errorMessage = err[0].Error() - } - - if err[0].Error() == errRemoteTierUppercase.Error() { - errorCode = 400 - errorMessage = err[0].Error() - } - if err[0].Error() == errRemoteTierBucketNotFound.Error() { - errorCode = 400 - errorMessage = err[0].Error() - } - if err[0].Error() == errRemoteInvalidCredentials.Error() { - errorCode = 403 - errorMessage = err[0].Error() - } - if err[0].Error() == errFileTooLarge.Error() { - errorCode = 413 - errorMessage = err[0].Error() - } - // bucket already exists - if minio.ToErrorResponse(err[0]).Code == "BucketAlreadyOwnedByYou" { - errorCode = 400 - errorMessage = "Bucket already exists" - } - } - return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err[0].Error())} -} - -func getFrame(skipFrames int) runtime.Frame { - // We need the frame at index skipFrames+2, since we never want runtime.Callers and getFrame - targetFrameIndex := skipFrames + 2 - - // Set size to targetFrameIndex+2 to ensure we have room for one more caller than we need - programCounters := make([]uintptr, targetFrameIndex+2) - n := runtime.Callers(0, programCounters) - - frame := runtime.Frame{Function: "unknown"} - if n > 0 { - frames := runtime.CallersFrames(programCounters[:n]) - for more, frameIndex := true, 0; more && frameIndex <= targetFrameIndex; frameIndex++ { - var frameCandidate runtime.Frame - frameCandidate, more = frames.Next() - if frameIndex == targetFrameIndex { - frame = frameCandidate - } - } - } - - return frame -} diff --git a/restapi/errors.go b/restapi/errors.go new file mode 100644 index 000000000..707a2e4c7 --- /dev/null +++ b/restapi/errors.go @@ -0,0 +1,231 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package restapi + +import ( + "context" + "errors" + + "github.com/go-openapi/swag" + "github.com/minio/console/models" + "github.com/minio/madmin-go" + "github.com/minio/minio-go/v7" +) + +var ( + ErrDefault = errors.New("an errors occurred, please try again") + ErrInvalidLogin = errors.New("invalid Login") + ErrForbidden = errors.New("403 Forbidden") + ErrFileTooLarge = errors.New("413 File too Large") + ErrInvalidSession = errors.New("invalid session") + ErrNotFound = errors.New("not found") + ErrGroupAlreadyExists = errors.New("error group name already in use") + ErrInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value") + ErrBucketBodyNotInRequest = errors.New("error bucket body not in request") + ErrBucketNameNotInRequest = errors.New("error bucket name not in request") + ErrGroupBodyNotInRequest = errors.New("error group body not in request") + ErrGroupNameNotInRequest = errors.New("error group name not in request") + ErrPolicyNameNotInRequest = errors.New("error policy name not in request") + ErrPolicyBodyNotInRequest = errors.New("error policy body not in request") + ErrInvalidEncryptionAlgorithm = errors.New("error invalid encryption algorithm") + ErrSSENotConfigured = errors.New("error server side encryption configuration not found") + ErrBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found") + ErrChangePassword = errors.New("error please check your current password") + ErrInvalidLicense = errors.New("invalid license key") + ErrLicenseNotFound = errors.New("license not found") + ErrAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself") + ErrAccessDenied = errors.New("access denied") + ErrOauth2Provider = errors.New("unable to contact configured identity provider") + ErrNonUniqueAccessKey = errors.New("access key already in use") + ErrRemoteTierExists = errors.New("specified remote tier already exists") + ErrRemoteTierNotFound = errors.New("specified remote tier was not found") + ErrRemoteTierUppercase = errors.New("tier name must be in uppercase") + ErrRemoteTierBucketNotFound = errors.New("remote tier bucket not found") + ErrRemoteInvalidCredentials = errors.New("invalid remote tier credentials") + ErrUnableToGetTenantUsage = errors.New("unable to get tenant usage") + ErrTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster") + ErrTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant") + ErrTooFewAvailableNodes = errors.New("there is not enough available nodes to satisfy this requirement") + ErrFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant") + ErrUnableToGetTenantLogs = errors.New("unable to get tenant logs") + ErrUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates") + ErrUpdatingEncryptionConfig = errors.New("unable to update encryption configuration") + ErrDeletingEncryptionConfig = errors.New("error disabling tenant encryption") + ErrEncryptionConfigNotFound = errors.New("encryption configuration not found") + ErrPolicyNotFound = errors.New("policy does not exist") +) + +// ErrorWithContext : +func ErrorWithContext(ctx context.Context, err ...interface{}) *models.Error { + errorCode := int32(500) + errorMessage := ErrDefault.Error() + var err1 error + var exists bool + if len(err) > 0 { + if err1, exists = err[0].(error); exists { + if err1.Error() == ErrForbidden.Error() { + errorCode = 403 + } + if err1 == ErrNotFound { + errorCode = 404 + errorMessage = ErrNotFound.Error() + } + if errors.Is(err1, ErrInvalidLogin) { + errorCode = 401 + errorMessage = ErrInvalidLogin.Error() + } + // console invalid erasure coding value + if errors.Is(err1, ErrInvalidErasureCodingValue) { + errorCode = 400 + errorMessage = ErrInvalidErasureCodingValue.Error() + } + if errors.Is(err1, ErrBucketBodyNotInRequest) { + errorCode = 400 + errorMessage = ErrBucketBodyNotInRequest.Error() + } + if errors.Is(err1, ErrBucketNameNotInRequest) { + errorCode = 400 + errorMessage = ErrBucketNameNotInRequest.Error() + } + if errors.Is(err1, ErrGroupBodyNotInRequest) { + errorCode = 400 + errorMessage = ErrGroupBodyNotInRequest.Error() + } + if errors.Is(err1, ErrGroupNameNotInRequest) { + errorCode = 400 + errorMessage = ErrGroupNameNotInRequest.Error() + } + if errors.Is(err1, ErrPolicyNameNotInRequest) { + errorCode = 400 + errorMessage = ErrPolicyNameNotInRequest.Error() + } + if errors.Is(err1, ErrPolicyBodyNotInRequest) { + errorCode = 400 + errorMessage = ErrPolicyBodyNotInRequest.Error() + } + // console invalid session errors + if errors.Is(err1, ErrInvalidSession) { + errorCode = 401 + errorMessage = ErrInvalidSession.Error() + } + if errors.Is(err1, ErrGroupAlreadyExists) { + errorCode = 400 + errorMessage = ErrGroupAlreadyExists.Error() + } + // Bucket life cycle not configured + if errors.Is(err1, ErrBucketLifeCycleNotConfigured) { + errorCode = 404 + errorMessage = ErrBucketLifeCycleNotConfigured.Error() + } + // Encryption not configured + if errors.Is(err1, ErrSSENotConfigured) { + errorCode = 404 + errorMessage = ErrSSENotConfigured.Error() + } + // account change password + if errors.Is(err1, ErrChangePassword) { + errorCode = 403 + errorMessage = ErrChangePassword.Error() + } + if madmin.ToErrorResponse(err1).Code == "SignatureDoesNotMatch" { + errorCode = 403 + errorMessage = ErrChangePassword.Error() + } + if errors.Is(err1, ErrLicenseNotFound) { + errorCode = 404 + errorMessage = ErrLicenseNotFound.Error() + } + if errors.Is(err1, ErrInvalidLicense) { + errorCode = 404 + errorMessage = ErrInvalidLicense.Error() + } + if errors.Is(err1, ErrAvoidSelfAccountDelete) { + errorCode = 403 + errorMessage = ErrAvoidSelfAccountDelete.Error() + } + if errors.Is(err1, ErrAccessDenied) { + errorCode = 403 + errorMessage = ErrAccessDenied.Error() + } + if errors.Is(err1, ErrPolicyNotFound) { + errorCode = 404 + errorMessage = ErrPolicyNotFound.Error() + } + if madmin.ToErrorResponse(err1).Code == "AccessDenied" { + errorCode = 403 + errorMessage = ErrAccessDenied.Error() + } + if madmin.ToErrorResponse(err1).Code == "InvalidAccessKeyId" { + errorCode = 401 + errorMessage = ErrInvalidSession.Error() + } + // console invalid session errors + if madmin.ToErrorResponse(err1).Code == "XMinioAdminNoSuchUser" { + errorCode = 401 + errorMessage = ErrInvalidSession.Error() + } + // tiering errors + if err1.Error() == ErrRemoteTierExists.Error() { + errorCode = 400 + errorMessage = err1.Error() + } + if err1.Error() == ErrRemoteTierNotFound.Error() { + errorCode = 400 + errorMessage = err1.Error() + } + + if err1.Error() == ErrRemoteTierUppercase.Error() { + errorCode = 400 + errorMessage = err1.Error() + } + if err1.Error() == ErrRemoteTierBucketNotFound.Error() { + errorCode = 400 + errorMessage = err1.Error() + } + if err1.Error() == ErrRemoteInvalidCredentials.Error() { + errorCode = 403 + errorMessage = err1.Error() + } + if err1.Error() == ErrFileTooLarge.Error() { + errorCode = 413 + errorMessage = err1.Error() + } + // bucket already exists + if minio.ToErrorResponse(err1).Code == "BucketAlreadyOwnedByYou" { + errorCode = 400 + errorMessage = "Bucket already exists" + } + LogError(err1.Error(), err...) + LogIf(ctx, err1, err...) + } + + if len(err) > 1 && err[1] != nil { + if err2, ok := err[1].(error); ok { + errorMessage = err2.Error() + } + } + } + return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err1.Error())} + +} + +// Error receives an errors object and parse it against k8sErrors, returns the right errors code paired with a generic errors message +func Error(err ...interface{}) *models.Error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + return ErrorWithContext(ctx, err...) +} diff --git a/restapi/errors_test.go b/restapi/errors_test.go new file mode 100644 index 000000000..561693627 --- /dev/null +++ b/restapi/errors_test.go @@ -0,0 +1,138 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package restapi + +import ( + "context" + "fmt" + "testing" + + "github.com/go-openapi/swag" + "github.com/minio/console/models" + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + type args struct { + err []interface{} + } + + type testError struct { + name string + args args + want *models.Error + } + + var tests []testError + + type expectedError struct { + err error + code int + } + + appErrors := map[string]expectedError{ + "ErrDefault": {code: 500, err: ErrDefault}, + "ErrInvalidLogin": {code: 401, err: ErrInvalidLogin}, + "ErrForbidden": {code: 403, err: ErrForbidden}, + "ErrFileTooLarge": {code: 413, err: ErrFileTooLarge}, + "ErrInvalidSession": {code: 401, err: ErrInvalidSession}, + "ErrNotFound": {code: 404, err: ErrNotFound}, + "ErrGroupAlreadyExists": {code: 400, err: ErrGroupAlreadyExists}, + "ErrInvalidErasureCodingValue": {code: 400, err: ErrInvalidErasureCodingValue}, + "ErrBucketBodyNotInRequest": {code: 400, err: ErrBucketBodyNotInRequest}, + "ErrBucketNameNotInRequest": {code: 400, err: ErrBucketNameNotInRequest}, + "ErrGroupBodyNotInRequest": {code: 400, err: ErrGroupBodyNotInRequest}, + "ErrGroupNameNotInRequest": {code: 400, err: ErrGroupNameNotInRequest}, + "ErrPolicyNameNotInRequest": {code: 400, err: ErrPolicyNameNotInRequest}, + "ErrPolicyBodyNotInRequest": {code: 400, err: ErrPolicyBodyNotInRequest}, + "ErrInvalidEncryptionAlgorithm": {code: 500, err: ErrInvalidEncryptionAlgorithm}, + "ErrSSENotConfigured": {code: 404, err: ErrSSENotConfigured}, + "ErrBucketLifeCycleNotConfigured": {code: 404, err: ErrBucketLifeCycleNotConfigured}, + "ErrChangePassword": {code: 403, err: ErrChangePassword}, + "ErrInvalidLicense": {code: 404, err: ErrInvalidLicense}, + "ErrLicenseNotFound": {code: 404, err: ErrLicenseNotFound}, + "ErrAvoidSelfAccountDelete": {code: 403, err: ErrAvoidSelfAccountDelete}, + "ErrAccessDenied": {code: 403, err: ErrAccessDenied}, + "ErrNonUniqueAccessKey": {code: 500, err: ErrNonUniqueAccessKey}, + "ErrRemoteTierExists": {code: 400, err: ErrRemoteTierExists}, + "ErrRemoteTierNotFound": {code: 400, err: ErrRemoteTierNotFound}, + "ErrRemoteTierUppercase": {code: 400, err: ErrRemoteTierUppercase}, + "ErrRemoteTierBucketNotFound": {code: 400, err: ErrRemoteTierBucketNotFound}, + "ErrRemoteInvalidCredentials": {code: 403, err: ErrRemoteInvalidCredentials}, + "ErrUnableToGetTenantUsage": {code: 500, err: ErrUnableToGetTenantUsage}, + "ErrTooManyNodes": {code: 500, err: ErrTooManyNodes}, + "ErrTooFewNodes": {code: 500, err: ErrTooFewNodes}, + "ErrTooFewAvailableNodes": {code: 500, err: ErrTooFewAvailableNodes}, + "ErrFewerThanFourNodes": {code: 500, err: ErrFewerThanFourNodes}, + "ErrUnableToGetTenantLogs": {code: 500, err: ErrUnableToGetTenantLogs}, + "ErrUnableToUpdateTenantCertificates": {code: 500, err: ErrUnableToUpdateTenantCertificates}, + "ErrUpdatingEncryptionConfig": {code: 500, err: ErrUpdatingEncryptionConfig}, + "ErrDeletingEncryptionConfig": {code: 500, err: ErrDeletingEncryptionConfig}, + "ErrEncryptionConfigNotFound": {code: 500, err: ErrEncryptionConfigNotFound}, + } + + for k, e := range appErrors { + tests = append(tests, testError{ + name: fmt.Sprintf("%s error", k), + args: args{ + err: []interface{}{e.err}, + }, + want: &models.Error{Code: int32(e.code), Message: swag.String(e.err.Error()), DetailedMessage: swag.String(e.err.Error())}, + }) + } + tests = append(tests, + testError{ + name: "passing multiple errors", + args: args{ + err: []interface{}{ErrDefault, ErrInvalidLogin}, + }, + want: &models.Error{Code: int32(500), Message: swag.String(ErrDefault.Error()), DetailedMessage: swag.String(ErrDefault.Error())}, + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Error(tt.args.err...) + assert.Equalf(t, tt.want.Code, got.Code, "Error(%v) Got (%v)", tt.want.Code, got.Code) + assert.Equalf(t, *tt.want.DetailedMessage, *got.DetailedMessage, "Error(%s) Got (%s)", *tt.want.DetailedMessage, *got.DetailedMessage) + }) + } +} + +func TestErrorWithContext(t *testing.T) { + type args struct { + ctx context.Context + err []interface{} + } + tests := []struct { + name string + args args + want *models.Error + }{ + { + name: "default error", + args: args{ + ctx: context.Background(), + err: []interface{}{ErrDefault}, + }, + want: &models.Error{Code: 500, Message: swag.String(ErrDefault.Error()), DetailedMessage: swag.String(ErrDefault.Error())}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, ErrorWithContext(tt.args.ctx, tt.args.err...), "ErrorWithContext(%v, %v)", tt.args.ctx, tt.args.err) + }) + } +} diff --git a/restapi/logs.go b/restapi/logs.go index 8222ec552..38ce72cee 100644 --- a/restapi/logs.go +++ b/restapi/logs.go @@ -18,6 +18,7 @@ package restapi import ( + "context" "errors" "log" "os" @@ -36,10 +37,14 @@ func logError(msg string, data ...interface{}) { errorLog.Printf(msg+"\n", data...) } +func logIf(ctx context.Context, err error, errKind ...interface{}) { +} + // globally changeable logger styles var ( LogInfo = logInfo LogError = logError + LogIf = logIf ) // Context captures all command line flags values diff --git a/restapi/tls.go b/restapi/tls.go index 56c630fc1..1f2077d33 100644 --- a/restapi/tls.go +++ b/restapi/tls.go @@ -23,7 +23,8 @@ import ( "time" ) -func prepareSTSClientTransport(insecure bool) *http.Transport { +// PrepareSTSClientTransport : +func PrepareSTSClientTransport(insecure bool) *http.Transport { // This takes github.com/minio/madmin-go/transport.go as an example // // DefaultTransport - this default transport is similar to @@ -56,7 +57,7 @@ func prepareSTSClientTransport(insecure bool) *http.Transport { // PrepareConsoleHTTPClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole // custom configurations include the use of CA certificates func PrepareConsoleHTTPClient(insecure bool) *http.Client { - transport := prepareSTSClientTransport(insecure) + transport := PrepareSTSClientTransport(insecure) // Return http client with default configuration c := &http.Client{ Transport: transport, diff --git a/restapi/user_account.go b/restapi/user_account.go index 00ff98018..d8f81645a 100644 --- a/restapi/user_account.go +++ b/restapi/user_account.go @@ -55,7 +55,7 @@ func changePassword(ctx context.Context, client MinioAdmin, session *models.Prin // getChangePasswordResponse will validate user knows what is the current password (avoid account hijacking), update user account password // and authenticate the user generating a new session token/cookie func getChangePasswordResponse(session *models.Principal, params accountApi.AccountChangePasswordParams) (*models.LoginResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // changePassword operations requires an AdminClient initialized with parent account credentials not // STS credentials @@ -64,7 +64,7 @@ func getChangePasswordResponse(session *models.Principal, params accountApi.Acco STSSecretAccessKey: *params.Body.CurrentSecretKey, }) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // parentAccountClient will contain access and secret key credentials for the user userClient := AdminClient{Client: parentAccountClient} @@ -73,18 +73,18 @@ func getChangePasswordResponse(session *models.Principal, params accountApi.Acco // currentSecretKey will compare currentSecretKey against the stored secret key inside the encrypted session if err := changePassword(ctx, userClient, session, newSecretKey); err != nil { - return nil, prepareError(errChangePassword, nil, err) + return nil, ErrorWithContext(ctx, ErrChangePassword, nil, err) } // user credentials are updated at this point, we need to generate a new admin client and authenticate using // the new credentials credentials, err := getConsoleCredentials(accessKey, newSecretKey) if err != nil { - return nil, prepareError(errInvalidCredentials, nil, err) + return nil, ErrorWithContext(ctx, ErrInvalidLogin, nil, err) } // authenticate user and generate new session token sessionID, err := login(credentials, &auth.SessionFeatures{HideMenu: session.Hm}) if err != nil { - return nil, prepareError(errInvalidCredentials, nil, err) + return nil, ErrorWithContext(ctx, ErrInvalidLogin, nil, err) } // serialize output loginResponse := &models.LoginResponse{ diff --git a/restapi/user_account_test.go b/restapi/user_account_test.go index 1d3e3006c..8f7e95948 100644 --- a/restapi/user_account_test.go +++ b/restapi/user_account_test.go @@ -19,6 +19,7 @@ package restapi import ( "context" "errors" + "net/http" "testing" "github.com/minio/console/models" @@ -36,6 +37,7 @@ func Test_getChangePasswordResponse(t *testing.T) { CurrentSecretKey := "string" NewSecretKey := "string" changePasswordParameters := accountApi.AccountChangePasswordParams{ + HTTPRequest: &http.Request{}, Body: &models.AccountChangePasswordRequest{ CurrentSecretKey: &CurrentSecretKey, NewSecretKey: &NewSecretKey, diff --git a/restapi/user_bucket_quota.go b/restapi/user_bucket_quota.go index ca5b4500c..2fb66431d 100644 --- a/restapi/user_bucket_quota.go +++ b/restapi/user_bucket_quota.go @@ -21,8 +21,6 @@ import ( "errors" "fmt" - "github.com/go-openapi/swag" - "github.com/go-openapi/runtime/middleware" "github.com/minio/console/restapi/operations" bucektApi "github.com/minio/console/restapi/operations/bucket" @@ -53,19 +51,17 @@ func registerBucketQuotaHandlers(api *operations.ConsoleAPI) { } func setBucketQuotaResponse(session *models.Principal, params bucektApi.SetBucketQuotaParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - - if err := setBucketQuota(params.HTTPRequest.Context(), &adminClient, ¶ms.Name, params.Body); err != nil { - return &models.Error{ - Code: 500, - Message: swag.String(err.Error()), - } + if err := setBucketQuota(ctx, &adminClient, ¶ms.Name, params.Body); err != nil { + return ErrorWithContext(ctx, err) } return nil } @@ -97,32 +93,28 @@ func setBucketQuota(ctx context.Context, ac *AdminClient, bucket *string, bucket } func getBucketQuotaResponse(session *models.Principal, params bucektApi.GetBucketQuotaParams) (*models.BucketQuota, *models.Error) { - + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - quota, err := getBucketQuota(params.HTTPRequest.Context(), &adminClient, ¶ms.Name) + quota, err := getBucketQuota(ctx, &adminClient, ¶ms.Name) if err != nil { - return nil, &models.Error{ - Code: 500, - Message: swag.String(err.Error()), - } + return nil, ErrorWithContext(ctx, err) } return quota, nil } func getBucketQuota(ctx context.Context, ac *AdminClient, bucket *string) (*models.BucketQuota, error) { - quota, err := ac.getBucketQuota(ctx, *bucket) if err != nil { return nil, err } - return &models.BucketQuota{ Quota: int64(quota.Quota), Type: string(quota.Type), diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go index e29b7a740..4fd4a1212 100644 --- a/restapi/user_buckets.go +++ b/restapi/user_buckets.go @@ -47,7 +47,7 @@ import ( func registerBucketsHandlers(api *operations.ConsoleAPI) { // list buckets api.BucketListBucketsHandler = bucketApi.ListBucketsHandlerFunc(func(params bucketApi.ListBucketsParams, session *models.Principal) middleware.Responder { - listBucketsResponse, err := getListBucketsResponse(session) + listBucketsResponse, err := getListBucketsResponse(session, params) if err != nil { return bucketApi.NewListBucketsDefault(int(err.Code)).WithPayload(err) } @@ -55,7 +55,7 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // make bucket api.BucketMakeBucketHandler = bucketApi.MakeBucketHandlerFunc(func(params bucketApi.MakeBucketParams, session *models.Principal) middleware.Responder { - if err := getMakeBucketResponse(session, params.Body); err != nil { + if err := getMakeBucketResponse(session, params); err != nil { return bucketApi.NewMakeBucketDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewMakeBucketCreated() @@ -79,7 +79,7 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // set bucket policy api.BucketBucketSetPolicyHandler = bucketApi.BucketSetPolicyHandlerFunc(func(params bucketApi.BucketSetPolicyParams, session *models.Principal) middleware.Responder { - bucketSetPolicyResp, err := getBucketSetPolicyResponse(session, params.Name, params.Body) + bucketSetPolicyResp, err := getBucketSetPolicyResponse(session, params) if err != nil { return bucketApi.NewBucketSetPolicyDefault(int(err.Code)).WithPayload(err) } @@ -87,7 +87,7 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // set bucket tags api.BucketPutBucketTagsHandler = bucketApi.PutBucketTagsHandlerFunc(func(params bucketApi.PutBucketTagsParams, session *models.Principal) middleware.Responder { - err := getPutBucketTagsResponse(session, params.BucketName, params.Body) + err := getPutBucketTagsResponse(session, params) if err != nil { return bucketApi.NewPutBucketTagsDefault(int(err.Code)).WithPayload(err) } @@ -95,15 +95,15 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // get bucket versioning api.BucketGetBucketVersioningHandler = bucketApi.GetBucketVersioningHandlerFunc(func(params bucketApi.GetBucketVersioningParams, session *models.Principal) middleware.Responder { - getBucketVersioning, err := getBucketVersionedResponse(session, params.BucketName) + getBucketVersioning, err := getBucketVersionedResponse(session, params) if err != nil { - return bucketApi.NewGetBucketVersioningDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewGetBucketVersioningDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewGetBucketVersioningOK().WithPayload(getBucketVersioning) }) // update bucket versioning api.BucketSetBucketVersioningHandler = bucketApi.SetBucketVersioningHandlerFunc(func(params bucketApi.SetBucketVersioningParams, session *models.Principal) middleware.Responder { - err := setBucketVersioningResponse(session, params.BucketName, ¶ms) + err := setBucketVersioningResponse(session, params) if err != nil { return bucketApi.NewSetBucketVersioningDefault(500).WithPayload(err) } @@ -111,17 +111,17 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // get bucket replication api.BucketGetBucketReplicationHandler = bucketApi.GetBucketReplicationHandlerFunc(func(params bucketApi.GetBucketReplicationParams, session *models.Principal) middleware.Responder { - getBucketReplication, err := getBucketReplicationResponse(session, params.BucketName) + getBucketReplication, err := getBucketReplicationResponse(session, params) if err != nil { - return bucketApi.NewGetBucketReplicationDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewGetBucketReplicationDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewGetBucketReplicationOK().WithPayload(getBucketReplication) }) // get single bucket replication rule api.BucketGetBucketReplicationRuleHandler = bucketApi.GetBucketReplicationRuleHandlerFunc(func(params bucketApi.GetBucketReplicationRuleParams, session *models.Principal) middleware.Responder { - getBucketReplicationRule, err := getBucketReplicationRuleResponse(session, params.BucketName, params.RuleID) + getBucketReplicationRule, err := getBucketReplicationRuleResponse(session, params) if err != nil { - return bucketApi.NewGetBucketReplicationRuleDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewGetBucketReplicationRuleDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewGetBucketReplicationRuleOK().WithPayload(getBucketReplicationRule) }) @@ -157,7 +157,7 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // get bucket retention config api.BucketGetBucketRetentionConfigHandler = bucketApi.GetBucketRetentionConfigHandlerFunc(func(params bucketApi.GetBucketRetentionConfigParams, session *models.Principal) middleware.Responder { - response, err := getBucketRetentionConfigResponse(session, params.BucketName) + response, err := getBucketRetentionConfigResponse(session, params) if err != nil { return bucketApi.NewGetBucketRetentionConfigDefault(int(err.Code)).WithPayload(err) } @@ -165,9 +165,9 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) { }) // get bucket object locking status api.BucketGetBucketObjectLockingStatusHandler = bucketApi.GetBucketObjectLockingStatusHandlerFunc(func(params bucketApi.GetBucketObjectLockingStatusParams, session *models.Principal) middleware.Responder { - getBucketObjectLockingStatus, err := getBucketObjectLockingResponse(session, params.BucketName) + getBucketObjectLockingStatus, err := getBucketObjectLockingResponse(session, params) if err != nil { - return bucketApi.NewGetBucketObjectLockingStatusDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + return bucketApi.NewGetBucketObjectLockingStatusDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewGetBucketObjectLockingStatusOK().WithPayload(getBucketObjectLockingStatus) }) @@ -192,17 +192,20 @@ const ( func doSetVersioning(client MCClient, state VersionState) error { err := client.setVersioning(context.Background(), string(state)) if err != nil { - LogError("error setting versioning for bucket: %s", err.Cause) return err.Cause } return nil } -func setBucketVersioningResponse(session *models.Principal, bucketName string, params *bucketApi.SetBucketVersioningParams) *models.Error { +func setBucketVersioningResponse(session *models.Principal, params bucketApi.SetBucketVersioningParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + + bucketName := params.BucketName s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used @@ -215,28 +218,27 @@ func setBucketVersioningResponse(session *models.Principal, bucketName string, p } if err := doSetVersioning(amcClient, versioningState); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, fmt.Errorf("error setting versioning for bucket: %s", err)) } return nil } -func getBucketReplicationResponse(session *models.Principal, bucketName string) (*models.BucketReplicationResponse, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketReplicationResponse(session *models.Principal, params bucketApi.GetBucketReplicationParams) (*models.BucketReplicationResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} // we will tolerate this call failing - res, err := minioClient.getBucketReplication(ctx, bucketName) + res, err := minioClient.getBucketReplication(ctx, params.BucketName) if err != nil { - LogError("error versioning bucket: %v", err) + ErrorWithContext(ctx, err) } var rules []*models.BucketReplicationRule @@ -271,31 +273,30 @@ func getBucketReplicationResponse(session *models.Principal, bucketName string) return bucketRResponse, nil } -func getBucketReplicationRuleResponse(session *models.Principal, bucketName, ruleID string) (*models.BucketReplicationRule, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketReplicationRuleResponse(session *models.Principal, params bucketApi.GetBucketReplicationRuleParams) (*models.BucketReplicationRule, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} - replicationRules, err := minioClient.getBucketReplication(ctx, bucketName) + replicationRules, err := minioClient.getBucketReplication(ctx, params.BucketName) if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, err) } var foundRule replication.Rule found := false for i := range replicationRules.Rules { - if replicationRules.Rules[i].ID == ruleID { + if replicationRules.Rules[i].ID == params.RuleID { foundRule = replicationRules.Rules[i] found = true break @@ -303,7 +304,7 @@ func getBucketReplicationRuleResponse(session *models.Principal, bucketName, rul } if !found { - return nil, errors.New("no rule is set with this ID") + return nil, ErrorWithContext(ctx, errors.New("no rule is set with this ID")) } repDelMarkerStatus := false @@ -340,14 +341,13 @@ func getBucketReplicationRuleResponse(session *models.Principal, bucketName, rul return returnRule, nil } -func getBucketVersionedResponse(session *models.Principal, bucketName string) (*models.BucketVersioningResponse, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketVersionedResponse(session *models.Principal, params bucketApi.GetBucketVersioningParams) (*models.BucketVersioningResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -355,9 +355,9 @@ func getBucketVersionedResponse(session *models.Principal, bucketName string) (* minioClient := minioClient{client: mClient} // we will tolerate this call failing - res, err := minioClient.getBucketVersioning(ctx, bucketName) + res, err := minioClient.getBucketVersioning(ctx, params.BucketName) if err != nil { - LogError("error versioning bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err)) } // serialize output @@ -412,20 +412,20 @@ func getAccountBuckets(ctx context.Context, client MinioAdmin) ([]*models.Bucket } // getListBucketsResponse performs listBuckets() and serializes it to the handler's output -func getListBucketsResponse(session *models.Principal) (*models.ListBucketsResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getListBucketsResponse(session *models.Principal, params bucketApi.ListBucketsParams) (*models.ListBucketsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} buckets, err := getAccountBuckets(ctx, adminClient) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output @@ -443,16 +443,17 @@ func makeBucket(ctx context.Context, client MinioClient, bucketName string, obje } // getMakeBucketResponse performs makeBucket() to create a bucket with its access policy -func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getMakeBucketResponse(session *models.Principal, params bucketApi.MakeBucketParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // bucket request needed to proceed + br := params.Body if br == nil { - return prepareError(errBucketBodyNotInRequest) + return ErrorWithContext(ctx, ErrBucketBodyNotInRequest) } mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -464,15 +465,15 @@ func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketReque } if err := makeBucket(ctx, minioClient, *br.Name, br.Locking); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } - // make sure to delete bucket if an error occurs after bucket was created + // make sure to delete bucket if an errors occurs after bucket was created defer func() { if err != nil { - LogError("error creating bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error creating bucket: %v", err)) if err := removeBucket(minioClient, *br.Name); err != nil { - LogError("error removing bucket: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error removing bucket: %v", err)) } } }() @@ -481,14 +482,14 @@ func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketReque if br.Versioning || br.Retention != nil { s3Client, err := newS3BucketClient(session, *br.Name, "") if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used amcClient := mcClient{client: s3Client} if err = doSetVersioning(amcClient, VersionEnable); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, fmt.Errorf("error setting versioning for bucket: %s", err)) } } @@ -496,14 +497,14 @@ func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketReque if br.Quota != nil && br.Quota.Enabled != nil && *br.Quota.Enabled { mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} // we will tolerate this call failing if err := setBucketQuota(ctx, &adminClient, br.Name, br.Quota); err != nil { - LogError("error versioning bucket:", err) + ErrorWithContext(ctx, fmt.Errorf("error versioning bucket: %v", err)) } } @@ -511,7 +512,7 @@ func getMakeBucketResponse(session *models.Principal, br *models.MakeBucketReque if br.Retention != nil { err = setBucketRetentionConfig(ctx, minioClient, *br.Name, *br.Retention.Mode, *br.Retention.Unit, br.Retention.Validity) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } } return nil @@ -550,14 +551,14 @@ func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName s // getBucketSetPolicyResponse calls setBucketAccessPolicy() to set a access policy to a bucket // and returns the serialized output. -func getBucketSetPolicyResponse(session *models.Principal, bucketName string, req *models.SetBucketPolicyRequest) (*models.Bucket, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketSetPolicyResponse(session *models.Principal, params bucketApi.BucketSetPolicyParams) (*models.Bucket, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() // get updated bucket details and return it mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -565,45 +566,49 @@ func getBucketSetPolicyResponse(session *models.Principal, bucketName string, re mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - + bucketName := params.Name + req := params.Body if err := setBucketAccessPolicy(ctx, minioClient, bucketName, *req.Access, req.Definition); err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // set bucket access policy bucket, err := getBucketInfo(ctx, minioClient, adminClient, bucketName) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return bucket, nil } // putBucketTags sets tags for a bucket -func getPutBucketTagsResponse(session *models.Principal, bucketName string, req *models.PutBucketTagsRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getPutBucketTagsResponse(session *models.Principal, params bucketApi.PutBucketTagsParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} + req := params.Body + bucketName := params.BucketName + newTagSet, err := tags.NewTags(req.Tags, true) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } err = minioClient.SetBucketTagging(ctx, bucketName, newTagSet) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -615,20 +620,22 @@ func removeBucket(client MinioClient, bucketName string) error { // getDeleteBucketResponse performs removeBucket() to delete a bucket func getDeleteBucketResponse(session *models.Principal, params bucketApi.DeleteBucketParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() if params.Name == "" { - return prepareError(errBucketNameNotInRequest) + return ErrorWithContext(ctx, ErrBucketNameNotInRequest) } bucketName := params.Name mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} if err := removeBucket(minioClient, bucketName); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -638,8 +645,8 @@ func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdm var bucketAccess models.BucketAccess policyStr, err := client.getBucketPolicy(context.Background(), bucketName) if err != nil { - // we can tolerate this error - LogError("error getting bucket policy: %v", err) + // we can tolerate this errors + ErrorWithContext(ctx, fmt.Errorf("error getting bucket policy: %v", err)) } if policyStr == "" { @@ -658,8 +665,8 @@ func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdm } bucketTags, err := client.GetBucketTagging(ctx, bucketName) if err != nil { - // we can tolerate this error - LogError("error getting bucket tags: %v", err) + // we can tolerate this errors + ErrorWithContext(ctx, fmt.Errorf("error getting bucket tags: %v", err)) } bucketDetails := &models.BucketDetails{} if bucketTags != nil { @@ -692,11 +699,11 @@ func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdm // getBucketInfoResponse calls getBucketInfo() to get the bucket's info func getBucketInfoResponse(session *models.Principal, params bucketApi.BucketInfoParams) (*models.Bucket, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -704,7 +711,7 @@ func getBucketInfoResponse(session *models.Principal, params bucketApi.BucketInf mAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -712,7 +719,7 @@ func getBucketInfoResponse(session *models.Principal, params bucketApi.BucketInf bucket, err := getBucketInfo(ctx, minioClient, adminClient, params.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return bucket, nil @@ -751,24 +758,24 @@ func enableBucketEncryption(ctx context.Context, client MinioClient, bucketName case models.BucketEncryptionTypeSseDashS3: config = sse.NewConfigurationSSES3() default: - return errInvalidEncryptionAlgorithm + return ErrInvalidEncryptionAlgorithm } return client.setBucketEncryption(ctx, bucketName, config) } // enableBucketEncryptionResponse calls enableBucketEncryption() to create new encryption configuration for provided bucket name func enableBucketEncryptionResponse(session *models.Principal, params bucketApi.EnableBucketEncryptionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} if err := enableBucketEncryption(ctx, minioClient, params.BucketName, *params.Body.EncType, params.Body.KmsKeyID); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -780,17 +787,17 @@ func disableBucketEncryption(ctx context.Context, client MinioClient, bucketName // disableBucketEncryptionResponse calls disableBucketEncryption() func disableBucketEncryptionResponse(session *models.Principal, params bucketApi.DisableBucketEncryptionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} if err := disableBucketEncryption(ctx, minioClient, params.BucketName); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -801,24 +808,24 @@ func getBucketEncryptionInfo(ctx context.Context, client MinioClient, bucketName return nil, err } if len(bucketInfo.Rules) == 0 { - return nil, ErrorGeneric + return nil, ErrDefault } return &models.BucketEncryptionInfo{Algorithm: bucketInfo.Rules[0].Apply.SSEAlgorithm, KmsMasterKeyID: bucketInfo.Rules[0].Apply.KmsMasterKeyID}, nil } func getBucketEncryptionInfoResponse(session *models.Principal, params bucketApi.GetBucketEncryptionInfoParams) (*models.BucketEncryptionInfo, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} bucketInfo, err := getBucketEncryptionInfo(ctx, minioClient, params.BucketName) if err != nil { - return nil, prepareError(errSSENotConfigured, err) + return nil, ErrorWithContext(ctx, ErrSSENotConfigured, err) } return bucketInfo, nil } @@ -854,18 +861,18 @@ func setBucketRetentionConfig(ctx context.Context, client MinioClient, bucketNam } func getSetBucketRetentionConfigResponse(session *models.Principal, params bucketApi.SetBucketRetentionConfigParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} err = setBucketRetentionConfig(ctx, minioClient, params.BucketName, *params.Body.Mode, *params.Body.Unit, params.Body.Validity) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -925,12 +932,13 @@ func getBucketRetentionConfig(ctx context.Context, client MinioClient, bucketNam return config, nil } -func getBucketRetentionConfigResponse(session *models.Principal, bucketName string) (*models.GetBucketRetentionConfig, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketRetentionConfigResponse(session *models.Principal, params bucketApi.GetBucketRetentionConfigParams) (*models.GetBucketRetentionConfig, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + bucketName := params.BucketName mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation @@ -939,19 +947,18 @@ func getBucketRetentionConfigResponse(session *models.Principal, bucketName stri config, err := getBucketRetentionConfig(ctx, minioClient, bucketName) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return config, nil } -func getBucketObjectLockingResponse(session *models.Principal, bucketName string) (*models.BucketObLockingResponse, error) { - ctx, cancel := context.WithCancel(context.Background()) +func getBucketObjectLockingResponse(session *models.Principal, params bucketApi.GetBucketObjectLockingStatusParams) (*models.BucketObLockingResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - + bucketName := params.BucketName mClient, err := newMinioClient(session) if err != nil { - LogError("error creating MinIO Client: %v", err) - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating MinIO Client: %v", err)) } // create a minioClient interface implementation // defining the client to be used @@ -965,7 +972,7 @@ func getBucketObjectLockingResponse(session *models.Principal, bucketName string ObjectLockingEnabled: false, }, nil } - return nil, err + return nil, ErrorWithContext(ctx, err) } // serialize output @@ -975,21 +982,20 @@ func getBucketObjectLockingResponse(session *models.Principal, bucketName string } func getBucketRewindResponse(session *models.Principal, params bucketApi.GetBucketRewindParams) (*models.RewindResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() var prefix = "" if params.Prefix != nil { encodedPrefix := SanitizeEncodedPrefix(*params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } s3Client, err := newS3BucketClient(session, params.BucketName, prefix) if err != nil { - LogError("error creating S3Client: %v", err) - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, fmt.Errorf("error creating S3Client: %v", err)) } // create a mc S3Client interface implementation @@ -999,7 +1005,7 @@ func getBucketRewindResponse(session *models.Principal, params bucketApi.GetBuck parsedDate, errDate := time.Parse(time.RFC3339, params.Date) if errDate != nil { - return nil, prepareError(errDate) + return nil, ErrorWithContext(ctx, errDate) } var rewindItems []*models.RewindItem diff --git a/restapi/user_buckets_events.go b/restapi/user_buckets_events.go index 27dd3e895..64cef6053 100644 --- a/restapi/user_buckets_events.go +++ b/restapi/user_buckets_events.go @@ -39,14 +39,14 @@ func registerBucketEventsHandlers(api *operations.ConsoleAPI) { }) // create bucket event api.BucketCreateBucketEventHandler = bucketApi.CreateBucketEventHandlerFunc(func(params bucketApi.CreateBucketEventParams, session *models.Principal) middleware.Responder { - if err := getCreateBucketEventsResponse(session, params.BucketName, params.Body); err != nil { + if err := getCreateBucketEventsResponse(session, params); err != nil { return bucketApi.NewCreateBucketEventDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewCreateBucketEventCreated() }) // delete bucket event api.BucketDeleteBucketEventHandler = bucketApi.DeleteBucketEventHandlerFunc(func(params bucketApi.DeleteBucketEventParams, session *models.Principal) middleware.Responder { - if err := getDeleteBucketEventsResponse(session, params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil { + if err := getDeleteBucketEventsResponse(session, params); err != nil { return bucketApi.NewDeleteBucketEventDefault(int(err.Code)).WithPayload(err) } return bucketApi.NewDeleteBucketEventNoContent() @@ -125,9 +125,11 @@ func listBucketEvents(client MinioClient, bucketName string) ([]*models.Notifica // getListBucketsResponse performs listBucketEvents() and serializes it to the handler's output func getListBucketEventsResponse(session *models.Principal, params bucketApi.ListBucketEventsParams) (*models.ListBucketEventsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -135,7 +137,7 @@ func getListBucketEventsResponse(session *models.Principal, params bucketApi.Lis bucketEvents, err := listBucketEvents(minioClient, params.BucketName) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output listBucketsResponse := &models.ListBucketEventsResponse{ @@ -175,19 +177,21 @@ func createBucketEvent(ctx context.Context, client MCClient, arn string, notific } // getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification -func getCreateBucketEventsResponse(session *models.Principal, bucketName string, eventReq *models.BucketEventRequest) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getCreateBucketEventsResponse(session *models.Principal, params bucketApi.CreateBucketEventParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + bucketName := params.BucketName + eventReq := params.Body s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used mcClient := mcClient{client: s3Client} err = createBucketEvent(ctx, mcClient, *eventReq.Configuration.Arn, eventReq.Configuration.Events, eventReq.Configuration.Prefix, eventReq.Configuration.Suffix, eventReq.IgnoreExisting) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -211,19 +215,24 @@ func joinNotificationEvents(events []models.NotificationEventType) string { } // getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification -func getDeleteBucketEventsResponse(session *models.Principal, bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getDeleteBucketEventsResponse(session *models.Principal, params bucketApi.DeleteBucketEventParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + bucketName := params.BucketName + arn := params.Arn + events := params.Body.Events + prefix := params.Body.Prefix + suffix := params.Body.Suffix s3Client, err := newS3BucketClient(session, bucketName, "") if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used mcClient := mcClient{client: s3Client} err = deleteBucketEventNotification(ctx, mcClient, arn, events, prefix, suffix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } diff --git a/restapi/user_buckets_lifecycle.go b/restapi/user_buckets_lifecycle.go index 6460acaa0..fa3f13938 100644 --- a/restapi/user_buckets_lifecycle.go +++ b/restapi/user_buckets_lifecycle.go @@ -142,11 +142,11 @@ func getBucketLifecycle(ctx context.Context, client MinioClient, bucketName stri // getBucketLifecycleResponse performs getBucketLifecycle() and serializes it to the handler's output func getBucketLifecycleResponse(session *models.Principal, params bucketApi.GetBucketLifecycleParams) (*models.BucketLifecycleResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -154,7 +154,7 @@ func getBucketLifecycleResponse(session *models.Principal, params bucketApi.GetB bucketEvents, err := getBucketLifecycle(ctx, minioClient, params.BucketName) if err != nil { - return nil, prepareError(errBucketLifeCycleNotConfigured, err) + return nil, ErrorWithContext(ctx, ErrBucketLifeCycleNotConfigured, err) } return bucketEvents, nil } @@ -226,7 +226,7 @@ func addBucketLifecycle(ctx context.Context, client MinioClient, params bucketAp } } else { - // Non set, we return error + // Non set, we return errors return errors.New("no valid configuration requested") } @@ -241,11 +241,11 @@ func addBucketLifecycle(ctx context.Context, client MinioClient, params bucketAp // getAddBucketLifecycleResponse returns the response of adding a bucket lifecycle response func getAddBucketLifecycleResponse(session *models.Principal, params bucketApi.AddBucketLifecycleParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -253,7 +253,7 @@ func getAddBucketLifecycleResponse(session *models.Principal, params bucketApi.A err = addBucketLifecycle(ctx, minioClient, params) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil @@ -325,7 +325,7 @@ func editBucketLifecycle(ctx context.Context, client MinioClient, params bucketA } } else { - // Non set, we return error + // Non set, we return errors return errors.New("no valid configuration requested") } @@ -340,11 +340,11 @@ func editBucketLifecycle(ctx context.Context, client MinioClient, params bucketA // getEditBucketLifecycleRule returns the response of bucket lifecycle tier edit func getEditBucketLifecycleRule(session *models.Principal, params bucketApi.UpdateBucketLifecycleParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -352,7 +352,7 @@ func getEditBucketLifecycleRule(session *models.Principal, params bucketApi.Upda err = editBucketLifecycle(ctx, minioClient, params) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil @@ -394,11 +394,11 @@ func deleteBucketLifecycle(ctx context.Context, client MinioClient, params bucke // getDeleteBucketLifecycleRule returns the response of bucket lifecycle tier delete func getDeleteBucketLifecycleRule(session *models.Principal, params bucketApi.DeleteBucketLifecycleRuleParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -406,7 +406,7 @@ func getDeleteBucketLifecycleRule(session *models.Principal, params bucketApi.De err = deleteBucketLifecycle(ctx, minioClient, params) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil @@ -480,11 +480,11 @@ func addMultiBucketLifecycle(ctx context.Context, client MinioClient, params buc // getAddMultiBucketLifecycleResponse returns the response of multibucket lifecycle assignment func getAddMultiBucketLifecycleResponse(session *models.Principal, params bucketApi.AddMultiBucketLifecycleParams) (*models.MultiLifecycleResult, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used diff --git a/restapi/user_buckets_test.go b/restapi/user_buckets_test.go index 813e81fb0..27dec79a3 100644 --- a/restapi/user_buckets_test.go +++ b/restapi/user_buckets_test.go @@ -143,18 +143,17 @@ func TestMakeBucket(t *testing.T) { // mock minIO client minClient := minioClientMock{} function := "makeBucket()" - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() // Test-1: makeBucket() create a bucket // mock function response from makeBucketWithContext(ctx) minioMakeBucketWithContextMock = func(ctx context.Context, bucketName, location string, objectLock bool) error { return nil } if err := makeBucket(ctx, minClient, "bucktest1", true); err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } - // Test-2 makeBucket() make sure errors are handled correctly when error on MakeBucketWithContext + // Test-2 makeBucket() make sure errors are handled correctly when errors on MakeBucketWithContext minioMakeBucketWithContextMock = func(ctx context.Context, bucketName, location string, objectLock bool) error { return errors.New("error") } @@ -175,10 +174,10 @@ func TestDeleteBucket(t *testing.T) { return nil } if err := removeBucket(minClient, "bucktest1"); err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } - // Test-2: removeBucket() make sure errors are handled correctly when error on DeleteBucket() + // Test-2: removeBucket() make sure errors are handled correctly when errors on DeleteBucket() // mock function response from removeBucket(bucketName) minioRemoveBucketMock = func(bucketName string) error { return errors.New("error") @@ -193,8 +192,7 @@ func TestBucketInfo(t *testing.T) { // mock minIO client minClient := minioClientMock{} adminClient := adminClientMock{} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() function := "getBucketInfo()" // Test-1: getBucketInfo() get a bucket with PRIVATE access @@ -248,7 +246,7 @@ func TestBucketInfo(t *testing.T) { bucketInfo, err := getBucketInfo(ctx, minClient, adminClient, bucketToSet) if err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } assert.Equal(outputExpected.Name, bucketInfo.Name) assert.Equal(outputExpected.Access, bucketInfo.Access) @@ -272,7 +270,7 @@ func TestBucketInfo(t *testing.T) { } bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet) if err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } assert.Equal(outputExpected.Name, bucketInfo.Name) assert.Equal(outputExpected.Access, bucketInfo.Access) @@ -296,7 +294,7 @@ func TestBucketInfo(t *testing.T) { } bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet) if err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } assert.Equal(outputExpected.Name, bucketInfo.Name) assert.Equal(outputExpected.Access, bucketInfo.Access) @@ -304,7 +302,7 @@ func TestBucketInfo(t *testing.T) { assert.Equal(outputExpected.Size, bucketInfo.Size) assert.Equal(outputExpected.Objects, bucketInfo.Objects) - // Test-4: getBucketInfo() returns an error while parsing invalid policy + // Test-4: getBucketInfo() returns an errors while parsing invalid policy mockPolicy = "policyinvalid" minioGetBucketPolicyMock = func(bucketName string) (string, error) { return mockPolicy, nil @@ -321,14 +319,13 @@ func TestBucketInfo(t *testing.T) { assert.Equal("invalid character 'p' looking for beginning of value", err.Error()) } - // Test-4: getBucketInfo() handle GetBucketPolicy error correctly + // Test-4: getBucketInfo() handle GetBucketPolicy errors correctly // Test removed since we can tolerate this scenario now } func TestSetBucketAccess(t *testing.T) { assert := assert.New(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() // mock minIO client minClient := minioClientMock{} @@ -339,30 +336,30 @@ func TestSetBucketAccess(t *testing.T) { return nil } if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPUBLIC, ""); err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } // Test-2: setBucketAccessPolicy() set private access if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", models.BucketAccessPRIVATE, ""); err != nil { - t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + t.Errorf("Failed on %s:, errors occurred: %s", function, err.Error()) } - // Test-3: setBucketAccessPolicy() set invalid access, expected error + // Test-3: setBucketAccessPolicy() set invalid access, expected errors if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", "other", ""); assert.Error(err) { assert.Equal("access: `other` not supported", err.Error()) } - // Test-4: setBucketAccessPolicy() set access on empty bucket name, expected error + // Test-4: setBucketAccessPolicy() set access on empty bucket name, expected errors if err := setBucketAccessPolicy(ctx, minClient, "", models.BucketAccessPRIVATE, ""); assert.Error(err) { assert.Equal("error: bucket name not present", err.Error()) } - // Test-5: setBucketAccessPolicy() set empty access on bucket, expected error + // Test-5: setBucketAccessPolicy() set empty access on bucket, expected errors if err := setBucketAccessPolicy(ctx, minClient, "bucktest1", "", ""); assert.Error(err) { assert.Equal("error: bucket access not present", err.Error()) } - // Test-5: setBucketAccessPolicy() handle error on setPolicy call + // Test-5: setBucketAccessPolicy() handle errors on setPolicy call minioSetBucketPolicyWithContextMock = func(ctx context.Context, bucketName, policy string) error { return errors.New("error") } @@ -373,8 +370,7 @@ func TestSetBucketAccess(t *testing.T) { } func Test_enableBucketEncryption(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() minClient := minioClientMock{} type args struct { ctx context.Context @@ -410,7 +406,7 @@ func Test_enableBucketEncryption(t *testing.T) { bucketName: "test", encryptionType: "sse-s3", mockEnableBucketEncryptionFunc: func(ctx context.Context, bucketName string, config *sse.Configuration) error { - return errorGenericInvalidSession + return ErrInvalidSession }, }, wantErr: true, @@ -420,15 +416,14 @@ func Test_enableBucketEncryption(t *testing.T) { t.Run(tt.name, func(t *testing.T) { minioSetBucketEncryptionMock = tt.args.mockEnableBucketEncryptionFunc if err := enableBucketEncryption(tt.args.ctx, tt.args.client, tt.args.bucketName, tt.args.encryptionType, tt.args.kmsKeyID); (err != nil) != tt.wantErr { - t.Errorf("enableBucketEncryption() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("enableBucketEncryption() errors = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_disableBucketEncryption(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() minClient := minioClientMock{} type args struct { ctx context.Context @@ -460,7 +455,7 @@ func Test_disableBucketEncryption(t *testing.T) { client: minClient, bucketName: "test", mockBucketDisableFunc: func(ctx context.Context, bucketName string) error { - return ErrorGeneric + return ErrDefault }, }, wantErr: true, @@ -470,15 +465,14 @@ func Test_disableBucketEncryption(t *testing.T) { t.Run(tt.name, func(t *testing.T) { minioRemoveBucketEncryptionMock = tt.args.mockBucketDisableFunc if err := disableBucketEncryption(tt.args.ctx, tt.args.client, tt.args.bucketName); (err != nil) != tt.wantErr { - t.Errorf("disableBucketEncryption() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("disableBucketEncryption() errors = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_getBucketEncryptionInfo(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() minClient := minioClientMock{} type args struct { ctx context.Context @@ -535,7 +529,7 @@ func Test_getBucketEncryptionInfo(t *testing.T) { client: minClient, bucketName: "test", mockBucketEncryptionGet: func(ctx context.Context, bucketName string) (*sse.Configuration, error) { - return nil, errSSENotConfigured + return nil, ErrSSENotConfigured }, }, wantErr: true, @@ -546,7 +540,7 @@ func Test_getBucketEncryptionInfo(t *testing.T) { minioGetBucketEncryptionMock = tt.args.mockBucketEncryptionGet got, err := getBucketEncryptionInfo(tt.args.ctx, tt.args.client, tt.args.bucketName) if (err != nil) != tt.wantErr { - t.Errorf("getBucketEncryptionInfo() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("getBucketEncryptionInfo() errors = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { @@ -558,8 +552,7 @@ func Test_getBucketEncryptionInfo(t *testing.T) { func Test_SetBucketRetentionConfig(t *testing.T) { assert := assert.New(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() minClient := minioClientMock{} type args struct { ctx context.Context @@ -651,7 +644,7 @@ func Test_SetBucketRetentionConfig(t *testing.T) { expectedError: errors.New("invalid retention unit"), }, { - name: "Handle error on objec lock function", + name: "Handle errors on objec lock function", args: args{ ctx: ctx, client: minClient, @@ -672,9 +665,9 @@ func Test_SetBucketRetentionConfig(t *testing.T) { err := setBucketRetentionConfig(tt.args.ctx, tt.args.client, tt.args.bucketName, tt.args.mode, tt.args.unit, tt.args.validity) if tt.expectedError != nil { fmt.Println(t.Name()) - assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("setObjectRetention() error: `%s`, wantErr: `%s`", err, tt.expectedError)) + assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("setObjectRetention() errors: `%s`, wantErr: `%s`", err, tt.expectedError)) } else { - assert.Nil(err, fmt.Sprintf("setBucketRetentionConfig() error: %v, wantErr: %v", err, tt.expectedError)) + assert.Nil(err, fmt.Sprintf("setBucketRetentionConfig() errors: %v, wantErr: %v", err, tt.expectedError)) } }) } @@ -682,8 +675,7 @@ func Test_SetBucketRetentionConfig(t *testing.T) { func Test_GetBucketRetentionConfig(t *testing.T) { assert := assert.New(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() minClient := minioClientMock{} type args struct { ctx context.Context @@ -751,7 +743,7 @@ func Test_GetBucketRetentionConfig(t *testing.T) { { // Description: if minio return NoSuchObjectLockConfiguration, don't panic // and return empty response - name: "Handle NoLock Config error", + name: "Handle NoLock Config errors", args: args{ ctx: ctx, client: minClient, @@ -767,7 +759,7 @@ func Test_GetBucketRetentionConfig(t *testing.T) { expectedError: nil, }, { - name: "Return error on invalid mode", + name: "Return errors on invalid mode", args: args{ ctx: ctx, client: minClient, @@ -782,7 +774,7 @@ func Test_GetBucketRetentionConfig(t *testing.T) { expectedError: errors.New("invalid retention mode"), }, { - name: "Return error on invalid unit", + name: "Return errors on invalid unit", args: args{ ctx: ctx, client: minClient, @@ -805,9 +797,9 @@ func Test_GetBucketRetentionConfig(t *testing.T) { if tt.expectedError != nil { fmt.Println(t.Name()) - assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("getBucketRetentionConfig() error: `%s`, wantErr: `%s`", err, tt.expectedError)) + assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("getBucketRetentionConfig() errors: `%s`, wantErr: `%s`", err, tt.expectedError)) } else { - assert.Nil(err, fmt.Sprintf("getBucketRetentionConfig() error: %v, wantErr: %v", err, tt.expectedError)) + assert.Nil(err, fmt.Sprintf("getBucketRetentionConfig() errors: %v, wantErr: %v", err, tt.expectedError)) if !reflect.DeepEqual(resp, tt.expectedResponse) { t.Errorf("getBucketRetentionConfig() resp: %v, expectedResponse: %v", resp, tt.expectedResponse) return @@ -819,8 +811,7 @@ func Test_GetBucketRetentionConfig(t *testing.T) { func Test_SetBucketVersioning(t *testing.T) { assert := assert.New(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := context.Background() errorMsg := "Error Message" minClient := s3ClientMock{} type args struct { @@ -874,7 +865,7 @@ func Test_SetBucketVersioning(t *testing.T) { if tt.expectedError != nil { fmt.Println(t.Name()) - assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("getBucketRetentionConfig() error: `%s`, wantErr: `%s`", err, tt.expectedError)) + assert.Equal(tt.expectedError.Error(), err.Error(), fmt.Sprintf("getBucketRetentionConfig() errors: `%s`, wantErr: `%s`", err, tt.expectedError)) } }) } @@ -1166,7 +1157,7 @@ func Test_getAccountBuckets(t *testing.T) { args: args{ ctx: context.Background(), mockBucketList: madmin.AccountInfo{}, - mockError: errors.New("some error"), + mockError: errors.New("some errors"), }, want: []*models.Bucket{}, wantErr: assert.Error, diff --git a/restapi/user_log_search.go b/restapi/user_log_search.go index 2ddc41457..4f9c831d5 100644 --- a/restapi/user_log_search.go +++ b/restapi/user_log_search.go @@ -17,6 +17,7 @@ package restapi import ( + "context" "encoding/json" "fmt" "net/http" @@ -42,7 +43,9 @@ func registerLogSearchHandlers(api *operations.ConsoleAPI) { // getLogSearchResponse performs a query to Log Search if Enabled func getLogSearchResponse(session *models.Principal, params logApi.LogSearchParams) (*models.LogSearchResponse, *models.Error) { - sessionResp, err := getSessionResponse(session) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + sessionResp, err := getSessionResponse(ctx, session) if err != nil { return nil, err } @@ -80,31 +83,28 @@ func getLogSearchResponse(session *models.Principal, params logApi.LogSearchPara endpoint = fmt.Sprintf("%s&pageSize=%d", endpoint, *params.PageSize) endpoint = fmt.Sprintf("%s&pageNo=%d", endpoint, *params.PageNo) - return logSearch(endpoint) + response, errLogSearch := logSearch(endpoint) + if errLogSearch != nil { + return nil, ErrorWithContext(ctx, errLogSearch) + } + return response, nil } -func logSearch(endpoint string) (*models.LogSearchResponse, *models.Error) { +func logSearch(endpoint string) (*models.LogSearchResponse, error) { httpClnt := GetConsoleHTTPClient() resp, err := httpClnt.Get(endpoint) if err != nil { - return nil, &models.Error{ - Code: int32(500), - Message: swag.String("Log Search API not available."), - DetailedMessage: swag.String("The Log Search API cannot be reached. Please review the URL and try again."), - } + return nil, fmt.Errorf("the Log Search API cannot be reached. Please review the URL and try again %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, &models.Error{ - Code: int32(resp.StatusCode), - Message: swag.String(fmt.Sprintf("error retrieving logs: %s", http.StatusText(resp.StatusCode))), - } + return nil, fmt.Errorf("error retrieving logs: %s", http.StatusText(resp.StatusCode)) } var results []map[string]interface{} if err = json.NewDecoder(resp.Body).Decode(&results); err != nil { - return nil, prepareError(err) + return nil, err } return &models.LogSearchResponse{ diff --git a/restapi/user_log_search_test.go b/restapi/user_log_search_test.go index 0ea4032a1..a311f27e9 100644 --- a/restapi/user_log_search_test.go +++ b/restapi/user_log_search_test.go @@ -17,16 +17,14 @@ package restapi import ( - "crypto/sha256" "encoding/json" "fmt" "net/http" "net/http/httptest" + "reflect" "testing" - "github.com/go-openapi/swag" "github.com/minio/console/models" - asrt "github.com/stretchr/testify/assert" ) func TestLogSearch(t *testing.T) { @@ -41,7 +39,7 @@ func TestLogSearch(t *testing.T) { "request_id": "16595A4E30CCFE79", "user_agent": "MinIO (linux; amd64) madmin-go/0.0.1", "response_status": "OK", - "response_status_code": 200, + "response_status_code": float64(200), "request_content_length": nil, "response_content_length": nil, }, { @@ -54,13 +52,12 @@ func TestLogSearch(t *testing.T) { "request_id": "16595A4DA906FBA9", "user_agent": "Go-http-client/1.1", "response_status": "OK", - "response_status_code": 200, + "response_status_code": float64(200), "request_content_length": nil, "response_content_length": nil, }, } - assert := asrt.New(t) type args struct { apiResponse string apiResponseCode int @@ -76,7 +73,7 @@ func TestLogSearch(t *testing.T) { name string args args expectedResponse *models.LogSearchResponse - expectedError *models.Error + wantErr bool }{ { name: "200 Success response", @@ -85,7 +82,7 @@ func TestLogSearch(t *testing.T) { apiResponseCode: 200, }, expectedResponse: successfulResponse, - expectedError: nil, + wantErr: false, }, { name: "500 unsuccessful response", @@ -94,10 +91,7 @@ func TestLogSearch(t *testing.T) { apiResponseCode: 500, }, expectedResponse: nil, - expectedError: &models.Error{ - Code: 500, - Message: swag.String(fmt.Sprintf("error retrieving logs: %s", http.StatusText(500))), - }, + wantErr: true, }, } @@ -112,27 +106,33 @@ func TestLogSearch(t *testing.T) { resp, err := logSearch(testRequest.URL) - if tt.expectedError != nil { - assert.Equal(tt.expectedError.Code, err.Code, fmt.Sprintf("logSearch() error code: `%v`, wantErr: `%v`", err.Code, tt.expectedError)) - assert.Equal(tt.expectedError.Message, err.Message, fmt.Sprintf("logSearch() error message: `%v`, wantErr: `%v`", err.Message, tt.expectedError)) - } else { - assert.Nil(err, fmt.Sprintf("logSearch() error: %v, wantErr: %v", err, tt.expectedError)) - buf1, err1 := tt.expectedResponse.MarshalBinary() - buf2, err2 := resp.MarshalBinary() - if err1 != err2 { - t.Errorf("logSearch() resp: %v, expectedResponse: %v", resp, tt.expectedResponse) - return - } - h := sha256.New() - h.Write(buf1) - checkSum1 := fmt.Sprintf("%x\n", h.Sum(nil)) - h.Reset() - h.Write(buf2) - checkSum2 := fmt.Sprintf("%x\n", h.Sum(nil)) - if checkSum1 != checkSum2 { - t.Errorf("logSearch() resp: %v, expectedResponse: %v", resp, tt.expectedResponse) - } + if (err != nil) != tt.wantErr { + t.Errorf("logSearch() error = %v, wantErr %v", err, tt.wantErr) } + if !reflect.DeepEqual(resp, tt.expectedResponse) { + t.Errorf("\ngot: %d \nwant: %d", resp, tt.expectedResponse) + } + //if tt.wantErr { + // assert.Equal(tt.expectedError.Code, err.Code, fmt.Sprintf("logSearch() error code: `%v`, wantErr: `%v`", err.Code, tt.expectedError)) + // assert.Equal(tt.expectedError.Message, err.Message, fmt.Sprintf("logSearch() error message: `%v`, wantErr: `%v`", err.Message, tt.expectedError)) + //} else { + // assert.Nil(err, fmt.Sprintf("logSearch() error: %v, wantErr: %v", err, tt.expectedError)) + // buf1, err1 := tt.expectedResponse.MarshalBinary() + // buf2, err2 := resp.MarshalBinary() + // if err1 != err2 { + // t.Errorf("logSearch() resp: %v, expectedResponse: %v", resp, tt.expectedResponse) + // return + // } + // h := sha256.New() + // h.Write(buf1) + // checkSum1 := fmt.Sprintf("%x\n", h.Sum(nil)) + // h.Reset() + // h.Write(buf2) + // checkSum2 := fmt.Sprintf("%x\n", h.Sum(nil)) + // if checkSum1 != checkSum2 { + // t.Errorf("logSearch() resp: %v, expectedResponse: %v", resp, tt.expectedResponse) + // } + //} }) } } diff --git a/restapi/user_login.go b/restapi/user_login.go index ab76f3dad..9b046a0e1 100644 --- a/restapi/user_login.go +++ b/restapi/user_login.go @@ -36,7 +36,7 @@ import ( func registerLoginHandlers(api *operations.ConsoleAPI) { // GET login strategy api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder { - loginDetails, err := getLoginDetailsResponse(params.HTTPRequest) + loginDetails, err := getLoginDetailsResponse(params) if err != nil { return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err) } @@ -44,7 +44,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) { }) // POST login using user credentials api.AuthLoginHandler = authApi.LoginHandlerFunc(func(params authApi.LoginParams) middleware.Responder { - loginResponse, err := getLoginResponse(params.Body) + loginResponse, err := getLoginResponse(params) if err != nil { return authApi.NewLoginDefault(int(err.Code)).WithPayload(err) } @@ -57,7 +57,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) { }) // POST login using external IDP api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder { - loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body) + loginResponse, err := getLoginOauth2AuthResponse(params) if err != nil { return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err) } @@ -82,7 +82,7 @@ func login(credentials ConsoleCredentialsI, sessionFeatures *auth.SessionFeature token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), sessionFeatures) if err != nil { LogError("error authenticating user: %v", err) - return nil, errInvalidCredentials + return nil, ErrInvalidLogin } return &token, nil } @@ -109,11 +109,14 @@ func getConsoleCredentials(accessKey, secretKey string) (*ConsoleCredentials, er } // getLoginResponse performs login() and serializes it to the handler's output -func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.Error) { +func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + lr := params.Body // prepare console credentials consoleCreds, err := getConsoleCredentials(*lr.AccessKey, *lr.SecretKey) if err != nil { - return nil, prepareError(err, errInvalidCredentials, err) + return nil, ErrorWithContext(ctx, err, ErrInvalidLogin, err) } sf := &auth.SessionFeatures{} if lr.Features != nil { @@ -121,7 +124,7 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.E } sessionID, err := login(consoleCreds, sf) if err != nil { - return nil, prepareError(err, errInvalidCredentials, err) + return nil, ErrorWithContext(ctx, err, ErrInvalidLogin, err) } // serialize output loginResponse := &models.LoginResponse{ @@ -131,16 +134,18 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.E } // getLoginDetailsResponse returns information regarding the Console authentication mechanism. -func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) { +func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() loginStrategy := models.LoginDetailsLoginStrategyForm redirectURL := "" - + r := params.HTTPRequest if oauth2.IsIDPEnabled() { loginStrategy = models.LoginDetailsLoginStrategyRedirect // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient()) if err != nil { - return nil, prepareError(err, errOauth2Provider) + return nil, ErrorWithContext(ctx, err, ErrOauth2Provider) } // Validate user against IDP identityProvider := &auth.IdentityProvider{Client: oauth2Client} @@ -159,26 +164,28 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, userCredentials, err := provider.VerifyIdentity(ctx, code, state) if err != nil { LogError("error validating user identity against idp: %v", err) - return nil, errInvalidCredentials + return nil, ErrInvalidLogin } return userCredentials, nil } -func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.LoginResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + r := params.HTTPRequest + lr := params.Body if oauth2.IsIDPEnabled() { // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient()) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // initialize new identity provider identityProvider := auth.IdentityProvider{Client: oauth2Client} // Validate user against IDP userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // initialize admin client // login user against console and generate session token @@ -187,7 +194,7 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque AccountAccessKey: "", }, nil) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // serialize output loginResponse := &models.LoginResponse{ @@ -195,5 +202,5 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque } return loginResponse, nil } - return nil, prepareError(ErrorGeneric) + return nil, ErrorWithContext(ctx, ErrDefault) } diff --git a/restapi/user_logout.go b/restapi/user_logout.go index 2b34cfdcd..d5a9fd043 100644 --- a/restapi/user_logout.go +++ b/restapi/user_logout.go @@ -46,7 +46,7 @@ func logout(credentials ConsoleCredentialsI) { credentials.Expire() } -// getLogoutResponse performs logout() and returns nil or error +// getLogoutResponse performs logout() and returns nil or errors func getLogoutResponse(session *models.Principal) { creds := getConsoleCredentialsFromSession(session) credentials := ConsoleCredentials{ConsoleCredentials: creds} diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 3452f17c1..e3772ca20 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -79,13 +79,13 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) { // download object api.ObjectDownloadObjectHandler = objectApi.DownloadObjectHandlerFunc(func(params objectApi.DownloadObjectParams, session *models.Principal) middleware.Responder { isFolder := false - + ctx := params.HTTPRequest.Context() var prefix string if params.Prefix != "" { encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return objectApi.NewDownloadObjectDefault(int(400)).WithPayload(prepareError(err)) + return objectApi.NewDownloadObjectDefault(400).WithPayload(ErrorWithContext(ctx, err)) } prefix = string(decodedPrefix) } @@ -173,6 +173,8 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) { // getListObjectsResponse returns a list of objects func getListObjectsResponse(session *models.Principal, params objectApi.ListObjectsParams) (*models.ListObjectsResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() var prefix string var recursive bool var withVersions bool @@ -181,7 +183,7 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje encodedPrefix := SanitizeEncodedPrefix(*params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } @@ -196,19 +198,19 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje } // bucket request needed to proceed if params.BucketName == "" { - return nil, prepareError(errBucketNameNotInRequest) + return nil, ErrorWithContext(ctx, ErrBucketNameNotInRequest) } mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} - objs, err := listBucketObjects(params.HTTPRequest.Context(), minioClient, params.BucketName, prefix, recursive, withVersions, withMetadata) + objs, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, recursive, withVersions, withMetadata) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } resp := &models.ListObjectsResponse{ @@ -254,7 +256,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin if err != nil { errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { - LogError("error getting legal hold status for %s : %v", lsObj.VersionID, err) + ErrorWithContext(ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err)) } } else { if legalHoldStatus != nil { @@ -266,7 +268,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin if err != nil { errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { - LogError("error getting retention status for %s : %v", lsObj.VersionID, err) + ErrorWithContext(ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err)) } } else { if retention != nil && retUntilDate != nil { @@ -277,7 +279,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin } tags, err := client.getObjectTagging(ctx, bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID}) if err != nil { - LogError("error getting object tags for %s : %v", lsObj.VersionID, err) + ErrorWithContext(ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err)) } else { obj.Tags = tags.ToMap() } @@ -367,18 +369,17 @@ func parseRange(s string, size int64) ([]httpRange, error) { } func getDownloadObjectResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *models.Error) { - ctx := context.Background() - + ctx := params.HTTPRequest.Context() var prefix string mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } if params.Prefix != "" { encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } @@ -391,7 +392,7 @@ func getDownloadObjectResponse(session *models.Principal, params objectApi.Downl resp, err := mClient.GetObject(ctx, params.BucketName, prefix, opts) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) { @@ -414,17 +415,15 @@ func getDownloadObjectResponse(session *models.Principal, params objectApi.Downl stat, err := resp.Stat() if err != nil { minErr := minio.ToErrorResponse(err) - // non-200 means we requested something wrong rw.WriteHeader(minErr.StatusCode) - - LogError("Failed to get Stat() response from server for %s (version %s): %v", prefix, opts.VersionID, minErr.Error()) + ErrorWithContext(ctx, fmt.Errorf("Failed to get Stat() response from server for %s (version %s): %v", prefix, opts.VersionID, minErr.Error())) return } // if we are getting a Range Request (video) handle that specially ranges, err := parseRange(params.HTTPRequest.Header.Get("Range"), stat.Size) if err != nil { - LogError("Unable to parse range header input %s: %v", params.HTTPRequest.Header.Get("Range"), err) + ErrorWithContext(ctx, fmt.Errorf("Unable to parse range header input %s: %v", params.HTTPRequest.Header.Get("Range"), err)) rw.WriteHeader(400) return } @@ -454,7 +453,7 @@ func getDownloadObjectResponse(session *models.Principal, params objectApi.Downl _, err = resp.Seek(start, io.SeekStart) if err != nil { - LogError("Unable to seek at offset %d: %v", start, err) + ErrorWithContext(ctx, fmt.Errorf("Unable to seek at offset %d: %v", start, err)) rw.WriteHeader(400) return } @@ -468,21 +467,21 @@ func getDownloadObjectResponse(session *models.Principal, params objectApi.Downl rw.Header().Set("Content-Length", fmt.Sprintf("%d", length)) _, err = io.Copy(rw, io.LimitReader(resp, length)) if err != nil { - LogError("Unable to write all data to client: %v", err) + ErrorWithContext(ctx, fmt.Errorf("Unable to write all data to client: %v", err)) return } }), nil } func getDownloadFolderResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *models.Error) { - ctx := context.Background() - + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() var prefix string mClient, err := newMinioClient(session) if params.Prefix != "" { encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } @@ -490,12 +489,12 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl folders := strings.Split(prefix, "/") if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } minioClient := minioClient{client: mClient} objects, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, true, false, false) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } w := new(bytes.Buffer) zipw := zip.NewWriter(w) @@ -507,11 +506,11 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl name := folder + objects[i].Name[len(prefix)-1:] object, err := mClient.GetObject(ctx, params.BucketName, objects[i].Name, minio.GetObjectOptions{}) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } f, err := zipw.Create(name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } buf := new(bytes.Buffer) buf.ReadFrom(object) @@ -530,7 +529,7 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - LogError("Unable to parse encoded prefix %s: %v", encodedPrefix, err) + ErrorWithContext(ctx, fmt.Errorf("Unable to parse encoded prefix %s: %v", encodedPrefix, err)) return } @@ -552,27 +551,27 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl // Copy the stream _, err := io.Copy(rw, resp) if err != nil { - LogError("Unable to write all the requested data: %v", err) + ErrorWithContext(ctx, fmt.Errorf("Unable to write all the requested data: %v", err)) } }), nil } -// getDeleteObjectResponse returns whether there was an error on deletion of object +// getDeleteObjectResponse returns whether there was an errors on deletion of object func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteObjectParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() var prefix string if params.Path != "" { encodedPrefix := SanitizeEncodedPrefix(params.Path) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } s3Client, err := newS3BucketClient(session, params.BucketName, prefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used @@ -596,19 +595,19 @@ func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteO if allVersions && nonCurrentVersions { err := errors.New("cannot set delete all versions and delete non-current versions flags at the same time") - return prepareError(err) + return ErrorWithContext(ctx, err) } err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } -// getDeleteMultiplePathsResponse returns whether there was an error on deletion of any object +// getDeleteMultiplePathsResponse returns whether there was an errors on deletion of any object func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi.DeleteMultipleObjectsParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() var version string var allVersions bool @@ -622,14 +621,14 @@ func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi. prefix := params.Files[i].Path s3Client, err := newS3BucketClient(session, params.BucketName, prefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used mcClient := mcClient{client: s3Client} err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } } return nil @@ -680,7 +679,7 @@ OUTER_LOOP: switch content.Err.ToGoError().(type) { // ignore same as mc case mc.PathInsufficientPermission: - // Ignore Permission error. + // Ignore Permission errors. continue } close(contentCh) @@ -696,7 +695,7 @@ OUTER_LOOP: switch result.Err.ToGoError().(type) { // ignore same as mc case mc.PathInsufficientPermission: - // Ignore Permission error. + // Ignore Permission errors. continue } close(contentCh) @@ -712,7 +711,7 @@ OUTER_LOOP: switch result.Err.ToGoError().(type) { // ignore same as mc case mc.PathInsufficientPermission: - // Ignore Permission error. + // Ignore Permission errors. continue } return result.Err.Cause @@ -738,7 +737,7 @@ func deleteSingleObject(ctx context.Context, client MCClient, bucket, object str switch result.Err.ToGoError().(type) { // ignore same as mc case mc.PathInsufficientPermission: - // Ignore Permission error. + // Ignore Permission errors. continue } return result.Err.Cause @@ -767,17 +766,16 @@ func deleteNonCurrentVersions(ctx context.Context, client MCClient, bucket, path } func getUploadObjectResponse(session *models.Principal, params objectApi.PostBucketsBucketNameObjectsUploadParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used minioClient := minioClient{client: mClient} if err := uploadFiles(ctx, minioClient, params); err != nil { - return prepareError(err, ErrorGeneric) + return ErrorWithContext(ctx, err, ErrDefault) } return nil } @@ -832,20 +830,19 @@ func uploadFiles(ctx context.Context, client MinioClient, params objectApi.PostB // getShareObjectResponse returns a share object url func getShareObjectResponse(session *models.Principal, params objectApi.ShareObjectParams) (*string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() var prefix string if params.Prefix != "" { encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } s3Client, err := newS3BucketClient(session, params.BucketName, prefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a mc S3Client interface implementation // defining the client to be used @@ -856,7 +853,7 @@ func getShareObjectResponse(session *models.Principal, params objectApi.ShareObj } url, err := getShareObjectURL(ctx, mcClient, params.VersionID, expireDuration) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return url, nil } @@ -879,11 +876,10 @@ func getShareObjectURL(ctx context.Context, client MCClient, versionID string, d } func getSetObjectLegalHoldResponse(session *models.Principal, params objectApi.PutObjectLegalHoldParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -893,13 +889,13 @@ func getSetObjectLegalHoldResponse(session *models.Principal, params objectApi.P encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } err = setObjectLegalHold(ctx, minioClient, params.BucketName, prefix, params.VersionID, *params.Body.Status) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -915,11 +911,10 @@ func setObjectLegalHold(ctx context.Context, client MinioClient, bucketName, pre } func getSetObjectRetentionResponse(session *models.Principal, params objectApi.PutObjectRetentionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -929,13 +924,13 @@ func getSetObjectRetentionResponse(session *models.Principal, params objectApi.P encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } err = setObjectRetention(ctx, minioClient, params.BucketName, params.VersionID, prefix, params.Body) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -968,11 +963,10 @@ func setObjectRetention(ctx context.Context, client MinioClient, bucketName, ver } func deleteObjectRetentionResponse(session *models.Principal, params objectApi.DeleteObjectRetentionParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -982,13 +976,13 @@ func deleteObjectRetentionResponse(session *models.Principal, params objectApi.D encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } err = deleteObjectRetention(ctx, minioClient, params.BucketName, prefix, params.VersionID) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -1003,11 +997,10 @@ func deleteObjectRetention(ctx context.Context, client MinioClient, bucketName, } func getPutObjectTagsResponse(session *models.Principal, params objectApi.PutObjectTagsParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -1017,13 +1010,13 @@ func getPutObjectTagsResponse(session *models.Principal, params objectApi.PutObj encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } err = putObjectTags(ctx, minioClient, params.BucketName, prefix, params.VersionID, params.Body.Tags) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -1041,11 +1034,10 @@ func putObjectTags(ctx context.Context, client MinioClient, bucketName, prefix, // Restore Object Version func getPutObjectRestoreResponse(session *models.Principal, params objectApi.PutObjectRestoreParams) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -1056,14 +1048,14 @@ func getPutObjectRestoreResponse(session *models.Principal, params objectApi.Put encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } err = restoreObject(ctx, minioClient, params.BucketName, prefix, params.VersionID) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -1098,11 +1090,10 @@ func restoreObject(ctx context.Context, client MinioClient, bucketName, prefix, // Metadata Response from minio-go API func getObjectMetadataResponse(session *models.Principal, params objectApi.GetObjectMetadataParams) (*models.Metadata, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := params.HTTPRequest.Context() mClient, err := newMinioClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a minioClient interface implementation // defining the client to be used @@ -1113,7 +1104,7 @@ func getObjectMetadataResponse(session *models.Principal, params objectApi.GetOb encodedPrefix := SanitizeEncodedPrefix(params.Prefix) decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } prefix = string(decodedPrefix) } @@ -1121,7 +1112,7 @@ func getObjectMetadataResponse(session *models.Principal, params objectApi.GetOb objectInfo, err := getObjectInfo(ctx, minioClient, params.BucketName, prefix) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } metadata := &models.Metadata{ObjectMetadata: objectInfo.Metadata} diff --git a/restapi/user_service_accounts.go b/restapi/user_service_accounts.go index ba294831b..78f810b7f 100644 --- a/restapi/user_service_accounts.go +++ b/restapi/user_service_accounts.go @@ -36,7 +36,7 @@ import ( func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { // Create Service Account api.ServiceAccountCreateServiceAccountHandler = saApi.CreateServiceAccountHandlerFunc(func(params saApi.CreateServiceAccountParams, session *models.Principal) middleware.Responder { - creds, err := getCreateServiceAccountResponse(session, params.Body) + creds, err := getCreateServiceAccountResponse(session, params) if err != nil { return saApi.NewCreateServiceAccountDefault(int(err.Code)).WithPayload(err) } @@ -44,7 +44,7 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { }) // Create User Service Account api.UserCreateAUserServiceAccountHandler = userApi.CreateAUserServiceAccountHandlerFunc(func(params userApi.CreateAUserServiceAccountParams, session *models.Principal) middleware.Responder { - creds, err := getCreateAUserServiceAccountResponse(session, params.Body, params.Name) + creds, err := getCreateAUserServiceAccountResponse(session, params) if err != nil { return saApi.NewCreateServiceAccountDefault(int(err.Code)).WithPayload(err) } @@ -52,14 +52,14 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { }) // Create User Service Account api.UserCreateServiceAccountCredentialsHandler = userApi.CreateServiceAccountCredentialsHandlerFunc(func(params userApi.CreateServiceAccountCredentialsParams, session *models.Principal) middleware.Responder { - creds, err := getCreateAUserServiceAccountCredsResponse(session, params.Body, params.Name) + creds, err := getCreateAUserServiceAccountCredsResponse(session, params) if err != nil { return saApi.NewCreateServiceAccountDefault(int(err.Code)).WithPayload(err) } return userApi.NewCreateServiceAccountCredentialsCreated().WithPayload(creds) }) api.ServiceAccountCreateServiceAccountCredsHandler = saApi.CreateServiceAccountCredsHandlerFunc(func(params saApi.CreateServiceAccountCredsParams, session *models.Principal) middleware.Responder { - creds, err := getCreateServiceAccountCredsResponse(session, params.Body) + creds, err := getCreateServiceAccountCredsResponse(session, params) if err != nil { return saApi.NewCreateServiceAccountDefault(int(err.Code)).WithPayload(err) } @@ -67,7 +67,9 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { }) // List Service Accounts for User api.ServiceAccountListUserServiceAccountsHandler = saApi.ListUserServiceAccountsHandlerFunc(func(params saApi.ListUserServiceAccountsParams, session *models.Principal) middleware.Responder { - serviceAccounts, err := getUserServiceAccountsResponse(session, "") + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + serviceAccounts, err := getUserServiceAccountsResponse(ctx, session, "") if err != nil { return saApi.NewListUserServiceAccountsDefault(int(err.Code)).WithPayload(err) } @@ -76,7 +78,7 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { // Delete a User's service account api.ServiceAccountDeleteServiceAccountHandler = saApi.DeleteServiceAccountHandlerFunc(func(params saApi.DeleteServiceAccountParams, session *models.Principal) middleware.Responder { - if err := getDeleteServiceAccountResponse(session, params.AccessKey); err != nil { + if err := getDeleteServiceAccountResponse(session, params); err != nil { return saApi.NewDeleteServiceAccountDefault(int(err.Code)).WithPayload(err) } return saApi.NewDeleteServiceAccountNoContent() @@ -84,7 +86,9 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { // List Service Accounts for User api.UserListAUserServiceAccountsHandler = userApi.ListAUserServiceAccountsHandlerFunc(func(params userApi.ListAUserServiceAccountsParams, session *models.Principal) middleware.Responder { - serviceAccounts, err := getUserServiceAccountsResponse(session, params.Name) + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + serviceAccounts, err := getUserServiceAccountsResponse(ctx, session, params.Name) if err != nil { return saApi.NewListUserServiceAccountsDefault(int(err.Code)).WithPayload(err) } @@ -92,7 +96,7 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { }) api.ServiceAccountGetServiceAccountPolicyHandler = saApi.GetServiceAccountPolicyHandlerFunc(func(params saApi.GetServiceAccountPolicyParams, session *models.Principal) middleware.Responder { - serviceAccounts, err := getServiceAccountPolicyResponse(session, params.AccessKey) + serviceAccounts, err := getServiceAccountPolicyResponse(session, params) if err != nil { return saApi.NewGetServiceAccountPolicyDefault(int(err.Code)).WithPayload(err) } @@ -100,7 +104,7 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { }) api.ServiceAccountSetServiceAccountPolicyHandler = saApi.SetServiceAccountPolicyHandlerFunc(func(params saApi.SetServiceAccountPolicyParams, session *models.Principal) middleware.Responder { - err := getSetServiceAccountPolicyResponse(session, params.AccessKey, *params.Policy.Policy) + err := getSetServiceAccountPolicyResponse(session, params) if err != nil { return saApi.NewSetServiceAccountPolicyDefault(int(err.Code)).WithPayload(err) } @@ -109,7 +113,7 @@ func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { // Delete multiple service accounts api.ServiceAccountDeleteMultipleServiceAccountsHandler = saApi.DeleteMultipleServiceAccountsHandlerFunc(func(params saApi.DeleteMultipleServiceAccountsParams, session *models.Principal) middleware.Responder { - if err := getDeleteMultipleServiceAccountsResponse(session, params.SelectedSA); err != nil { + if err := getDeleteMultipleServiceAccountsResponse(session, params); err != nil { return saApi.NewDeleteMultipleServiceAccountsDefault(int(err.Code)).WithPayload(err) } return saApi.NewDeleteMultipleServiceAccountsNoContent() @@ -157,21 +161,21 @@ func createServiceAccountCreds(ctx context.Context, userClient MinioAdmin, polic // getCreateServiceAccountResponse creates a service account with the defined policy for the user that // is requestingit ,it first gets the credentials of the user and creates a client which is going to // make the call to create the Service Account -func getCreateServiceAccountResponse(session *models.Principal, serviceAccount *models.ServiceAccountRequest) (*models.ServiceAccountCreds, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getCreateServiceAccountResponse(session *models.Principal, params saApi.CreateServiceAccountParams) (*models.ServiceAccountCreds, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() userAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} - saCreds, err := createServiceAccount(ctx, userAdminClient, serviceAccount.Policy) + saCreds, err := createServiceAccount(ctx, userAdminClient, params.Body.Policy) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return saCreds, nil } @@ -218,91 +222,88 @@ func createAUserServiceAccountCreds(ctx context.Context, userClient MinioAdmin, // getCreateServiceAccountResponse creates a service account with the defined policy for the user that // is requesting it ,it first gets the credentials of the user and creates a client which is going to // make the call to create the Service Account -func getCreateAUserServiceAccountResponse(session *models.Principal, serviceAccount *models.ServiceAccountRequest, user string) (*models.ServiceAccountCreds, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getCreateAUserServiceAccountResponse(session *models.Principal, params userApi.CreateAUserServiceAccountParams) (*models.ServiceAccountCreds, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() userAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} - saCreds, err := createAUserServiceAccount(ctx, userAdminClient, serviceAccount.Policy, user) + saCreds, err := createAUserServiceAccount(ctx, userAdminClient, params.Body.Policy, params.Name) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return saCreds, nil } // getCreateServiceAccountCredsResponse creates a service account with the defined policy for the user that // is requesting it, and with the credentials provided -func getCreateAUserServiceAccountCredsResponse(session *models.Principal, serviceAccount *models.ServiceAccountRequestCreds, user string) (*models.ServiceAccountCreds, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getCreateAUserServiceAccountCredsResponse(session *models.Principal, params userApi.CreateServiceAccountCredentialsParams) (*models.ServiceAccountCreds, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - userAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} - + serviceAccount := params.Body + user := params.Name if user == serviceAccount.AccessKey { - return nil, prepareError(errors.New("Access Key already in use")) + return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) } - accounts, err := userAdminClient.listServiceAccounts(ctx, user) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } - for i := 0; i < len(accounts.Accounts); i++ { if accounts.Accounts[i] == serviceAccount.AccessKey { - return nil, prepareError(errors.New("Access Key already in use")) + return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) } } - saCreds, err := createAUserServiceAccountCreds(ctx, userAdminClient, serviceAccount.Policy, user, serviceAccount.AccessKey, serviceAccount.SecretKey) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return saCreds, nil } -func getCreateServiceAccountCredsResponse(session *models.Principal, serviceAccount *models.ServiceAccountRequestCreds) (*models.ServiceAccountCreds, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getCreateServiceAccountCredsResponse(session *models.Principal, params saApi.CreateServiceAccountCredsParams) (*models.ServiceAccountCreds, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - + serviceAccount := params.Body userAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} if session.AccountAccessKey == serviceAccount.AccessKey { - return nil, prepareError(errors.New("Access Key already in use")) + return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) } accounts, err := userAdminClient.listServiceAccounts(ctx, "") if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } for i := 0; i < len(accounts.Accounts); i++ { if accounts.Accounts[i] == serviceAccount.AccessKey { - return nil, prepareError(errors.New("Access Key already in use")) + return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) } } saCreds, err := createServiceAccountCreds(ctx, userAdminClient, serviceAccount.Policy, serviceAccount.AccessKey, serviceAccount.SecretKey) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return saCreds, nil } @@ -322,13 +323,10 @@ func getUserServiceAccounts(ctx context.Context, userClient MinioAdmin, user str // getUserServiceAccountsResponse authenticates the user and calls // getUserServiceAccounts to list the user's service accounts -func getUserServiceAccountsResponse(session *models.Principal, user string) (models.ServiceAccounts, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - +func getUserServiceAccountsResponse(ctx context.Context, session *models.Principal, user string) (models.ServiceAccounts, *models.Error) { userAdmin, err := NewMinioAdminClient(session) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used @@ -336,7 +334,7 @@ func getUserServiceAccountsResponse(session *models.Principal, user string) (mod serviceAccounts, err := getUserServiceAccounts(ctx, userAdminClient, user) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return serviceAccounts, nil } @@ -347,20 +345,19 @@ func deleteServiceAccount(ctx context.Context, userClient MinioAdmin, accessKey } // getDeleteServiceAccountResponse authenticates the user and calls deleteServiceAccount -func getDeleteServiceAccountResponse(session *models.Principal, accessKey string) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getDeleteServiceAccountResponse(session *models.Principal, params saApi.DeleteServiceAccountParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - + accessKey := params.AccessKey userAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} - if err := deleteServiceAccount(ctx, userAdminClient, accessKey); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } @@ -381,13 +378,13 @@ func getServiceAccountPolicy(ctx context.Context, userClient MinioAdmin, accessK // getServiceAccountPolicyResponse authenticates the user and calls // getServiceAccountPolicy to get the policy for a service account -func getServiceAccountPolicyResponse(session *models.Principal, accessKey string) (string, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getServiceAccountPolicyResponse(session *models.Principal, params saApi.GetServiceAccountPolicyParams) (string, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - + accessKey := params.AccessKey userAdmin, err := NewMinioAdminClient(session) if err != nil { - return "", prepareError(err) + return "", ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used @@ -395,7 +392,7 @@ func getServiceAccountPolicyResponse(session *models.Principal, accessKey string serviceAccounts, err := getServiceAccountPolicy(ctx, userAdminClient, accessKey) if err != nil { - return "", prepareError(err) + return "", ErrorWithContext(ctx, err) } return serviceAccounts, nil } @@ -408,13 +405,14 @@ func setServiceAccountPolicy(ctx context.Context, userClient MinioAdmin, accessK // getSetServiceAccountPolicyResponse authenticates the user and calls // getSetServiceAccountPolicy to set the policy for a service account -func getSetServiceAccountPolicyResponse(session *models.Principal, accessKey string, policy string) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getSetServiceAccountPolicyResponse(session *models.Principal, params saApi.SetServiceAccountPolicyParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() - + accessKey := params.AccessKey + policy := *params.Policy.Policy userAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used @@ -422,25 +420,26 @@ func getSetServiceAccountPolicyResponse(session *models.Principal, accessKey str err = setServiceAccountPolicy(ctx, userAdminClient, accessKey, policy) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } return nil } // getDeleteMultipleServiceAccountsResponse authenticates the user and calls deleteServiceAccount for each account listed in selectedSAs -func getDeleteMultipleServiceAccountsResponse(session *models.Principal, selectedSAs []string) *models.Error { - ctx, cancel := context.WithCancel(context.Background()) +func getDeleteMultipleServiceAccountsResponse(session *models.Principal, params saApi.DeleteMultipleServiceAccountsParams) *models.Error { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() + selectedSAs := params.SelectedSA userAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } // create a MinIO user Admin Client interface implementation // defining the client to be used userAdminClient := AdminClient{Client: userAdmin} for _, sa := range selectedSAs { if err := deleteServiceAccount(ctx, userAdminClient, sa); err != nil { - return prepareError(err) + return ErrorWithContext(ctx, err) } } return nil diff --git a/restapi/user_session.go b/restapi/user_session.go index 0dafa5569..a5cd20eac 100644 --- a/restapi/user_session.go +++ b/restapi/user_session.go @@ -68,7 +68,7 @@ func isErasureMode() bool { func registerSessionHandlers(api *operations.ConsoleAPI) { // session check api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder { - sessionResp, err := getSessionResponse(session) + sessionResp, err := getSessionResponse(params.HTTPRequest.Context(), session) if err != nil { return authApi.NewSessionCheckDefault(int(err.Code)).WithPayload(err) } @@ -91,12 +91,12 @@ func getClaimsFromToken(sessionToken string) (map[string]interface{}, error) { } // getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI -func getSessionResponse(session *models.Principal) (*models.SessionResponse, *models.Error) { - ctx, cancel := context.WithCancel(context.Background()) +func getSessionResponse(ctx context.Context, session *models.Principal) (*models.SessionResponse, *models.Error) { + ctx, cancel := context.WithCancel(ctx) defer cancel() // serialize output if session == nil { - return nil, prepareError(errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, ErrInvalidSession) } tokenClaims, _ := getClaimsFromToken(session.STSSessionToken) @@ -107,20 +107,20 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo STSSessionToken: session.STSSessionToken, }) if err != nil { - return nil, prepareError(err, errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, err, ErrInvalidSession) } userAdminClient := AdminClient{Client: mAdminClient} // Obtain the current policy assigned to this user // necessary for generating the list of allowed endpoints accountInfo, err := getAccountInfo(ctx, userAdminClient) if err != nil { - return nil, prepareError(err, errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, err, ErrInvalidSession) } rawPolicy := policies.ReplacePolicyVariables(tokenClaims, accountInfo) policy, err := minioIAMPolicy.ParseConfig(bytes.NewReader(rawPolicy)) if err != nil { - return nil, prepareError(err, errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, err, ErrInvalidSession) } currTime := time.Now().UTC() @@ -143,7 +143,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo claims, err := getClaimsFromToken(session.STSSessionToken) if err != nil { - return nil, prepareError(err, errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, err, ErrInvalidSession) } // Support all LDAP, JWT variables @@ -222,12 +222,12 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo } serializedPolicy, err := json.Marshal(policy) if err != nil { - return nil, prepareError(err, errorGenericInvalidSession) + return nil, ErrorWithContext(ctx, err, ErrInvalidSession) } var sessionPolicy *models.IamPolicy err = json.Unmarshal(serializedPolicy, &sessionPolicy) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } sessionResp := &models.SessionResponse{ Features: getListOfEnabledFeatures(session), diff --git a/restapi/user_session_test.go b/restapi/user_session_test.go new file mode 100644 index 000000000..f9ba3396c --- /dev/null +++ b/restapi/user_session_test.go @@ -0,0 +1,159 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package restapi + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/minio/console/models" + "github.com/minio/console/pkg/auth/idp/oauth2" + "github.com/minio/console/pkg/auth/ldap" + "github.com/stretchr/testify/assert" +) + +func Test_getSessionResponse(t *testing.T) { + type args struct { + ctx context.Context + session *models.Principal + } + ctx := context.Background() + tests := []struct { + name string + args args + want *models.SessionResponse + wantErr bool + preFunc func() + postFunc func() + }{ + { + name: "empty session", + args: args{ + ctx: ctx, + session: nil, + }, + want: nil, + wantErr: true, + }, + { + name: "malformed minio endpoint URL", + args: args{ + ctx: ctx, + session: &models.Principal{ + STSAccessKeyID: "", + STSSecretAccessKey: "", + STSSessionToken: "", + AccountAccessKey: "", + Hm: false, + }, + }, + want: nil, + wantErr: true, + preFunc: func() { + os.Setenv(ConsoleMinIOServer, "malformed") + }, + postFunc: func() { + os.Unsetenv(ConsoleMinIOServer) + }, + }, + { + name: "malformed session", + args: args{ + ctx: ctx, + session: &models.Principal{ + STSAccessKeyID: "W257A03HTI7L30F7YCRD", + STSSecretAccessKey: "g+QVorWQR8aSy+k3OHOoYn0qKpENld72faCMfYps", + STSSessionToken: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJXMjU3QTAzSFRJN0wzMEY3WUNSRCIsImV4cCI6MTY1MTAxNjU1OCwicGFyZW50IjoibWluaW8ifQ.uFFIIEQ6qM_QvMM297ODi_uK2IA1pwvsDbyBGErkQKqtbY_Ynte8GUkNsSHBEMCT9Fr7uUwaxK41kUqjtbqAwA", + AccountAccessKey: "minio", + Hm: false, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc() + } + session, err := getSessionResponse(tt.args.ctx, tt.args.session) + if (err != nil) != tt.wantErr { + t.Errorf("getSessionResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(session, tt.want) { + t.Errorf("getSessionResponse() got = %v, want %v", session, tt.want) + } + if tt.postFunc != nil { + tt.postFunc() + } + }) + } +} + +func Test_getListOfEnabledFeatures(t *testing.T) { + type args struct { + session *models.Principal + } + tests := []struct { + name string + args args + want []string + preFunc func() + postFunc func() + }{ + { + name: "all features are enabled", + args: args{ + session: &models.Principal{ + STSAccessKeyID: "", + STSSecretAccessKey: "", + STSSessionToken: "", + AccountAccessKey: "", + Hm: true, + }, + }, + want: []string{"log-search", "oidc-idp", "external-idp", "ldap-idp", "external-idp", "hide-menu"}, + preFunc: func() { + os.Setenv(ConsoleLogQueryURL, "http://logsearchapi:8080") + os.Setenv(oauth2.ConsoleIDPURL, "http://external-idp.com") + os.Setenv(oauth2.ConsoleIDPClientID, "eaeaeaeaeaea") + os.Setenv(ldap.ConsoleLDAPEnabled, "on") + }, + postFunc: func() { + os.Unsetenv(ConsoleLogQueryURL) + os.Unsetenv(oauth2.ConsoleIDPURL) + os.Unsetenv(oauth2.ConsoleIDPClientID) + os.Unsetenv(ldap.ConsoleLDAPEnabled) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc() + } + assert.Equalf(t, tt.want, getListOfEnabledFeatures(tt.args.session), "getListOfEnabledFeatures(%v)", tt.args.session) + if tt.postFunc != nil { + tt.postFunc() + } + }) + } +} diff --git a/restapi/user_version.go b/restapi/user_version.go index 4f3ba9a12..093e1c2a2 100644 --- a/restapi/user_version.go +++ b/restapi/user_version.go @@ -17,9 +17,12 @@ package restapi import ( + "context" "net/http" "time" + xhttp "github.com/minio/console/pkg/http" + "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" "github.com/minio/console/pkg/utils" @@ -29,7 +32,7 @@ import ( func registerVersionHandlers(api *operations.ConsoleAPI) { api.SystemCheckMinIOVersionHandler = systemApi.CheckMinIOVersionHandlerFunc(func(params systemApi.CheckMinIOVersionParams) middleware.Responder { - versionResponse, err := getVersionResponse() + versionResponse, err := getVersionResponse(params) if err != nil { return systemApi.NewCheckMinIOVersionDefault(int(err.Code)).WithPayload(err) } @@ -38,13 +41,15 @@ func registerVersionHandlers(api *operations.ConsoleAPI) { } // getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI -func getVersionResponse() (*models.CheckVersionResponse, *models.Error) { - ver, err := utils.GetLatestMinIOImage(&utils.HTTPClient{ +func getVersionResponse(params systemApi.CheckMinIOVersionParams) (*models.CheckVersionResponse, *models.Error) { + ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) + defer cancel() + ver, err := utils.GetLatestMinIOImage(&xhttp.Client{ Client: &http.Client{ Timeout: 15 * time.Second, }}) if err != nil { - return nil, prepareError(err) + return nil, ErrorWithContext(ctx, err) } return &models.CheckVersionResponse{ LatestVersion: *ver, diff --git a/restapi/user_watch.go b/restapi/user_watch.go index a92902b31..b09e2d2dc 100644 --- a/restapi/user_watch.go +++ b/restapi/user_watch.go @@ -51,9 +51,13 @@ func startWatch(ctx context.Context, conn WSConn, wsc MCClient, options *watchOp } for _, event := range events { // Serialize message to be sent - bytes, _ := json.Marshal(event) + bytes, err := json.Marshal(event) + if err != nil { + LogError("error on json.Marshal: %v", err) + return err + } // Send Message through websocket connection - err := conn.writeMessage(websocket.TextMessage, bytes) + err = conn.writeMessage(websocket.TextMessage, bytes) if err != nil { LogError("error writeMessage: %v", err) return err diff --git a/restapi/utils_test.go b/restapi/utils_test.go index 897f42cae..28046b8d9 100644 --- a/restapi/utils_test.go +++ b/restapi/utils_test.go @@ -17,7 +17,9 @@ package restapi import ( + "net/http" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -62,3 +64,153 @@ func TestUniqueKeys(t *testing.T) { responseArray := UniqueKeys(exampleMixedArray) assert.ElementsMatchf(responseArray, exampleUniqueArray, "returned array doesn't contain the correct elements %s") } + +func TestRandomCharStringWithAlphabet(t *testing.T) { + type args struct { + n int + alphabet string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "generated random string has the right length", + args: args{ + n: 10, + alphabet: "A", + }, + want: "AAAAAAAAAA", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, RandomCharStringWithAlphabet(tt.args.n, tt.args.alphabet), "RandomCharStringWithAlphabet(%v, %v)", tt.args.n, tt.args.alphabet) + }) + } +} + +func TestNewSessionCookieForConsole(t *testing.T) { + type args struct { + token string + } + tests := []struct { + name string + args args + want http.Cookie + }{ + { + name: "session cookie has the right token an security configuration", + args: args{ + token: "jwt-xxxxxxxxx", + }, + want: http.Cookie{ + Path: "/", + Value: "jwt-xxxxxxxxx", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Name: "token", + MaxAge: 3600, + Expires: time.Now().Add(1 * time.Hour), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewSessionCookieForConsole(tt.args.token) + assert.Equalf(t, tt.want.Value, got.Value, "NewSessionCookieForConsole(%v)", tt.args.token) + assert.Equalf(t, tt.want.Path, got.Path, "NewSessionCookieForConsole(%v)", tt.args.token) + assert.Equalf(t, tt.want.HttpOnly, got.HttpOnly, "NewSessionCookieForConsole(%v)", tt.args.token) + assert.Equalf(t, tt.want.Name, got.Name, "NewSessionCookieForConsole(%v)", tt.args.token) + assert.Equalf(t, tt.want.MaxAge, got.MaxAge, "NewSessionCookieForConsole(%v)", tt.args.token) + assert.Equalf(t, tt.want.SameSite, got.SameSite, "NewSessionCookieForConsole(%v)", tt.args.token) + }) + } +} + +func TestExpireSessionCookie(t *testing.T) { + tests := []struct { + name string + want http.Cookie + }{ + { + name: "cookie is expired correctly", + want: http.Cookie{ + Name: "token", + Value: "", + MaxAge: -1, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ExpireSessionCookie() + assert.Equalf(t, tt.want.Name, got.Name, "ExpireSessionCookie()") + assert.Equalf(t, tt.want.Value, got.Value, "ExpireSessionCookie()") + assert.Equalf(t, tt.want.MaxAge, got.MaxAge, "ExpireSessionCookie()") + }) + } +} + +func TestSanitizeEncodedPrefix(t *testing.T) { + type args struct { + rawPrefix string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "replace spaces with +", + args: args{ + rawPrefix: "hello world", + }, + want: "hello+world", + }, + { + name: "replace spaces with +", + args: args{ + rawPrefix: " hello-world ", + }, + want: "+++hello-world+++", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, SanitizeEncodedPrefix(tt.args.rawPrefix), "SanitizeEncodedPrefix(%v)", tt.args.rawPrefix) + }) + } +} + +func Test_isSafeToPreview(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "mime type is safe to preview", + args: args{ + str: "image/jpeg", + }, + want: true, + }, + { + name: "mime type is not safe to preview", + args: args{ + str: "application/zip", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, isSafeToPreview(tt.args.str), "isSafeToPreview(%v)", tt.args.str) + }) + } +} diff --git a/restapi/ws_handle.go b/restapi/ws_handle.go index b6fd5a62a..af7baa13d 100644 --- a/restapi/ws_handle.go +++ b/restapi/ws_handle.go @@ -18,12 +18,15 @@ package restapi import ( "context" + "fmt" "net" "net/http" "strconv" "strings" "time" + "github.com/minio/console/pkg/utils" + "github.com/go-openapi/errors" "github.com/gorilla/websocket" "github.com/minio/console/models" @@ -121,17 +124,25 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) { // on the path. // Request should come like ws://:/ws/ func serveWS(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() // Perform authentication before upgrading to a Websocket Connection // authenticate WS connection with Console session, err := auth.GetClaimsFromTokenInRequest(req) if err != nil { + ErrorWithContext(ctx, err) errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error())) return } + //// DELETE ME !!! + //upgrader.CheckOrigin = func(r *http.Request) bool { + // return true + //} + // upgrades the HTTP server connection to the WebSocket protocol. conn, err := upgrader.Upgrade(w, req, nil) if err != nil { + ErrorWithContext(ctx, err) errors.ServeError(w, req, err) return } @@ -141,6 +152,7 @@ func serveWS(w http.ResponseWriter, req *http.Request) { case strings.HasPrefix(wsPath, `/trace`): wsAdminClient, err := newWebSocketAdminClient(conn, session) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } @@ -172,11 +184,12 @@ func serveWS(w http.ResponseWriter, req *http.Request) { path: path, } - go wsAdminClient.trace(traceRequestItem) + go wsAdminClient.trace(ctx, traceRequestItem) case strings.HasPrefix(wsPath, `/console`): wsAdminClient, err := newWebSocketAdminClient(conn, session) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } @@ -187,60 +200,64 @@ func serveWS(w http.ResponseWriter, req *http.Request) { node: node, logType: logType, } - go wsAdminClient.console(logRequestItem) + go wsAdminClient.console(ctx, logRequestItem) case strings.HasPrefix(wsPath, `/health-info`): deadline, err := getHealthInfoOptionsFromReq(req) if err != nil { - LogError("error getting health info options: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error getting health info options: %v", err)) closeWsConn(conn) return } wsAdminClient, err := newWebSocketAdminClient(conn, session) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } - go wsAdminClient.healthInfo(deadline) + go wsAdminClient.healthInfo(ctx, deadline) case strings.HasPrefix(wsPath, `/heal`): hOptions, err := getHealOptionsFromReq(req) if err != nil { - LogError("error getting heal options: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error getting heal options: %v", err)) closeWsConn(conn) return } wsAdminClient, err := newWebSocketAdminClient(conn, session) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } - go wsAdminClient.heal(hOptions) + go wsAdminClient.heal(ctx, hOptions) case strings.HasPrefix(wsPath, `/watch`): wOptions, err := getWatchOptionsFromReq(req) if err != nil { - LogError("error getting watch options: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error getting watch options: %v", err)) closeWsConn(conn) return } wsS3Client, err := newWebSocketS3Client(conn, session, wOptions.BucketName) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } - go wsS3Client.watch(wOptions) + go wsS3Client.watch(ctx, wOptions) case strings.HasPrefix(wsPath, `/speedtest`): speedtestOpts, err := getSpeedtestOptionsFromReq(req) if err != nil { - LogError("error getting speedtest options: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error getting speedtest options: %v", err)) closeWsConn(conn) return } wsAdminClient, err := newWebSocketAdminClient(conn, session) if err != nil { + ErrorWithContext(ctx, err) closeWsConn(conn) return } - go wsAdminClient.speedtest(speedtestOpts) + go wsAdminClient.speedtest(ctx, speedtestOpts) default: // path not found @@ -292,26 +309,54 @@ func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, bucket // if the client sends a Close Message the context will be // canceled. If the connection is closed the goroutine inside // will return. -func wsReadClientCtx(conn WSConn) context.Context { +func wsReadClientCtx(parentContext context.Context, conn WSConn) context.Context { // a cancel context is needed to end all goroutines used ctx, cancel := context.WithCancel(context.Background()) + + var requestID string + var SessionID string + var UserAgent string + var Host string + var RemoteHost string + + if val, o := parentContext.Value(utils.ContextRequestID).(string); o { + requestID = val + } + if val, o := parentContext.Value(utils.ContextRequestUserID).(string); o { + SessionID = val + } + if val, o := parentContext.Value(utils.ContextRequestUserAgent).(string); o { + UserAgent = val + } + if val, o := parentContext.Value(utils.ContextRequestHost).(string); o { + Host = val + } + if val, o := parentContext.Value(utils.ContextRequestRemoteAddr).(string); o { + RemoteHost = val + } + + ctx = context.WithValue(ctx, utils.ContextRequestID, requestID) + ctx = context.WithValue(ctx, utils.ContextRequestUserID, SessionID) + ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, UserAgent) + ctx = context.WithValue(ctx, utils.ContextRequestHost, Host) + ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, RemoteHost) + go func() { defer cancel() for { _, _, err := conn.readMessage() if err != nil { - // if error of type websocket.CloseError and is Unexpected + // if errors of type websocket.CloseError and is Unexpected if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { - LogError("error unexpected CloseError on ReadMessage: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error unexpected CloseError on ReadMessage: %v", err)) return } // Not all errors are of type websocket.CloseError. if _, ok := err.(*websocket.CloseError); !ok { - LogError("error on ReadMessage: %v", err) + ErrorWithContext(ctx, fmt.Errorf("error on ReadMessage: %v", err)) return } // else is an expected Close Error - LogError("closed conn.ReadMessage: %v", err) return } } @@ -327,7 +372,7 @@ func closeWsConn(conn *websocket.Conn) { // trace serves madmin.ServiceTraceInfo // on a Websocket connection. -func (wsc *wsAdminClient) trace(traceRequestItem TraceRequest) { +func (wsc *wsAdminClient) trace(ctx context.Context, traceRequestItem TraceRequest) { defer func() { LogInfo("trace stopped") // close connection after return @@ -335,7 +380,7 @@ func (wsc *wsAdminClient) trace(traceRequestItem TraceRequest) { }() LogInfo("trace started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startTraceInfo(ctx, wsc.conn, wsc.client, traceRequestItem) @@ -344,7 +389,7 @@ func (wsc *wsAdminClient) trace(traceRequestItem TraceRequest) { // console serves madmin.GetLogs // on a Websocket connection. -func (wsc *wsAdminClient) console(logRequestItem LogRequest) { +func (wsc *wsAdminClient) console(ctx context.Context, logRequestItem LogRequest) { defer func() { LogInfo("console logs stopped") // close connection after return @@ -352,14 +397,14 @@ func (wsc *wsAdminClient) console(logRequestItem LogRequest) { }() LogInfo("console logs started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startConsoleLog(ctx, wsc.conn, wsc.client, logRequestItem) sendWsCloseMessage(wsc.conn, err) } -func (wsc *wsS3Client) watch(params *watchOptions) { +func (wsc *wsS3Client) watch(ctx context.Context, params *watchOptions) { defer func() { LogInfo("watch stopped") // close connection after return @@ -367,14 +412,14 @@ func (wsc *wsS3Client) watch(params *watchOptions) { }() LogInfo("watch started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startWatch(ctx, wsc.conn, wsc.client, params) sendWsCloseMessage(wsc.conn, err) } -func (wsc *wsAdminClient) heal(opts *healOptions) { +func (wsc *wsAdminClient) heal(ctx context.Context, opts *healOptions) { defer func() { LogInfo("heal stopped") // close connection after return @@ -382,14 +427,14 @@ func (wsc *wsAdminClient) heal(opts *healOptions) { }() LogInfo("heal started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startHeal(ctx, wsc.conn, wsc.client, opts) sendWsCloseMessage(wsc.conn, err) } -func (wsc *wsAdminClient) healthInfo(deadline *time.Duration) { +func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duration) { defer func() { LogInfo("health info stopped") // close connection after return @@ -397,14 +442,14 @@ func (wsc *wsAdminClient) healthInfo(deadline *time.Duration) { }() LogInfo("health info started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline) sendWsCloseMessage(wsc.conn, err) } -func (wsc *wsAdminClient) speedtest(opts *madmin.SpeedtestOpts) { +func (wsc *wsAdminClient) speedtest(ctx context.Context, opts *madmin.SpeedtestOpts) { defer func() { LogInfo("speedtest stopped") // close connection after return @@ -412,7 +457,7 @@ func (wsc *wsAdminClient) speedtest(opts *madmin.SpeedtestOpts) { }() LogInfo("speedtest started") - ctx := wsReadClientCtx(wsc.conn) + ctx = wsReadClientCtx(ctx, wsc.conn) err := startSpeedtest(ctx, wsc.conn, wsc.client, opts) @@ -434,7 +479,7 @@ func sendWsCloseMessage(conn WSConn, err error) { return } // else, internal server error - conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, ErrorGeneric.Error())) + conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, ErrDefault.Error())) return } // normal closure diff --git a/restapi/ws_handle_test.go b/restapi/ws_handle_test.go index 595e85e08..9fcbe3726 100644 --- a/restapi/ws_handle_test.go +++ b/restapi/ws_handle_test.go @@ -17,6 +17,7 @@ package restapi import ( + "context" "errors" "testing" @@ -50,7 +51,8 @@ func TestWSHandle(t *testing.T) { connReadMessageMock = func() (messageType int, p []byte, err error) { return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""} } - ctx := wsReadClientCtx(mockWSConn) + parentCtx := context.Background() + ctx := wsReadClientCtx(parentCtx, mockWSConn) <-ctx.Done() // closed ctx correctly @@ -59,7 +61,7 @@ func TestWSHandle(t *testing.T) { connReadMessageMock = func() (messageType int, p []byte, err error) { return 0, []byte{}, errors.New("error") } - ctx2 := wsReadClientCtx(mockWSConn) + ctx2 := wsReadClientCtx(parentCtx, mockWSConn) <-ctx2.Done() // closed ctx correctly @@ -67,7 +69,7 @@ func TestWSHandle(t *testing.T) { connReadMessageMock = func() (messageType int, p []byte, err error) { return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""} } - ctx3 := wsReadClientCtx(mockWSConn) + ctx3 := wsReadClientCtx(parentCtx, mockWSConn) <-ctx3.Done() // closed ctx correctly }