github.com/minio/console@v1.3.0/api/configure_console.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // This file is safe to edit. Once it exists it will not be overwritten
    18  
    19  package api
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"crypto/tls"
    25  	"fmt"
    26  	"io"
    27  	"io/fs"
    28  	"log"
    29  	"net"
    30  	"net/http"
    31  	"path"
    32  	"path/filepath"
    33  	"regexp"
    34  	"strings"
    35  	"sync"
    36  	"time"
    37  
    38  	"github.com/google/uuid"
    39  
    40  	"github.com/minio/console/pkg/logger"
    41  	"github.com/minio/console/pkg/utils"
    42  	"github.com/minio/minio-go/v7/pkg/credentials"
    43  
    44  	"github.com/klauspost/compress/gzhttp"
    45  
    46  	portal_ui "github.com/minio/console/web-app"
    47  	"github.com/minio/pkg/v2/env"
    48  	"github.com/minio/pkg/v2/mimedb"
    49  	xnet "github.com/minio/pkg/v2/net"
    50  
    51  	"github.com/go-openapi/errors"
    52  	"github.com/go-openapi/swag"
    53  	"github.com/minio/console/api/operations"
    54  	"github.com/minio/console/models"
    55  	"github.com/minio/console/pkg/auth"
    56  	"github.com/unrolled/secure"
    57  )
    58  
    59  //go:generate swagger generate server --target ../../console --name Console --spec ../swagger.yml
    60  
    61  var additionalServerFlags = struct {
    62  	CertsDir string `long:"certs-dir" description:"path to certs directory" env:"CONSOLE_CERTS_DIR"`
    63  }{}
    64  
    65  const (
    66  	SubPath = "CONSOLE_SUBPATH"
    67  )
    68  
    69  var (
    70  	cfgSubPath  = "/"
    71  	subPathOnce sync.Once
    72  )
    73  
    74  func configureFlags(api *operations.ConsoleAPI) {
    75  	api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{
    76  		{
    77  			ShortDescription: "additional server flags",
    78  			Options:          &additionalServerFlags,
    79  		},
    80  	}
    81  }
    82  
    83  func configureAPI(api *operations.ConsoleAPI) http.Handler {
    84  	// Applies when the "x-token" header is set
    85  	api.KeyAuth = func(token string, _ []string) (*models.Principal, error) {
    86  		// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
    87  		// was generated and signed by us in the first place
    88  		if token == "Anonymous" {
    89  			return &models.Principal{}, nil
    90  		}
    91  		claims, err := auth.ParseClaimsFromToken(token)
    92  		if err != nil {
    93  			api.Logger("Unable to validate the session token %s: %v", token, err)
    94  			return nil, errors.New(401, "incorrect api key auth")
    95  		}
    96  		return &models.Principal{
    97  			STSAccessKeyID:     claims.STSAccessKeyID,
    98  			STSSecretAccessKey: claims.STSSecretAccessKey,
    99  			STSSessionToken:    claims.STSSessionToken,
   100  			AccountAccessKey:   claims.AccountAccessKey,
   101  			Hm:                 claims.HideMenu,
   102  			Ob:                 claims.ObjectBrowser,
   103  			CustomStyleOb:      claims.CustomStyleOB,
   104  		}, nil
   105  	}
   106  	api.AnonymousAuth = func(_ string) (*models.Principal, error) {
   107  		return &models.Principal{}, nil
   108  	}
   109  
   110  	// Register login handlers
   111  	registerLoginHandlers(api)
   112  	// Register logout handlers
   113  	registerLogoutHandlers(api)
   114  	// Register bucket handlers
   115  	registerBucketsHandlers(api)
   116  	// Register all users handlers
   117  	registerUsersHandlers(api)
   118  	// Register groups handlers
   119  	registerGroupsHandlers(api)
   120  	// Register policies handlers
   121  	registersPoliciesHandler(api)
   122  	// Register configurations handlers
   123  	registerConfigHandlers(api)
   124  	// Register bucket events handlers
   125  	registerBucketEventsHandlers(api)
   126  	// Register bucket lifecycle handlers
   127  	registerBucketsLifecycleHandlers(api)
   128  	// Register service handlers
   129  	registerServiceHandlers(api)
   130  	// Register session handlers
   131  	registerSessionHandlers(api)
   132  	// Register admin info handlers
   133  	registerAdminInfoHandlers(api)
   134  	// Register admin arns handlers
   135  	registerAdminArnsHandlers(api)
   136  	// Register admin notification endpoints handlers
   137  	registerAdminNotificationEndpointsHandlers(api)
   138  	// Register admin Service Account Handlers
   139  	registerServiceAccountsHandlers(api)
   140  	// Register admin remote buckets
   141  	registerAdminBucketRemoteHandlers(api)
   142  	// Register admin log search
   143  	registerLogSearchHandlers(api)
   144  	// Register admin subnet handlers
   145  	registerSubnetHandlers(api)
   146  	// Register admin KMS handlers
   147  	registerKMSHandlers(api)
   148  	// Register admin IDP handlers
   149  	registerIDPHandlers(api)
   150  	// Register Account handlers
   151  	registerAdminTiersHandlers(api)
   152  	// Register Inspect Handler
   153  	registerInspectHandler(api)
   154  	// Register nodes handlers
   155  	registerNodesHandler(api)
   156  
   157  	registerSiteReplicationHandler(api)
   158  	registerSiteReplicationStatusHandler(api)
   159  	// Register Support Handler
   160  	registerSupportHandlers(api)
   161  
   162  	// Operator Console
   163  
   164  	// Register Object's Handlers
   165  	registerObjectsHandlers(api)
   166  	// Register Bucket Quota's Handlers
   167  	registerBucketQuotaHandlers(api)
   168  	// Register Account handlers
   169  	registerAccountHandlers(api)
   170  
   171  	registerReleasesHandlers(api)
   172  
   173  	registerPublicObjectsHandlers(api)
   174  
   175  	api.PreServerShutdown = func() {}
   176  
   177  	api.ServerShutdown = func() {}
   178  
   179  	// do an initial subnet plan caching
   180  	fetchLicensePlan()
   181  
   182  	return setupGlobalMiddleware(api.Serve(setupMiddlewares))
   183  }
   184  
   185  // The TLS configuration before HTTPS server starts.
   186  func configureTLS(tlsConfig *tls.Config) {
   187  	tlsConfig.RootCAs = GlobalRootCAs
   188  	tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate
   189  }
   190  
   191  // The middleware configuration is for the handler executors. These do not apply to the swagger.json document.
   192  // The middleware executes after routing but before authentication, binding and validation
   193  func setupMiddlewares(handler http.Handler) http.Handler {
   194  	return handler
   195  }
   196  
   197  func ContextMiddleware(next http.Handler) http.Handler {
   198  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   199  		requestID := uuid.NewString()
   200  		ctx := context.WithValue(r.Context(), utils.ContextRequestID, requestID)
   201  		ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, r.UserAgent())
   202  		ctx = context.WithValue(ctx, utils.ContextRequestHost, r.Host)
   203  		ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, r.RemoteAddr)
   204  		ctx = context.WithValue(ctx, utils.ContextClientIP, getClientIP(r))
   205  		next.ServeHTTP(w, r.WithContext(ctx))
   206  	})
   207  }
   208  
   209  func AuditLogMiddleware(next http.Handler) http.Handler {
   210  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   211  		rw := logger.NewResponseWriter(w)
   212  		next.ServeHTTP(rw, r)
   213  		if strings.HasPrefix(r.URL.Path, "/ws") || strings.HasPrefix(r.URL.Path, "/api") {
   214  			logger.AuditLog(r.Context(), rw, r, map[string]interface{}{}, "Authorization", "Cookie", "Set-Cookie")
   215  		}
   216  	})
   217  }
   218  
   219  // The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
   220  // So this is a good place to plug in a panic handling middleware, logger and metrics
   221  func setupGlobalMiddleware(handler http.Handler) http.Handler {
   222  	gnext := gzhttp.GzipHandler(handler)
   223  	// if audit-log is enabled console will log all incoming request
   224  	next := AuditLogMiddleware(gnext)
   225  	// serve static files
   226  	next = FileServerMiddleware(next)
   227  	// add information to request context
   228  	next = ContextMiddleware(next)
   229  	// handle cookie or authorization header for session
   230  	next = AuthenticationMiddleware(next)
   231  
   232  	sslHostFn := secure.SSLHostFunc(func(host string) string {
   233  		xhost, err := xnet.ParseHost(host)
   234  		if err != nil {
   235  			return host
   236  		}
   237  		return net.JoinHostPort(xhost.Name, TLSPort)
   238  	})
   239  
   240  	// Secure middleware, this middleware wrap all the previous handlers and add
   241  	// HTTP security headers
   242  	secureOptions := secure.Options{
   243  		AllowedHosts:                    GetSecureAllowedHosts(),
   244  		AllowedHostsAreRegex:            GetSecureAllowedHostsAreRegex(),
   245  		HostsProxyHeaders:               GetSecureHostsProxyHeaders(),
   246  		SSLRedirect:                     GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0,
   247  		SSLHostFunc:                     &sslHostFn,
   248  		SSLHost:                         GetSecureTLSHost(),
   249  		STSSeconds:                      GetSecureSTSSeconds(),
   250  		STSIncludeSubdomains:            GetSecureSTSIncludeSubdomains(),
   251  		STSPreload:                      GetSecureSTSPreload(),
   252  		SSLTemporaryRedirect:            false,
   253  		ForceSTSHeader:                  GetSecureForceSTSHeader(),
   254  		FrameDeny:                       GetSecureFrameDeny(),
   255  		ContentTypeNosniff:              GetSecureContentTypeNonSniff(),
   256  		BrowserXssFilter:                GetSecureBrowserXSSFilter(),
   257  		ContentSecurityPolicy:           GetSecureContentSecurityPolicy(),
   258  		ContentSecurityPolicyReportOnly: GetSecureContentSecurityPolicyReportOnly(),
   259  		ReferrerPolicy:                  GetSecureReferrerPolicy(),
   260  		FeaturePolicy:                   GetSecureFeaturePolicy(),
   261  		IsDevelopment:                   false,
   262  	}
   263  	secureMiddleware := secure.New(secureOptions)
   264  	next = secureMiddleware.Handler(next)
   265  	return RejectS3Middleware(next)
   266  }
   267  
   268  const apiRequestErr = `<?xml version="1.0" encoding="UTF-8"?><Error><Code>InvalidArgument</Code><Message>S3 API Requests must be made to API port.</Message><RequestId>0</RequestId></Error>`
   269  
   270  // RejectS3Middleware will reject requests that have AWS S3 specific headers.
   271  func RejectS3Middleware(next http.Handler) http.Handler {
   272  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   273  		if len(r.Header.Get("X-Amz-Content-Sha256")) > 0 ||
   274  			len(r.Header.Get("X-Amz-Date")) > 0 ||
   275  			strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256") ||
   276  			r.URL.Query().Get("AWSAccessKeyId") != "" {
   277  
   278  			w.Header().Set("Location", getMinIOServer())
   279  			w.WriteHeader(http.StatusBadRequest)
   280  			w.Write([]byte(apiRequestErr))
   281  			return
   282  		}
   283  		next.ServeHTTP(w, r)
   284  	})
   285  }
   286  
   287  func AuthenticationMiddleware(next http.Handler) http.Handler {
   288  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   289  		token, err := auth.GetTokenFromRequest(r)
   290  		if err != nil && err != auth.ErrNoAuthToken {
   291  			http.Error(w, err.Error(), http.StatusUnauthorized)
   292  			return
   293  		}
   294  		sessionToken, _ := auth.DecryptToken(token)
   295  		// All handlers handle appropriately to return errors
   296  		// based on their swagger rules, we do not need to
   297  		// additionally return error here, let the next ServeHTTPs
   298  		// handle it appropriately.
   299  		if len(sessionToken) > 0 {
   300  			r.Header.Add("Authorization", fmt.Sprintf("Bearer  %s", string(sessionToken)))
   301  		} else {
   302  			r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "Anonymous"))
   303  		}
   304  		ctx := r.Context()
   305  		claims, _ := auth.ParseClaimsFromToken(string(sessionToken))
   306  		if claims != nil {
   307  			// save user session id context
   308  			ctx = context.WithValue(r.Context(), utils.ContextRequestUserID, claims.STSSessionToken)
   309  		}
   310  		next.ServeHTTP(w, r.WithContext(ctx))
   311  	})
   312  }
   313  
   314  // FileServerMiddleware serves files from the static folder
   315  func FileServerMiddleware(next http.Handler) http.Handler {
   316  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   317  		w.Header().Set("Server", globalAppName) // do not add version information
   318  		switch {
   319  		case strings.HasPrefix(r.URL.Path, "/ws"):
   320  			serveWS(w, r)
   321  		case strings.HasPrefix(r.URL.Path, "/api"):
   322  			next.ServeHTTP(w, r)
   323  		default:
   324  			buildFs, err := fs.Sub(portal_ui.GetStaticAssets(), "build")
   325  			if err != nil {
   326  				panic(err)
   327  			}
   328  			wrapHandlerSinglePageApplication(requestBounce(http.FileServer(http.FS(buildFs)))).ServeHTTP(w, r)
   329  		}
   330  	})
   331  }
   332  
   333  type notFoundRedirectRespWr struct {
   334  	http.ResponseWriter // We embed http.ResponseWriter
   335  	status              int
   336  }
   337  
   338  func (w *notFoundRedirectRespWr) WriteHeader(status int) {
   339  	w.status = status // Store the status for our own use
   340  	if status != http.StatusNotFound {
   341  		w.ResponseWriter.WriteHeader(status)
   342  	}
   343  }
   344  
   345  func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
   346  	if w.status != http.StatusNotFound {
   347  		return w.ResponseWriter.Write(p)
   348  	}
   349  	return len(p), nil // Lie that we successfully wrote it
   350  }
   351  
   352  // handleSPA handles the serving of the React Single Page Application
   353  func handleSPA(w http.ResponseWriter, r *http.Request) {
   354  	basePath := "/"
   355  	// For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE
   356  	if v := r.URL.Query().Get("cp"); v == "y" {
   357  		if base := r.URL.Query().Get("cpb"); base != "" {
   358  			// make sure the subpath has a trailing slash
   359  			if !strings.HasSuffix(base, "/") {
   360  				base = fmt.Sprintf("%s/", base)
   361  			}
   362  			basePath = base
   363  		}
   364  	}
   365  
   366  	indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html")
   367  	if err != nil {
   368  		http.Error(w, err.Error(), http.StatusInternalServerError)
   369  		return
   370  	}
   371  
   372  	sts := r.URL.Query().Get("sts")
   373  	stsAccessKey := r.URL.Query().Get("sts_a")
   374  	stsSecretKey := r.URL.Query().Get("sts_s")
   375  	overridenStyles := r.URL.Query().Get("ov_st")
   376  
   377  	// if these three parameters are present we are being asked to issue a session with these values
   378  	if sts != "" && stsAccessKey != "" && stsSecretKey != "" {
   379  		creds := credentials.NewStaticV4(stsAccessKey, stsSecretKey, sts)
   380  		consoleCreds := &ConsoleCredentials{
   381  			ConsoleCredentials: creds,
   382  			AccountAccessKey:   stsAccessKey,
   383  		}
   384  		sf := &auth.SessionFeatures{}
   385  		sf.HideMenu = true
   386  		sf.ObjectBrowser = true
   387  
   388  		if overridenStyles != "" {
   389  			err := ValidateEncodedStyles(overridenStyles)
   390  			if err != nil {
   391  				http.Error(w, err.Error(), http.StatusInternalServerError)
   392  				return
   393  			}
   394  
   395  			sf.CustomStyleOB = overridenStyles
   396  		}
   397  
   398  		sessionID, err := login(consoleCreds, sf)
   399  		if err != nil {
   400  			http.Error(w, err.Error(), http.StatusInternalServerError)
   401  			return
   402  		}
   403  
   404  		cookie := NewSessionCookieForConsole(*sessionID)
   405  
   406  		http.SetCookie(w, &cookie)
   407  
   408  		// Allow us to be iframed
   409  		w.Header().Del("X-Frame-Options")
   410  	}
   411  
   412  	indexPageBytes, err := io.ReadAll(indexPage)
   413  	if err != nil {
   414  		http.Error(w, err.Error(), http.StatusInternalServerError)
   415  		return
   416  	}
   417  
   418  	// if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else`
   419  	if basePath != "/" {
   420  		indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath)
   421  		// if we have a custom subpath replace it in
   422  	} else if getSubPath() != "/" {
   423  		indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath())
   424  	}
   425  	indexPageBytes = replaceLicense(indexPageBytes)
   426  
   427  	mimeType := mimedb.TypeByExtension(filepath.Ext(r.URL.Path))
   428  
   429  	if mimeType == "application/octet-stream" {
   430  		mimeType = "text/html"
   431  	}
   432  
   433  	w.Header().Set("Content-Type", mimeType)
   434  	http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes))
   435  }
   436  
   437  // wrapHandlerSinglePageApplication handles a http.FileServer returning a 404 and overrides it with index.html
   438  func wrapHandlerSinglePageApplication(h http.Handler) http.HandlerFunc {
   439  	return func(w http.ResponseWriter, r *http.Request) {
   440  		if r.URL.Path == "/" {
   441  			handleSPA(w, r)
   442  			return
   443  		}
   444  
   445  		w.Header().Set("Content-Type", mimedb.TypeByExtension(filepath.Ext(r.URL.Path)))
   446  		nfw := &notFoundRedirectRespWr{ResponseWriter: w}
   447  		h.ServeHTTP(nfw, r)
   448  		if nfw.status == http.StatusNotFound {
   449  			handleSPA(w, r)
   450  		}
   451  	}
   452  }
   453  
   454  type nullWriter struct{}
   455  
   456  func (lw nullWriter) Write(b []byte) (int, error) {
   457  	return len(b), nil
   458  }
   459  
   460  // As soon as server is initialized but not run yet, this function will be called.
   461  // If you need to modify a config, store server instance to stop it individually later, this is the place.
   462  // This function can be called multiple times, depending on the number of serving schemes.
   463  // scheme value will be set accordingly: "http", "https" or "unix"
   464  func configureServer(s *http.Server, _, _ string) {
   465  	// Turn-off random logger by Go net/http
   466  	s.ErrorLog = log.New(&nullWriter{}, "", 0)
   467  }
   468  
   469  func getSubPath() string {
   470  	subPathOnce.Do(func() {
   471  		cfgSubPath = parseSubPath(env.Get(SubPath, ""))
   472  	})
   473  	return cfgSubPath
   474  }
   475  
   476  func parseSubPath(v string) string {
   477  	v = strings.TrimSpace(v)
   478  	if v == "" {
   479  		return SlashSeparator
   480  	}
   481  	// Replace all unnecessary `\` to `/`
   482  	// also add pro-actively at the end.
   483  	subPath := path.Clean(filepath.ToSlash(v))
   484  	if !strings.HasPrefix(subPath, SlashSeparator) {
   485  		subPath = SlashSeparator + subPath
   486  	}
   487  	if !strings.HasSuffix(subPath, SlashSeparator) {
   488  		subPath += SlashSeparator
   489  	}
   490  	return subPath
   491  }
   492  
   493  func replaceBaseInIndex(indexPageBytes []byte, basePath string) []byte {
   494  	if basePath != "" {
   495  		validBasePath := regexp.MustCompile(`^[0-9a-zA-Z\/-]+$`)
   496  		if !validBasePath.MatchString(basePath) {
   497  			return indexPageBytes
   498  		}
   499  		indexPageStr := string(indexPageBytes)
   500  		newBase := fmt.Sprintf("<base href=\"%s\"/>", basePath)
   501  		indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
   502  		indexPageBytes = []byte(indexPageStr)
   503  
   504  	}
   505  	return indexPageBytes
   506  }
   507  
   508  func replaceLicense(indexPageBytes []byte) []byte {
   509  	indexPageStr := string(indexPageBytes)
   510  	newPlan := fmt.Sprintf("<meta name=\"minio-license\" content=\"%s\" />", InstanceLicensePlan.String())
   511  	indexPageStr = strings.Replace(indexPageStr, "<meta name=\"minio-license\" content=\"agpl\"/>", newPlan, 1)
   512  	indexPageBytes = []byte(indexPageStr)
   513  	return indexPageBytes
   514  }
   515  
   516  func requestBounce(handler http.Handler) http.Handler {
   517  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   518  		if strings.HasSuffix(r.URL.Path, "/") {
   519  			http.NotFound(w, r)
   520  			return
   521  		}
   522  
   523  		handler.ServeHTTP(w, r)
   524  	})
   525  }