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 := ¬FoundRedirectRespWr{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 }