package server import ( "encoding/json" "fmt" "net/http" "path" "strconv" "strings" _ "net/http/pprof" "github.com/NYTimes/gziphandler" "github.com/bouk/httprouter" jhttprouter "github.com/influxdata/httprouter" "github.com/influxdata/influxdb/chronograf" "github.com/influxdata/influxdb/chronograf/oauth2" "github.com/influxdata/influxdb/chronograf/roles" ) const ( // JSONType the mimetype for a json request JSONType = "application/json" ) // MuxOpts are the options for the router. Mostly related to auth. type MuxOpts struct { Logger chronograf.Logger Develop bool // Develop loads assets from filesystem instead of bindata Basepath string // URL path prefix under which all chronograf routes will be mounted UseAuth bool // UseAuth turns on Github OAuth and JWT Auth oauth2.Authenticator // Auth is used to authenticate and authorize ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux)) StatusFeedURL string // JSON Feed URL for the client Status page News Feed CustomLinks map[string]string // Any custom external links for client's User menu PprofEnabled bool // Mount pprof routes for profiling } // NewMux attaches all the route handlers; handler returned servers chronograf. func NewMux(opts MuxOpts, service Service) http.Handler { hr := httprouter.New() /* React Application */ assets := Assets(AssetsOpts{ Develop: opts.Develop, Logger: opts.Logger, }) // Prefix any URLs found in the React assets with any configured basepath prefixedAssets := NewDefaultURLPrefixer(opts.Basepath, assets, opts.Logger) // Compress the assets with gzip if an accepted encoding compressed := gziphandler.GzipHandler(prefixedAssets) // The react application handles all the routing if the server does not // know about the route. This means that we never have unknown routes on // the server. hr.NotFound = compressed var router chronograf.Router = hr // Set route prefix for all routes if basepath is present if opts.Basepath != "" { router = &MountableRouter{ Prefix: opts.Basepath, Delegate: hr, } //The assets handler is always unaware of basepaths, so the // basepath needs to always be removed before sending requests to it hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound) } EnsureMember := func(next http.HandlerFunc) http.HandlerFunc { return AuthorizedUser( service.Store, opts.UseAuth, roles.MemberRoleName, opts.Logger, next, ) } _ = EnsureMember EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc { return AuthorizedUser( service.Store, opts.UseAuth, roles.ViewerRoleName, opts.Logger, next, ) } EnsureEditor := func(next http.HandlerFunc) http.HandlerFunc { return AuthorizedUser( service.Store, opts.UseAuth, roles.EditorRoleName, opts.Logger, next, ) } EnsureAdmin := func(next http.HandlerFunc) http.HandlerFunc { return AuthorizedUser( service.Store, opts.UseAuth, roles.AdminRoleName, opts.Logger, next, ) } EnsureSuperAdmin := func(next http.HandlerFunc) http.HandlerFunc { return AuthorizedUser( service.Store, opts.UseAuth, roles.SuperAdminStatus, opts.Logger, next, ) } rawStoreAccess := func(next http.HandlerFunc) http.HandlerFunc { return RawStoreAccess(opts.Logger, next) } ensureOrgMatches := func(next http.HandlerFunc) http.HandlerFunc { return RouteMatchesPrincipal( service.Store, opts.UseAuth, opts.Logger, next, ) } if opts.PprofEnabled { // add profiling routes router.GET("/debug/pprof/:thing", http.DefaultServeMux.ServeHTTP) } /* Documentation */ router.GET("/swagger.json", Spec()) router.GET("/docs", Redoc("/swagger.json")) /* API */ // Organizations router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations)) router.POST("/chronograf/v1/organizations", EnsureSuperAdmin(service.NewOrganization)) router.GET("/chronograf/v1/organizations/:oid", EnsureAdmin(service.OrganizationID)) router.PATCH("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.UpdateOrganization)) router.DELETE("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.RemoveOrganization)) // Mappings router.GET("/chronograf/v1/mappings", EnsureSuperAdmin(service.Mappings)) router.POST("/chronograf/v1/mappings", EnsureSuperAdmin(service.NewMapping)) router.PUT("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.UpdateMapping)) router.DELETE("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.RemoveMapping)) // Source Proxy to Influx; Has gzip compression around the handler influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx))) router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx) // Write proxies line protocol write requests to InfluxDB router.POST("/chronograf/v1/sources/:id/write", EnsureViewer(service.Write)) // Queries is used to analyze a specific queries and does not create any // resources. It's a POST because Queries are POSTed to InfluxDB, but this // only modifies InfluxDB resources with certain metaqueries, e.g. DROP DATABASE. // // Admins should ensure that the InfluxDB source as the proper permissions // intended for Chronograf Users with the Viewer Role type. router.POST("/chronograf/v1/sources/:id/queries", EnsureViewer(service.Queries)) // Annotations are user-defined events associated with this source router.GET("/chronograf/v1/sources/:id/annotations", EnsureViewer(service.Annotations)) router.POST("/chronograf/v1/sources/:id/annotations", EnsureEditor(service.NewAnnotation)) router.GET("/chronograf/v1/sources/:id/annotations/:aid", EnsureViewer(service.Annotation)) router.DELETE("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.RemoveAnnotation)) router.PATCH("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.UpdateAnnotation)) // All possible permissions for users in this source router.GET("/chronograf/v1/sources/:id/permissions", EnsureViewer(service.Permissions)) // Services are resources that chronograf proxies to router.GET("/chronograf/v1/sources/:id/services", EnsureViewer(service.Services)) router.POST("/chronograf/v1/sources/:id/services", EnsureEditor(service.NewService)) router.GET("/chronograf/v1/sources/:id/services/:kid", EnsureViewer(service.ServiceID)) router.PATCH("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.UpdateService)) router.DELETE("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.RemoveService)) // Service Proxy router.GET("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureViewer(service.ProxyGet)) router.POST("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPost)) router.PATCH("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPatch)) router.DELETE("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyDelete)) // Layouts router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts)) router.GET("/chronograf/v1/layouts/:id", EnsureViewer(service.LayoutsID)) // Users associated with Chronograf router.GET("/chronograf/v1/me", service.Me) // Set current chronograf organization the user is logged into router.PUT("/chronograf/v1/me", service.UpdateMe(opts.Auth)) // TODO(desa): what to do about admin's being able to set superadmin router.GET("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.Users))) router.POST("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.NewUser))) router.GET("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UserID))) router.DELETE("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.RemoveUser))) router.PATCH("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UpdateUser))) router.GET("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.Users))) router.POST("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.NewUser))) router.GET("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UserID))) router.DELETE("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.RemoveUser))) router.PATCH("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UpdateUser))) // Dashboards router.GET("/chronograf/v1/dashboards", EnsureViewer(service.Dashboards)) router.POST("/chronograf/v1/dashboards", EnsureEditor(service.NewDashboard)) router.GET("/chronograf/v1/dashboards/:id", EnsureViewer(service.DashboardID)) router.DELETE("/chronograf/v1/dashboards/:id", EnsureEditor(service.RemoveDashboard)) router.PUT("/chronograf/v1/dashboards/:id", EnsureEditor(service.ReplaceDashboard)) router.PATCH("/chronograf/v1/dashboards/:id", EnsureEditor(service.UpdateDashboard)) // Dashboard Cells router.GET("/chronograf/v1/dashboards/:id/cells", EnsureViewer(service.DashboardCells)) router.POST("/chronograf/v1/dashboards/:id/cells", EnsureEditor(service.NewDashboardCell)) router.GET("/chronograf/v1/dashboards/:id/cells/:cid", EnsureViewer(service.DashboardCellID)) router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.RemoveDashboardCell)) router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.ReplaceDashboardCell)) // Dashboard Templates router.GET("/chronograf/v1/dashboards/:id/templates", EnsureViewer(service.Templates)) router.POST("/chronograf/v1/dashboards/:id/templates", EnsureEditor(service.NewTemplate)) router.GET("/chronograf/v1/dashboards/:id/templates/:tid", EnsureViewer(service.TemplateID)) router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.RemoveTemplate)) router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.ReplaceTemplate)) // Databases router.GET("/chronograf/v1/sources/:id/dbs", EnsureViewer(service.GetDatabases)) router.POST("/chronograf/v1/sources/:id/dbs", EnsureEditor(service.NewDatabase)) router.DELETE("/chronograf/v1/sources/:id/dbs/:db", EnsureEditor(service.DropDatabase)) // Retention Policies router.GET("/chronograf/v1/sources/:id/dbs/:db/rps", EnsureViewer(service.RetentionPolicies)) router.POST("/chronograf/v1/sources/:id/dbs/:db/rps", EnsureEditor(service.NewRetentionPolicy)) router.PUT("/chronograf/v1/sources/:id/dbs/:db/rps/:rp", EnsureEditor(service.UpdateRetentionPolicy)) router.DELETE("/chronograf/v1/sources/:id/dbs/:db/rps/:rp", EnsureEditor(service.DropRetentionPolicy)) // Measurements router.GET("/chronograf/v1/sources/:id/dbs/:db/measurements", EnsureViewer(service.Measurements)) // Global application config for Chronograf router.GET("/chronograf/v1/config", EnsureSuperAdmin(service.Config)) router.GET("/chronograf/v1/config/auth", EnsureSuperAdmin(service.AuthConfig)) router.PUT("/chronograf/v1/config/auth", EnsureSuperAdmin(service.ReplaceAuthConfig)) // Organization config settings for Chronograf router.GET("/chronograf/v1/org_config", EnsureViewer(service.OrganizationConfig)) router.GET("/chronograf/v1/org_config/logviewer", EnsureViewer(service.OrganizationLogViewerConfig)) router.PUT("/chronograf/v1/org_config/logviewer", EnsureEditor(service.ReplaceOrganizationLogViewerConfig)) router.GET("/chronograf/v1/env", EnsureViewer(service.Environment)) allRoutes := &AllRoutes{ Logger: opts.Logger, StatusFeed: opts.StatusFeedURL, CustomLinks: opts.CustomLinks, } getPrincipal := func(r *http.Request) oauth2.Principal { p, _ := HasAuthorizedToken(opts.Auth, r) return p } allRoutes.GetPrincipal = getPrincipal router.Handler("GET", "/chronograf/v1/", allRoutes) var out http.Handler /* Authentication */ if opts.UseAuth { // Encapsulate the router with OAuth2 var auth http.Handler auth, allRoutes.AuthRoutes = AuthAPI(opts, router) allRoutes.LogoutLink = path.Join(opts.Basepath, "/oauth/logout") // Create middleware that redirects to the appropriate provider logout router.GET("/oauth/logout", Logout("/", opts.Basepath, allRoutes.AuthRoutes)) out = Logger(opts.Logger, FlushingHandler(auth)) } else { out = Logger(opts.Logger, FlushingHandler(router)) } return out } // AuthAPI adds the OAuth routes if auth is enabled. func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes) { routes := AuthRoutes{} for _, pf := range opts.ProviderFuncs { pf(func(p oauth2.Provider, m oauth2.Mux) { urlName := PathEscape(strings.ToLower(p.Name())) loginPath := path.Join("/oauth", urlName, "login") logoutPath := path.Join("/oauth", urlName, "logout") callbackPath := path.Join("/oauth", urlName, "callback") router.Handler("GET", loginPath, m.Login()) router.Handler("GET", logoutPath, m.Logout()) router.Handler("GET", callbackPath, m.Callback()) routes = append(routes, AuthRoute{ Name: p.Name(), Label: strings.Title(p.Name()), // AuthRoutes are content served to the page. When Basepath is set, it // says that all content served to the page will be prefixed with the // basepath. Since these routes are consumed by JS, it will need the // basepath set to traverse a proxy correctly Login: path.Join(opts.Basepath, loginPath), Logout: path.Join(opts.Basepath, logoutPath), Callback: path.Join(opts.Basepath, callbackPath), }) }) } rootPath := path.Join(opts.Basepath, "/chronograf/v1") logoutPath := path.Join(opts.Basepath, "/oauth/logout") tokenMiddleware := AuthorizedToken(opts.Auth, opts.Logger, router) // Wrap the API with token validation middleware. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cleanPath := path.Clean(r.URL.Path) // compare ignoring path garbage, trailing slashes, etc. if (strings.HasPrefix(cleanPath, rootPath) && len(cleanPath) > len(rootPath)) || cleanPath == logoutPath { tokenMiddleware.ServeHTTP(w, r) return } router.ServeHTTP(w, r) }), routes } func encodeJSON(w http.ResponseWriter, status int, v interface{}, logger chronograf.Logger) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { unknownErrorWithMessage(w, err, logger) } } // Error writes an JSON message func Error(w http.ResponseWriter, code int, msg string, logger chronograf.Logger) { e := ErrorMessage{ Code: code, Message: msg, } b, err := json.Marshal(e) if err != nil { code = http.StatusInternalServerError b = []byte(`{"code": 500, "message":"server_error"}`) } logger. WithField("component", "server"). WithField("http_status ", code). Error("Error message ", msg) w.Header().Set("Content-Type", JSONType) w.WriteHeader(code) _, _ = w.Write(b) } func invalidData(w http.ResponseWriter, err error, logger chronograf.Logger) { Error(w, http.StatusUnprocessableEntity, fmt.Sprintf("%v", err), logger) } func invalidJSON(w http.ResponseWriter, logger chronograf.Logger) { Error(w, http.StatusBadRequest, "unparsable JSON", logger) } func unknownErrorWithMessage(w http.ResponseWriter, err error, logger chronograf.Logger) { Error(w, http.StatusInternalServerError, fmt.Sprintf("unknown error: %v", err), logger) } func notFound(w http.ResponseWriter, id interface{}, logger chronograf.Logger) { Error(w, http.StatusNotFound, fmt.Sprintf("ID %v not found", id), logger) } func paramID(key string, r *http.Request) (int, error) { ctx := r.Context() param := jhttprouter.ParamsFromContext(ctx).ByName(key) id, err := strconv.Atoi(param) if err != nil { return -1, fmt.Errorf("error converting ID %s", param) } return id, nil } func paramStr(key string, r *http.Request) (string, error) { ctx := r.Context() param := jhttprouter.ParamsFromContext(ctx).ByName(key) return param, nil }