408 lines
16 KiB
Go
408 lines
16 KiB
Go
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/v2/chronograf"
|
|
"github.com/influxdata/influxdb/v2/chronograf/oauth2"
|
|
"github.com/influxdata/influxdb/v2/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
|
|
}
|